/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/* ==========================================================================
 * The operator's chrome
 *
 * App-level scaffolding, and the only place it may live: the design system's
 * own files are in app/assets/stylesheets/gameon, which bin/sync-design-system
 * owns and prunes of anything it did not write.
 *
 * Nearly all of it is /admin. The exceptions are .admin-peek at the foot of
 * this file, .bga-home-list below, and .news-* at the very end. All three
 * render on PUBLIC pages. (So does .offers-abroad, beside .admin-peek: the
 * shops abroad in a ficha's «Dónde comprarlo».) The first is operator chrome, the second fixes the
 * height of one public list, and the third is the only page-specific LAYOUT
 * this app owns — «Noticias» set as a front page, which is a composition of
 * the system's tokens and not a component the system could hold.
 *
 * Layout only, and deliberately so. Every colour, height, radius and padding an
 * admin screen renders comes from a .go- class, so the panel keeps lining up
 * with the rest of the site for free — the moment a rule here grows a colour it
 * has started to fight the system. Two things invent a face: .admin-kbd and
 * .admin-swatch, neither of which the system has a primitive for; both are
 * candidates to upstream. (.admin-raw is not one of them — it caps a height and
 * wraps a line, and takes its type from .go-caption like everything else.)
 * .admin-peek borrows .go-badge and its own modifiers, and adds position plus
 * one hard shadow drawn in --go-color-brand — a token, not a colour invented
 * here. .admin-hit paints in tokens for the same reason and invents nothing:
 * it is .go-mark with the marker pen taken off, because the system's highlight
 * is sized for a heading and this one goes inside a sentence.
 * ========================================================================== */

/* The BGA tile keeps one stable height through every Turbo page. Its actions
 * sit on one line, so an announced game with two of them is no taller than a
 * published one with one.
 *
 * The pin is the whole rule: the row does NOT clip. It did while the actions
 * were a column and a pair genuinely outgrew the row, and that clip outlived
 * them. A control in this system is not just its border box — a `.go-btn`
 * carries a 2px hard shadow (`--go-shadow-xs`) beside and under it, and
 * `.go-btn:active` moves it a pixel further again — and the actions sit FLUSH
 * against the row's trailing edge, so a clip box there cut the shadow off the
 * last one and left a round button with a flat right side.
 *
 * Nothing is left for it to clip either: 64px pinned, less 8px of padding,
 * leaves 56px for a 40px cover, a title and a meta line that both ellipsize,
 * and one 24px line of actions. test/system/bga_actions_test.rb measures each
 * action's shadow against every box that could cut it. */
.bga-home-list .bga-home-row {
  height: var(--go-space-16);
  padding-block: var(--go-space-1);
}

.bga-home-list > .bga-home-row + .bga-home-row {
  padding-top: calc(var(--go-space-1) - var(--go-border-width));
}

/* ALIGNMENT: the actions sit on the game name's line, on every row of the tile.
 *
 * `.go-row-item` centres its aside against the ROW, and this row is pinned at
 * 64px while its body is 24px on an announced game and 40px on a published one
 * — a title alone, or a title over a meta line. Centred, the aside lands on the
 * title of the first and 8px under the title of the second, which is the offset
 * this rule exists to remove.
 *
 * `align: :top` is what «Tiendas» and «Novedades en tiendas» use for the same
 * problem, and it is wrong here: those rows take their height from a cover, so
 * top and centre are the same place. This one takes its height from the pin
 * above, so top-aligning drops the cover, the badge and the title 16px up an
 * announced game's row and leaves a hole under them.
 *
 * So the aside is centred WITH the meta line's height hung under it, which
 * makes its margin box exactly the body's box: the two centre together, and the
 * aside's one line is the title's line. A row with no meta line hangs nothing
 * and is already there. `--go-leading-xs` is `.go-row-item__meta`'s own line
 * height, not a measurement taken from this row.
 *
 * Measured at 1280px and at 390px by test/system/bga_actions_test.rb, which
 * compares the two boxes on every row of the tile rather than on the one shape
 * a fixture happens to render.
 *
 * The actions sit side by side rather than stacked, and that takes no rule at
 * all: `shared/_bga_row` puts them in a `.go-cluster`, which the system already
 * ships. */
.bga-home-row:has(.go-row-item__meta) .go-row-item__aside {
  margin-block-end: var(--go-leading-xs);
}

/* A name that leads to our ficha (shared/_bga_row) keeps the row's title
 * weight: `.go-link` is 600 and the titles beside it are 700, so a linked
 * name read lighter than its unlinked neighbours. */
.bga-home-row .go-row-item__title > .go-link { font-weight: inherit; }

/* The pager is the tile's foot band (`.go-tile__links`), drawn inside the
 * Turbo frame so it changes with the page. Only the way that exists is drawn,
 * and «Siguiente» keeps the right-hand end whether or not «Anterior» is there,
 * so it does not jump left on page 1. */
.bga-home-pager {
  display: flex;
  gap: var(--go-space-4);
}
.bga-home-pager > [rel="next"] { margin-inline-start: auto; }

/* The optional home-page masonry keeps the configured twelve-column widths,
 * then divides each panel's measured height into 4px rows. Stimulus adds the
 * ready class only after it measures the panels, so the regular bento remains
 * a complete fallback when JavaScript is unavailable. */
.go-bento--stacked.go-bento--masonry-ready {
  grid-auto-flow: row dense;
  grid-auto-rows: var(--go-space-1);
  row-gap: 0;
}

/* The shell: a fixed rail and a content column that may shrink. minmax(0,1fr)
 * rather than 1fr, or a wide .go-table inside it blows the column out instead
 * of scrolling within .go-table-scroll. */
.admin-shell {
  display: grid;
  grid-template-columns: 232px minmax(0, 1fr);
  gap: var(--go-space-6);
  align-items: start;
}

/* 64px of app bar plus the shell's own 24px, so a scrolled rail clears the
 * sticky bar exactly. */
.admin-shell__side {
  position: sticky;
  top: calc(var(--go-space-16) + var(--go-space-6));
  padding-block: var(--go-space-6);
  /* The registry can be taller than a short desktop viewport. A sticky box
   * taller than the space below its top edge strands its last destinations
   * below the fold, because page scrolling keeps the box pinned. Give the
   * rail that remaining viewport height and let it scroll, the same bargain
   * the phone app-bar menu makes in the design system. */
  max-height: calc(100dvh - var(--go-space-16) - var(--go-space-6));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.admin-shell__main {
  padding-block: var(--go-space-6);
  min-width: 0;
}

/* «Movimiento» — the tablero's fortnight chart. Four small charts stacked, each
 * scaled to its own peak, sharing one axis at the foot (admin/dashboard/
 * _movimiento explains why they are four and not one).
 *
 * Layout and one token per rule, like everything else in this file. The colour
 * arrives as --admin-chart-ink, set inline from Admin::Activity::FEEDS, and is
 * a design-system token in every case; nothing here invents one.
 *
 * The column count is NOT written down. `grid-auto-flow: column` with
 * `grid-auto-columns: 1fr` gives every day an equal share of whatever there is,
 * so Admin::Activity::SPAN is the only place the number 14 exists — and the
 * plots and the axis share the rule, which is what makes a tick stand under its
 * own bars. */
.admin-chart {
  display: grid;
  gap: var(--go-space-4);
}

/* 16px of heading + 4 + a 48px plot = 68, and 68 is on the 4px grid. Nothing
 * inside this panel may be a multiple of anything else: the jobs list sits
 * below it, and the alignment gate checks the absolute offset of its rows. */
.admin-chart__line {
  display: grid;
  gap: var(--go-space-1);
}

/* The line's own colour, beside its name. It is the whole legend: a chart whose
 * four hues clear the colour-blindness gate only WITH a second channel cannot
 * put that channel on another part of the page. */
.admin-chart__key {
  flex: none;
  width: var(--go-space-2);
  height: var(--go-space-2);
  border-radius: var(--go-radius-xs);
  background: var(--admin-chart-ink);
}

.admin-chart__plot,
.admin-chart__axis {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--go-border-width);
}

/* ALIGNMENT: the baseline is a box-shadow, exactly as .go-appbar draws the rule
 * under itself and for the same reason. A 1px border here is 1px of real flow
 * on a 48px box, four times over, and the whole right-hand column of the
 * tablero lands half a grid step low. A shadow draws the same hairline and
 * occupies nothing. */
.admin-chart__plot {
  height: var(--go-space-12);
  box-shadow: 0 var(--go-border-width-hair) 0 var(--go-color-line-soft);
}

.admin-chart__col {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  min-width: 0;
}

/* Thin, capped, square where it meets the baseline and rounded at the end that
 * carries the value. The 2px floor is the reason one listing against a peak of
 * four hundred is still a mark on the page: below it the bar rounds away to
 * nothing and an arrival reads as a day off. An empty day draws no bar at all —
 * that is the template's decision, and it is what keeps the floor honest. */
.admin-chart__bar {
  width: 100%;
  max-width: var(--go-space-4);
  height: max(var(--go-border-width), var(--admin-chart-share));
  border-radius: var(--go-space-1) var(--go-space-1) 0 0;
  background: var(--admin-chart-ink);
}

/* Two 16px lines: the day's name over its number. Both are needed and neither
 * is enough — «mié» alone repeats twice in a fortnight, and «27» alone is the
 * thing nobody can date without counting back. */
.admin-chart__tick {
  display: grid;
  justify-items: center;
  min-width: 0;
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  color: var(--go-color-ink-subtle);
}

/* Today, which is the one column that is still filling. The caption under the
 * chart says so in words; this is what lets the eye find it. */
.admin-chart__tick--today {
  color: var(--go-color-ink);
  font-weight: var(--go-weight-bold);
}

/* A closed <details> has no geometry, which is why the table of numbers may sit
 * inside this panel at all without the gate having an opinion about it. */
.admin-chart__numbers > summary {
  cursor: pointer;
  width: fit-content;
}

/* A run-log row may grow taller than its dense-table minimum when an error
 * wraps. Collapsed cell borders then split a pixel between the two rows and
 * make the whole panel half a pixel tall. The shop page stacks its facts below
 * that panel at narrower widths, so use separate borders for integer geometry. */
.admin-store-log .go-table { border-collapse: separate; border-spacing: 0; }

/* One unbounded field may not decide how wide a table is.
 *
 * The system already means for these to truncate: .go-cell-title and
 * .go-cell-meta both carry overflow/text-overflow/white-space, and
 * .go-cell-media__body carries min-width: 0. None of it does anything under
 * `table-layout: auto` without a CEILING to truncate against — the cell asks
 * for its max-content width and the column is sized to the answer. One
 * campaign's 316-character publisher credit («River Dragons Giant») took
 * «Campaña» to 1 323px inside a 1 044px panel and pushed the other seven
 * columns — plataforma, juego, alta, cierra, recaudado, seguidores, estado —
 * entirely off the right edge, at EVERY desktop width: the content column caps
 * at 1 044, so a wider monitor bought nothing. Measured 2 077px of table in a
 * 1 044px scroller, 1 033px of it unreachable without finding the horizontal
 * scrollbar under forty rows.
 *
 * Keyed to .go-table-scroll, which is the panel's own wrapper and appears
 * nowhere in the public views, so this is every admin table with a media cell
 * and not a special case for the one row that exposed it. The phone never had
 * the bug — below 700px .go-table--stack turns each row into a card — so the
 * ceiling is lifted there rather than fighting it.
 *
 * ch and not px: the cap is a reading length, and it should follow the font. */
.go-table-scroll .go-cell-media__body,
.go-table-scroll .go-cell-title,
.go-table-scroll .go-cell-meta { max-width: var(--admin-cell-max, 38ch); }

@media (max-width: 700px) {
  .go-table-scroll .go-cell-media__body,
  .go-table-scroll .go-cell-title,
  .go-table-scroll .go-cell-meta { max-width: none; }
}

/* At tablet and phone widths the facts column follows the runs rather than
 * sitting beside it. The dynamic log table leaves that first column two pixels
 * short of a complete grid unit, so close the unit before the next column. */
@media (max-width: 1120px) {
  .admin-store-runs { padding-bottom: var(--go-border-width); }
}

/* Below 760px there is no room for a column beside the content, so the rail
 * moves into the app bar's menu (.go-appbar__menu) and the shell becomes the
 * single column it already wants to be. The rail is not restyled to get there
 * — it keeps the vertical shape it is designed for, indents and count badges
 * and all — it is only rendered somewhere else.
 *
 * The aside is what goes away, not the rail: the layout renders the menu on
 * every panel screen, including «Cola de decisiones», which composes no shell
 * at all and would otherwise have no navigation on a phone. */
@media (max-width: 760px) {
  .admin-shell {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }

  .admin-shell__side { display: none; }
}

/* «Cola de decisiones» used to be lifted here, at phone widths only: the
 * callout was always last in the source and .admin-dash__panels carried an
 * `order: 1` that pushed the grid below it.
 *
 * The tablero renders it above the stats now whenever anything is pending, at
 * every width — the queue spans several backlogs and a hundred undecided records
 * is not something to find at the bottom of a page — so the phone had a rule
 * saying one thing and the desktop a template saying another. The template
 * won: it can tell an empty queue from a full one, and a CSS rule cannot.
 * Nothing app-level replaces this. */

/* The app bar's phone menu used to be .admin-menu, here. It is .go-appbar__menu
 * in the design system now: the public bar grew the same menu the moment its
 * seven pills stopped fitting on a phone, and two bars with the same problem at
 * the same width should not have two rules. Nothing app-level replaces it. */

/* «Cola de decisiones» is one decision at a time, so it reads as a column and
 * not as a page. It has no rule of its own: .go-container--narrow is already
 * the system's 760px measure, and a second copy of that number here would stop
 * tracking it. The queue is
 *   <div class="go-container go-container--narrow go-section"> … </div>
 */

/* The badges share a right edge the way the icons share a left one. A section
 * row spends its last 40px on the caret — 12 + 16 + 12, which is
 * --go-control-h-md because the button is square — so a bubble on «Listados»
 * sits that far in while one on «Cola de decisiones» would sit flush. Every row
 * WITHOUT a caret of its own reserves the same gutter, and the column of
 * numbers lines up. The rows that have one are left alone: their bubble is
 * inside the button, ahead of the caret, which lands it on the same edge. */
.admin-rail > .go-railitem,
.admin-rail__well > .go-railitem {
  padding-inline-end: var(--go-control-h-md);
}

/* A section of the rail — «Listados» and its two views, «Juegos» and its three
 * backlogs, «Canales» and what its walks bring back, «Eventos» and the two
 * calendars under it. Which items those are is Admin::Screen's `parent`; this
 * is only how they look, and which of them is open is the `disclosure`
 * controller and the `hidden` attribute on the well.
 *
 * THE FRAME IS AN OUTLINE, and on the section rather than on the row. An
 * outline costs no layout at all, and that is the whole reason for it here: a
 * 2px border would make a shut section 44px where every other row in the rail
 * is 40, so three or four rows would sit a grid unit further apart than the
 * rest — and it would need a negative inline margin to stop the frame indenting
 * the row inside it. With an outline the section measures 40 shut and 40 + 40·n
 * open, the content box is exactly where a loose row's is, and every icon in
 * the rail keeps one left edge with no compensation anywhere.
 *
 * There is no indent on a child any more. The hierarchy is the box.
 *
 * All four sides, because an outline has no sides; the bottom one lands on the
 * well's own background and cannot be seen, which is why this reads as the
 * left/top/right frame it was asked for. It is drawn 2px outside the rail's
 * column into .go-container's own 12–20px of padding, and it is 2px into a 4px
 * gap between rows, so it overlaps nothing and overflows nothing. */
.admin-rail__section {
  outline: var(--go-border-width) solid transparent;
  border-radius: var(--go-radius-md);
  min-width: 0;
}

/* Open, the frame takes the well's own colour, so the row and the items under
 * it read as one block instead of as a row that happens to sit above a tint.
 * Shut, it is transparent — the same 2px, so nothing moves between the two
 * states — and the section is one more row in a flat list. */
.admin-rail__section:has(> .admin-rail__well:not([hidden])) {
  outline-color: var(--go-color-surface-sunk);
}

/* The row: the destination, what is waiting under it, and the button that opens
 * it. `.go-railitem` gives every one of those its 40px and its padding, so the
 * caret is a 40×40 target and not a 16px glyph. */
.admin-rail__head {
  display: flex;
  align-items: center;
  min-width: 0;
}

/* `text-align` because «Eventos» is a <button>, and a button centres its text.
 * Every other row in the rail is an <a>, so the one section with nowhere to go
 * would be the one row whose label does not start where the others do. */
.admin-rail__head > .go-railitem {
  flex: 1 1 auto;
  text-align: start;
}

/* The caret keeps its 40px and gives the rest of the row to the destination.
 * Same specificity as the rule above, or the button grows too and the caret
 * lands in the middle of the rail. */
.admin-rail__head > .admin-rail__disclose { flex: none; }

.admin-rail__rollup { flex: none; }

/* Right when shut, down when open. A quarter turn keeps the icon's box 16×16,
 * which is what the alignment gate measures. */
.admin-rail__caret { flex: none; }

.admin-rail__section:has(> .admin-rail__well:not([hidden])) .admin-rail__caret {
  transform: rotate(90deg);
}

/* What is waiting under a shut section, added up. It goes the moment the
 * section opens, because every child then carries its own — two bubbles saying
 * the same thing is how a rail starts lying about one of them. */
.admin-rail__section:has(> .admin-rail__well:not([hidden])) .admin-rail__rollup {
  display: none;
}

/* The well the children sit in.
 *
 * ONE recessed surface holding the whole run, not a tint per item: `gap: 0`
 * inside it, so the children read as one block rather than as separate things
 * that happen to share a colour. Flat — a sunk background and the system's own
 * radius, no inset shadow, no bevel.
 *
 * No vertical padding, for the grid: a 40px item with no gap between siblings
 * makes the well 40·n tall. Any padding here would move every item after it off
 * the 4px grid, which the alignment gate measures on the page's absolute
 * offsets.
 *
 * The items keep every height, radius and colour .go-railitem gives them; hover
 * and aria-current still win. A selected child has to look selected first and
 * grouped second. */
.admin-rail__well {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--go-color-surface-sunk);
  border-end-start-radius: var(--go-radius-md);
  border-end-end-radius: var(--go-radius-md);
  min-width: 0;
}

/* The join between the row and the well below it. The row squares its bottom
 * corners and the first child squares its top ones — both only show when that
 * row is hovered or is the current page, which is exactly when a rounded corner
 * in the middle of the block would be visible. A shut section has no well under
 * it, so its row keeps every corner it has. */
.admin-rail__section:has(> .admin-rail__well:not([hidden])) .admin-rail__head > .go-railitem {
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

/* And the children among themselves, for the third time and the same reason.
 * A child is exactly as wide as the well, so its own radius put a rounded
 * corner inside a block of another colour: «Transcripciones» selected in the
 * middle of «Canales» read as a pill dropped into the well rather than as one
 * row of it. Square every corner and a child's tint fills the well edge to
 * edge.
 *
 * The LAST child takes the well's own bottom corners back. There its corner
 * and the well's are the same corner — the bottom of the whole block, against
 * the rail — and squaring it would show the well's rounded corner peeping out
 * from under a square tint. The first child needs no matching rule: the row
 * above it already squares its bottom edge, so the top of the well is flat on
 * both sides of that join. */
.admin-rail__well > .go-railitem {
  border-radius: 0;
}

.admin-rail__well > .go-railitem:last-child {
  border-end-start-radius: var(--go-radius-md);
  border-end-end-radius: var(--go-radius-md);
}

/* The join between the destination and the caret beside it, for the same reason
 * and in the same direction: the two are one row, and a rounded corner on each
 * facing edge drew a seam down the middle of it.
 *
 * The two halves tint on their own — hover is per element, and the current
 * page is one of them — so the row shows one tinted half beside a bare one,
 * or two halves in two different colours. A radius on each facing edge drew
 * a seam down the middle of both.
 *
 * Square both facing edges and the tint ends where the other half starts. The
 * outer edges keep the radius: the row's silhouette against the rail is
 * unchanged.
 *
 * Keyed on the caret being there. «Eventos» is the one section with nowhere to
 * go, so its whole row is a single button with no join in it, and it keeps
 * every corner. */
.admin-rail__head > .go-railitem:has(+ .admin-rail__disclose) {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}

.admin-rail__head > .admin-rail__disclose {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
}

/* A table row that navigates, and the anchor inside it that is the real link.
 *
 * The row's own affordance is the system's (`.go-table tbody tr:hover td`
 * tints it); all this adds is the pointer, so what looks clickable is what is.
 *
 * The anchor takes no colour and no size of its own — `inherit` and
 * `currentColor` only — so the cell keeps the type the system gives it. What it
 * drops is the underline: fifty underlined titles in a table whose every row is
 * clickable is the same fact stated once per row. It comes back on hover and on
 * keyboard focus, where it is the only signal there is. The clipping is
 * repeated here because the anchor, not the cell, is now the block that
 * overflows. */
tr[data-controller~="row-link"] { cursor: pointer; }

.admin-rowlink {
  display: block;
  color: inherit;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-rowlink:hover,
.admin-rowlink:focus-visible {
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* Machine text an operator has to READ: a model's reply, a backtrace, the
 * prompt a comparison froze, an answer no form box can hold.
 *
 * Every one of those blocks used to be a <pre> inside .go-scroller, and that
 * class is the wrong tool for all of them. It is an overflow valve for a row of
 * chips: a flex row that never wraps its children, scrolls sideways and hides
 * the scrollbar. A <pre> in it is one child of unbounded width, so a
 * 2 000-character JSON reply rendered as a single line, clipped at the panel's
 * edge, with nothing on screen to say there was more of it. That is how «Texto
 * tal cual llegó» arrived unreadable on the comparison screen.
 *
 * So the block wraps instead of scrolling sideways, and caps its height instead
 * of letting a long backtrace push everything under it off the page. Both
 * numbers are whole multiples of the 16px line box these blocks are set in
 * (.go-caption), so the box lands on the 4px grid at two lines, at forty, and
 * capped — which is what test/system/alignment_test.rb measures.
 *
 * `overflow-wrap: anywhere` rather than `break-word`: a minified JSON reply has
 * no spaces to break at, and `break-word` leaves that line as wide as it likes.
 *
 * `resize: vertical` because the cap is the sideways clip turned ninety
 * degrees: a body two lines past 240px hides those two lines, and an overlay
 * scrollbar — which is what a trackpad gets — appears only once somebody has
 * already scrolled. The handle is on screen before that, and a `stop_reason`
 * on line eighteen is worth nothing if the box does not say it can be opened.
 * Dragging it changes nothing else on the page: the block is in flow and the
 * panel below it moves with it.
 *
 * Callers add `tabindex="0"`. Chrome gives a scrollable region no keyboard
 * focus of its own, so without it the only way past the cap is a mouse. */
.admin-raw {
  max-height: calc(var(--go-leading-xs) * 15);
  resize: vertical;
  overflow: auto;
  overscroll-behavior: contain;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The phrase that was searched for, inside a transcript excerpt — and, on a
 * game's ficha, the game's name inside the line of a video that says it
 * (.transcript-hit, see .video-quote).
 *
 * The brand yellow and the system's own small radius, and nothing else: no
 * padding and no shadow, so a hit costs the line it sits in not one pixel of
 * width or height and five excerpts stay on the grid. That is why it is not
 * .go-mark — the system's highlight is the marker pen from the direction, sized
 * for a word in a 40px heading, and at caption size in the middle of a sentence
 * its padding pushes the neighbouring words apart and its hard shadow reads as
 * a button. Same token, laid flat.
 *
 * The browser's own <mark> is the other thing this replaces: Canary yellow with
 * black text is a colour this palette does not contain. */
.admin-hit,
.transcript-hit {
  background: var(--go-color-brand);
  border-radius: var(--go-radius-xs);
  color: var(--go-color-ink);
  /* A quoted line wraps, and a phrase that wraps with it is one box the
   * browser slices in two: without this the corners at the break are square
   * on one half and round on the other. .go-mark carries the same pair. */
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
}

/* Where a video names the game, in its row on the game's ficha: the minute
 * as a button, then the quoted captions (YoutubeGameMention::Quote).
 *
 * Side by side and baseline-aligned, so the minute reads as the first word of
 * the quote and the paragraph keeps a caption's line box. A button INSIDE the
 * paragraph would make every line it sits on 4px taller than the rest.
 *
 * The quote wraps under the minute when the row's body is too narrow to hold
 * both: on a phone the body is ~170px beside the thumbnail, and a quote beside
 * the button there was a column of two words a line.
 *
 * The rule on its left is what tells the quote from the summary under it: both
 * are captions, and without it «…might work.» ran straight into «Tom Vasel
 * responde…» as one paragraph. */
.video-quote {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--go-space-1) var(--go-space-2);
}

.video-quote > p {
  flex: 1 1 16em;
  min-width: 0;
  overflow-wrap: break-word;
  padding-inline-start: var(--go-space-2);
  border-inline-start: 2px solid var(--go-color-line-medium);
  color: var(--go-color-ink);
}

/* A keycap. The system has no kbd primitive, and the queue is driven from the
 * keyboard, so this is the one invented thing in the panel. The 1px border
 * with a 2px bottom is the whole illusion; it is inline, so it takes its box
 * from the text around it rather than from the control scale. */
.admin-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding-inline: 6px;
  border: 1px solid var(--go-color-line);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--go-color-surface);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: 1;
  color: var(--go-color-ink);
}

/* «Portada» — the home page's layout screen.
 *
 * Three rules and no colours of their own. .admin-swatch is the second invented
 * thing in the panel, alongside .admin-kbd: the system has no colour-input
 * primitive, and left to the browser the control is ~23px tall, which drags the
 * whole table row off the 4px grid. It is sized from the control scale and
 * borrowed nothing else. */
.admin-layout__num  { width: 72px; }
.admin-layout__span { width: 84px; }

/* Cells that hold one control and must not stretch. width:1% is the shrink-to-
 * fit idiom .go-td-action already uses; the widths above are what stops the
 * 100%-wide .go-input inside from resolving against nothing. */
.admin-layout__cell { width: 1%; white-space: nowrap; }

.admin-swatch {
  width: var(--go-control-h-md);
  height: var(--go-control-h-md);
  flex: none;
  padding: var(--go-border-width);
  border: var(--go-border);
  border-radius: var(--go-radius-sm);
  background: var(--go-color-surface);
  cursor: pointer;
}

/* The browser draws its own inset around the colour; without these the swatch
 * reads as a 4px-smaller square floating inside a border. */
.admin-swatch::-webkit-color-swatch-wrapper { padding: 0; }
.admin-swatch::-webkit-color-swatch { border: 0; border-radius: var(--go-radius-xs); }
.admin-swatch::-moz-color-swatch { border: 0; border-radius: var(--go-radius-xs); }

/* The expansion action includes a game name from the database. A long name
 * must not make the fixed-height, nowrap button wider than its panel. Cap both
 * the form flex item and its button, then cut only the visible label with CSS.
 * The full text stays in the DOM and in the accessible name. */
.admin-expansion-mark-form,
.admin-expansion-mark { min-width: 0; max-width: 100%; }

.admin-expansion-mark__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* One panel in the layout preview. It is a stand-in for a .go-tile and wears
 * the same tinted offset shadow, which is what makes the accent legible at this
 * size — and it is drawn from --go-cat, so the preview and the page are tinted
 * by one value rather than by two that can disagree.
 *
 * The panel's OTHER colour has nowhere to go on a shadow, so the column count
 * inside carries .go-badge--cat: a badge is exactly what the design system
 * fills with --go-cat and writes in --go-cat-ink, which makes the one filled
 * thing in the preview the one that shows the pair.
 *
 * [hidden] is restated because this rule sets `display`, and a class beats the
 * UA stylesheet: without it, hiding a panel would do nothing here. */
.admin-preview__panel {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--go-space-2);
  height: var(--go-control-h-lg);
  padding-inline: calc(var(--go-space-3) - var(--go-border-width));
  border: var(--go-border);
  border-radius: var(--go-radius-md);
  background: var(--go-color-surface);
  box-shadow: var(--go-border-width) var(--go-border-width) 0 var(--go-cat);
  min-width: 0;
}

.admin-preview__panel[hidden] { display: none; }

.admin-preview__label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  font-weight: var(--go-weight-semibold);
}

/* The app bar is sticky and 64px, so an in-page anchor lands underneath it:
 * the rail's «Tiendas» item jumps to #tiendas and the heading it was aiming at
 * is the part that ends up hidden. scroll-margin is the property for exactly
 * this and costs no layout. 64 + 16 of air, both from the space scale. */
[id] {
  scroll-margin-top: calc(var(--go-space-16) + var(--go-space-4));
}

/* An autocomplete inside a panel body: the subject picker on /admin/llm, and
 * the base-game picker on «Expansión» in a game's inspector. Both are the
 * shared combobox in a place it was not measured for, and the three rules
 * below are what that place needs.
 *
 * `.go-combobox__panel` is right-aligned with a 320px floor, which is right
 * for the app bar's 200px search box — the panel is wider than its input and
 * hangs to the left of it. Inside a panel body the input is the full column,
 * so the same rule drops a narrow box against the far edge with a metre of
 * empty input beside it, reading as a detached tooltip rather than as this
 * field's own results.
 *
 * Constraining the combobox instead of overriding the panel keeps the shared
 * component untouched (bin/sync-design-system --check stays quiet) and lines
 * the two up: the input is now the panel's width, so `right: 0` is also
 * `left: 0`. */
.admin-picker {
  display: block;
  max-width: 32rem;
}

/* And the panel takes the input's width rather than its own content's. The
 * shared rule is `right: 0` with a 320px floor and no ceiling, which suits a
 * fixed-width box in the app bar: the panel grows past its input and hangs to
 * the left, where there is page to hang over. Here the input sits inside a
 * panel body, so a video title long enough to widen the list pushes it out of
 * the card it belongs to. Pinning both edges makes the results exactly as wide
 * as the field they belong to, and the shared `text-overflow: ellipsis` on the
 * title then has a width to work against. */
.admin-picker .go-combobox__panel {
  left: 0;
  right: 0;
  width: auto;
  min-width: 0;
}

/* And its options are buttons, not links, because picking one goes nowhere:
 * it ticks the radio below, or it posts the form it is the submit button of.
 * `.go-combobox__option` was written for the site search, where every option
 * IS a destination, so it carries no button reset —
 * without one the browser's own width, border and centred label come through,
 * and a long video title makes the list scroll sideways instead of
 * ellipsizing. */
.admin-picker .go-combobox__option {
  /* `width: 100%` on a button whose box-sizing has not been reset adds the
     shared class's inline padding on top of the full width — an overflow of
     exactly 2 × --go-space-3, which is the sideways scrollbar this rule set
     out to remove. */
  box-sizing: border-box;
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* The shared class puts `overflow: hidden; text-overflow: ellipsis` on these,
 * and they are spans — which is to say inline boxes, on which `overflow` does
 * nothing at all. In the app bar it never shows: that panel is 320px wide with
 * no ceiling, so a long result simply widens it over the page. Here the panel
 * is pinned to the field, so the title has to actually clip. Blockifying is
 * what gives the shared rule a box to work in. */
.admin-picker .go-combobox__title,
.admin-picker .go-combobox__meta {
  display: block;
}

/* --- the peek -------------------------------------------------------------
 * An operator reading the public site, one click from the screen that governs
 * whatever they are looking at. See AdminPeekHelper and admin_peek_controller.js
 * for who renders it; this file says where it sits, when it shows, and what a
 * host gives up to make room for it.
 *
 * It wears .go-badge, and that is load-bearing rather than taste. audit.js
 * measures .go-btn twice — for a control height AND for an absolute offset on
 * the 4px grid — and this element hangs off a host's corner, where its offset
 * is a function of that host's height rather than of anything the system
 * controls. A .go-btn here would fail the alignment gate on every public page
 * an operator opens. A badge is measured only for a height that is a multiple
 * of four, and `--strong` is 24px. It is also the right primitive on its own
 * terms: a badge is a label on a thing, which is exactly what this is.
 *
 * Absolute, so it costs no layout at all. The audit checks the distance between
 * a list's children and the absolute offset of every row; a badge in flow inside
 * an <li> would add a line box and put the whole list off the grid. It also
 * keeps the shelf happy — .go-scroller clips in both axes, so anything hanging
 * outside a card's box there is sliced — and keeps shelf_controller.js's page
 * step, which it reads off the track's first child, measuring a card.
 *
 * THE TOP RIGHT of whatever holds it, inset on both axes, and the same corner
 * on every host: a row, a card, a chip and a page head all put it in one place,
 * so an operator learns one place to look instead of six.
 *
 * The corner is not free anywhere, and the rules below are what a host gives up
 * to hand it over. Every one of them was measured with rectangles across eight
 * public pages at 1280 and at 390, never with a point probe: an earlier version
 * of this file read one pixel with elementFromPoint, landed between two glyphs
 * and reported a lane free that was not.
 *
 * The badge WILL lie on something — artwork, a caption, the tail of a title on
 * the one row under the pointer. What it may never do is take a target away,
 * and that is two bars rather than one: it does not touch a CONTROL at all, and
 * it leaves every other clickable target at least 24px of uncovered width,
 * WCAG 2.5.8's minimum. A button in a row's lane keeps all of itself; a title
 * keeps its left half. test/system/admin_peek_test.rb asserts both rather than
 * leaving them to this comment. Re-measure the same way if a host grows
 * something up there.
 * ------------------------------------------------------------------------ */
:root {
  /* How far a row's right-hand lane drops to clear the badge: the inset, the
   * badge's own 24px, and a 4px gap under it. */
  --admin-peek-drop: 36px;
}

.admin-peek {
  position: absolute;
  z-index: 1;
  inset-block-start: var(--go-space-2);
  inset-inline-end: var(--go-space-2);

  /* The hard 2px offset is the system's own elevation idiom — `--go-shadow-xs`
   * is `2px 2px 0 var(--go-color-line)`, in gameon.css under `--go-shadow-*` —
   * drawn in brand yellow rather than in ink. The pill is ink-filled, so the
   * system's ink shadow would be invisible against it, and over a dark cover
   * the pill would have no edge at all. Yellow gives it one on artwork, on sand
   * and on paper alike. The offsets are retyped because no token takes a
   * colour; the colour itself is `--go-color-brand` and not one invented here. */
  box-shadow: 2px 2px 0 var(--go-color-brand);
}

/* The host is a <li class="go-list__item">, a <li class="go-row-item">, the
 * shelf card's bare <li>, a .go-sectionhead, the home video card's .go-col-1
 * wrapper or a chip's wrapper — six shapes with no class in common. `:has`
 * names the only thing they do share, which is the link itself, and it matches
 * nothing whatsoever on a reader's page: the helper renders no link there, so
 * there is nothing for the selector to find. */
:has(> .admin-peek) { position: relative; }

/* Quiet until the host is under the pointer. Sixty of these visible at once is
 * the page rewritten for the wrong reader, and the whole point is that an
 * operator sees the site as it is until they reach for something.
 *
 * Opacity and not `display`, because the link stays in the tab order: a
 * keyboard operator tabs to it and :focus-visible brings it back. pointer-events
 * has to travel with the opacity, or an invisible pill eats the click on
 * whatever it is sitting over.
 *
 * Only where there is a pointer to hover with. On touch and any coarse pointer
 * the peek is simply there — an affordance nobody can reach is worse than one
 * they can see. */
@media (hover: hover) and (pointer: fine) {
  .admin-peek {
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--go-duration-fast) var(--go-ease);
  }

  :has(> .admin-peek):hover > .admin-peek,
  .admin-peek:focus-visible {
    opacity: 1;
    pointer-events: auto;
  }
}

/* --- what a row gives up --------------------------------------------------
 * Nothing that a reader can measure, and that is the rule rather than a
 * summary of one. The badge is absolute, so it costs no layout; a row that
 * carried a peek used to be 32px taller than the same row on a reader's page,
 * and the price on it sat 36px under its own title, permanently and at rest.
 * The room was reserved for a badge a desktop operator cannot even see until
 * the pointer is on that one row.
 *
 * So the badge lies on what is under it. `.go-row-item__aside` is the design
 * system's right-hand lane and it is where a row keeps the thing it exists to
 * show — a price on /tiendas, a stock badge, a rating on a video — and the
 * badge may cover the top of it while that row is hovered, exactly as it may
 * cover the tail of a title. None of that is a target.
 *
 * A CONTROL is. «Ver en YouTube» on a video row is a button in that same
 * lane, and the badge may not touch a button at all —
 * test/system/admin_peek_test.rb holds that line. So a lane that carries a
 * control, and ONLY such a lane, drops below the badge.
 *
 * TRANSLATED AND NOT PADDED, so the drop cannot cost the row a pixel of height
 * on any shape, present or future. Padding was tried and it is free only while
 * the lane is not the tallest thing in its row — true on a /video row, whose
 * 72px figure sets the height, and false on a game ficha's BGA row, which is
 * 80px and grew to 84 the moment 36px of padding went under a 28px button. A
 * transform never takes part in layout at all: the row's box is exactly the box
 * a reader gets, and only the paint moves.
 *
 * What a transform can do instead is push a control out through the bottom of
 * its row, so that is the bar: test/system/admin_peek_test.rb measures every
 * dropped control against the box of the row that holds it, on eight pages at
 * two widths.
 *
 * DOWN rather than sideways, whichever way it moves: reserving the corner
 * horizontally squeezes `.go-row-item__body`, which does reflow — measured on
 * /video at 390, a 116px lane took a row from 132px to 292px.
 *
 * Keyed off `data-controller` and NOT off `:has(> .admin-peek)`, which is the
 * difference between reserving the room and reflowing to make it. No row call
 * site renders a badge: every one of them passes `**admin_peek(...)`, and the
 * badge arrives when admin_peek_controller.js connects. A rule that waits for
 * the badge therefore lands one frame late — measured on /tiendas, six rows go
 * 88px → 88/108/124px and the document grows 900px → 1016px after first paint,
 * on a listing that really carries sixty rows. The attribute is in the server's
 * HTML, so the room is there before the first pixel. Everything the `:has` asks
 * about is server-rendered too, for the same reason.
 *
 * Two shapes and one attribute: RowItemComponent puts the caller's attributes
 * on the <a> when the row is a link and on the <li> when it is not, and the
 * aside is inside whichever one it landed on. One descendant selector reaches
 * both. The chip rule below needs none of this — `.admin-peek-chip` and the
 * badge inside it are both server-rendered.
 *
 * The `:has` list is `COVER_PROBE`'s own CONTROL list, spelled the same way and
 * in the same order. Keep the two together: a control the test defends and this
 * rule does not know about is a control the badge is free to cover. */
[data-controller~="admin-peek"] .go-row-item__aside:has(button, input, select, summary, .go-btn, .go-chip) {
  transform: translateY(var(--admin-peek-drop));
}

/* Nothing else on a row moves. The badge may lie on the tail of a line while
 * the row is hovered — a title, a meta line, a summary, and now the top of a
 * price — and that is the same licence the placement before this one took with
 * the tail of a meta line. It costs nothing at rest, which is where a desktop
 * operator reads the page: the badge is invisible until the pointer is on that
 * one row.
 *
 * Reserving the title's tail as well was tried and reverted. A fixed lane is
 * read against the VIEWPORT and spent against a COLUMN, so «Nuevo en BGA» — a
 * 400px tile at 1280 — paid a 132px lane out of a 120px title and printed
 * «Ti…» on every row, permanently, at rest. What the badge covers on hover is
 * bounded instead: test/system/admin_peek_test.rb never lets the badge touch a
 * control at all, and holds every other clickable target to 24px of uncovered
 * width — WCAG 2.5.8's minimum. The two aside rules above are what satisfy the
 * first half.
 * ------------------------------------------------------------------------ */

/* Below 760px there is no row wide enough for the badge AND the word on it. A
 * `--lg` campaign row at 390 gives its title 95px and the labelled badge is 100
 * of them — it would cover the title whole, and on a coarse pointer the badge
 * never hides. The badge keeps the corner and drops the word: the icon alone is
 * 36px, and which screen it leads to is still in the tooltip and in the
 * accessible name. Same breakpoint the chip rule below uses, and the same
 * reason — at a phone width the page has run out of room, not out of things to
 * say. */
@media (max-width: 760px) {
  .admin-peek__label { display: none; }
}

/* --- what a chip gives up -------------------------------------------------
 * A chip's wrapper, and the only element that exists for an operator and for
 * nobody else — the home video card's wrapper is rendered for a reader too, so
 * that both are looking at the same grid. The chip row is a flex line, so this
 * has to take the chip's place in it and give up nothing: shrink-wrapped, not
 * stretched, and out of the way at every width. It is rendered only when there
 * is a peek to put in it, so a reader's chip row is the markup it always was.
 *
 * «Takes the chip's place» is why it carries the chip's cap as well. The cap
 * at the foot of this file is `max-width: 100%`, and a percentage resolves
 * against the containing block — for a wrapped chip that is this span, not the
 * row. `flex: none` sizes the span to max-content, so without a cap here the
 * chip caps at 100% of the whole label, which is no cap at all: measured on
 * «Eventos» at 390px, an operator's wrapper was 499px inside a 366px row and
 * the document scrolled to 511. Capped, the span takes the row's width and the
 * chip inside it takes the span's, so the label ellipsizes for the operator
 * exactly as it does for the reader. */
.admin-peek-chip {
  display: inline-flex;
  flex: none;
  max-width: 100%;
}

/* A chip is a 32px control and the badge is 24 of them, so «inside the corner»
 * is most of the chip. A chip is a FILTER, and a badge lying on its trailing
 * third turns that third into a link to somewhere else — hovering to read which
 * shop a chip names would hide the name and take the click with it. So the
 * badge floats clear of the chip and keeps the corner: its bottom edge on the
 * chip's top edge, right edges flush. */
.admin-peek-chip > .admin-peek {
  inset-block-start: auto;
  inset-block-end: 100%;
  inset-inline-end: 0;
}

/* Shops abroad in a ficha's «Dónde comprarlo» (games/_abroad_rows): rows
 * like the Mexican ones, quieter than them. On the footer band
 * (`.go-tile__links`, games/_abroad) when Mexican shops sell the game; in the
 * tile's body under `.offers-abroad-none` when none does. Measured on the
 * 2026-09-30 mockup (news-mockups/extranjero) before it was built. */

/* The empty state as one line: the tile still says Mexico has none, but it no
 * longer outweighs the shops that do sell the game. */
.offers-abroad-none {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--go-space-3);
  padding: var(--go-space-3) var(--go-space-4);
  border: var(--go-border-width) dashed var(--go-color-line-medium);
  border-radius: var(--go-radius-lg);
}
.offers-abroad-none__text {
  margin: 0;
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  flex: 1 1 16rem;
  min-width: 0;
}
.offers-abroad-none .game-alert { flex: none; }

.offers-abroad__head { margin: var(--go-space-5) 0 var(--go-space-1); }
.offers-abroad__head .go-caption { margin-top: var(--go-space-1); }
.offers-abroad > .offers-abroad__head { margin-top: 0; }

/* The peso price stays the boldest figure in the tile: a foreign row's name
 * and price step down one weight, and the price one size — at the Mexican
 * row's line height, so the badge keeps the same distance below it. */
.offers-abroad__list .go-row-item__title {
  color: var(--go-color-ink);
  font-weight: var(--go-weight-semibold);
}
.offers-abroad__list .go-price__now {
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-md);
  font-weight: var(--go-weight-bold);
}
/* On the sunk band the system's soft divider disappears; one step darker. */
.offers-abroad .go-list > * + * { border-top-color: var(--go-color-line-medium); }

/* ALIGNMENT: a divider's width comes out of the padding, as the system's rows
 * do, so the tile stays a multiple of 4px. */
.offers-abroad__list + .offers-abroad__amazon {
  margin: var(--go-space-2) 0 0;
  padding-top: calc(var(--go-space-3) - var(--go-border-width));
  border-top: var(--go-border-width) solid var(--go-color-line-medium);
}
.offers-abroad__amazon { margin: 0; }

/* …and the chip row makes the room for what now floats above it. Without this
 * the badges land on `.go-sectionhead__actions`, which is bottom-aligned and
 * ends a few pixels above the row — «Sugerir una tienda» measured 6px under a
 * chip's badge at 1280. Only a row that carries peeks pays for it.
 *
 * The room is made PER LINE and not once at the top, because above 760px the
 * row wraps too — it just takes more chips to do it. `margin-block-start`
 * alone was written when eight shops fitted one line at 1280px; the ninth
 * wrapped the row, and a badge floats above its OWN chip's top edge, so every
 * badge on the second line landed on the chips of the first — «Todas» covered
 * by 41×16 and «Infinite Skill» by 36×16. The row's own `gap` is 8px against a
 * 24px badge, which is why the line below cannot absorb one; `row-gap` carries
 * the same drop the margin does, so each line clears its own badges. It is the
 * number of chips that decides whether a row wraps, and no width is safe from
 * one more shop. */
.go-chip-row:has(.admin-peek-chip) {
  margin-block-start: var(--admin-peek-drop);
  row-gap: var(--admin-peek-drop);
}

/* Below 760px a phone's chip row is several lines of two or three chips, and
 * reserving the drop on every one of them would put the whole row on 64px
 * stripes — see «Filter chips wrap» at the foot of this file. A desktop row
 * wraps onto two or three lines and can afford it; a phone row cannot. So at a
 * phone width a chip does not carry a badge at all, and the row gives back
 * both the margin and the row-gap it would otherwise pay for one; the section
 * head's own peek is one tap away and leads to the same table.
 *
 * The rule this replaced blamed the scroller, which used to clip anything
 * above a chip. The scroller is gone and the answer is the same one. */
@media (max-width: 760px) {
  .admin-peek-chip > .admin-peek { display: none; }

  .go-chip-row:has(.admin-peek-chip) {
    margin-block-start: 0;
    row-gap: var(--go-space-2);
  }
}

/* --- what a page head gives up --------------------------------------------
 * Nothing, and it is the one host that has the corner to spare — but it wants
 * the badge FLUSH rather than inset. `.go-sectionhead` is `align-items:
 * flex-end`, so `.go-sectionhead__actions` — «Volver a Hoy» and, on three
 * sections, «Sugerir …» — sits at the bottom right and reaches up into the
 * corner: at 1280 the head is 60px tall and that cluster owns its bottom 32.
 * Flush leaves the badge clear of it; the 8px inset is what would push it onto
 * the buttons. At 390 the head stacks and the only thing the badge can reach is
 * the h1's own box, which is text. */
.go-sectionhead > .admin-peek {
  inset-block-start: 0;
  inset-inline-end: 0;
}

/* A head that toggles its tile says so, open as well as folded: the design
 * system points only a FOLDED head, because only that one was a way back in.
 * tile_collapse_controller.js#toggle makes every minimizable head the bar. */
.go-tile[data-tile-key] > .go-tile__head { cursor: pointer; }

/* A TILE's badge sits on its head's line, just left of the minimize toggle —
 * never on it, since the toggle is a control (and the head, when a reader taps
 * it, is one too). It is the tile's direct child and not the head's, because
 * the head is the design system's markup; these offsets put it there instead.
 * The head is the first 24px line inside the tile's padding, the badge
 * (`.go-badge--strong`) is 24px too, and the toggle is 24px with the head's
 * 8px gap.
 *
 * A minimized tile hides every child but its head, and the badge has to
 * outlast that: a folded «Nuevo en BGA» is still a feed an operator may want to
 * open. `.go-badge` restates the class so this outranks the fold's (0,3,0). */
.go-tile > .admin-peek {
  inset-block-start: var(--go-card-inset);
  inset-inline-end: calc(var(--go-card-inset) + var(--go-leading-lg) + var(--go-space-2));
}
.go-tile[data-collapsed] > .admin-peek.go-badge { display: inline-flex; }

/* --- what a card gives up -------------------------------------------------
 * Nothing either: a card's top right is artwork, which is decorative (`alt=""`,
 * with the title named right beside it) and the one region of a card that
 * carries nothing to read. Its own caption — on the shelf, the follower count
 * the strip is ranked by — is at the bottom and stays there.
 *
 * What a card's peek does have to do is TRAVEL with the card, because the card
 * moves. `.go-card--link:hover` in gameon.css lifts the anchor by 2px and
 * `:active` presses it back by 1px, and the peek is that anchor's SIBLING —
 * positioned against the <li>, so it cannot follow on its own. Left alone it
 * appears 2px out of register with the card it belongs to, every single time,
 * because hover is exactly when it appears. The two numbers are the design
 * system's, not this file's: if the card's lift ever changes upstream, this has
 * to change with it.
 *
 * Gated on the CARD's hover and not the <li>'s, which is not the same thing and
 * is the whole of the third state. The peek is the anchor's sibling and lies on
 * top of it, so moving the pointer onto the badge takes the hover OFF the card:
 * the card drops back to rest while `li:hover` still holds. Mirroring on the
 * <li> would keep the badge lifted with nothing under it to match, so it jumped
 * 2px at exactly the moment the operator was aiming at it. `.go-card--link` and
 * not `.go-shelfcard`, because the lift belongs to the link — a card with no
 * URL renders as an <article> and never moves.
 *
 * `.go-video` needs none of this — its hover underlines the title and moves
 * nothing. */
.go-shelf > li:has(> .go-card--link:hover) > .admin-peek {
  transform: translate(-2px, -2px);
}
.go-shelf > li:has(> .go-card--link:active) > .admin-peek {
  transform: translate(1px, 1px);
}

/* ==========================================================================
 * Filter chips wrap; they never scroll
 *
 * App-level because gameon.css is vendored. It is the same override for every
 * screen that filters — «Tiendas», «Crowdfunding», «Eventos», «Video», and the
 * six chip rows in the panel — because all of them are one class.
 *
 * The vendored `.go-chip-row` turns into a horizontal scroller below 760px:
 * `flex-wrap: nowrap` plus `overflow-x: auto`. That buys one thing and costs
 * two. CSS never lets one axis scroll while the other stays visible, so
 * `overflow-y` computes to `auto` with it and the box is exactly as tall as a
 * 32px chip — the 2px offset shadow every pressed chip carries
 * (`--go-shadow-xs`) is then sliced off flat along the bottom edge, on every
 * filtered page, at every phone width. The second cost is the filters
 * themselves: four chips fit a 390px screen and the rest sit behind a sideways
 * drag with nothing on the page that says so.
 *
 * So the row keeps at every width the wrapping it already has above 760px. A
 * second line costs 40px — a 32px chip and the row's own 8px gap, both on the
 * grid the audit measures — and it shows the reader every filter there is.
 * With no overflow to manage there is nothing left to clip: the shadow paints
 * into that gap, and into the 12px `.go-stack--3` leaves under the last line.
 *
 * The bleed goes with the scrolling. `margin-inline: -pad` + `padding-inline:
 * pad` exists so a scrolling row can reach the container's edge and cut its
 * last chip against the screen, which is the affordance that says it scrolls.
 * A wrapping row has no cut edge to advertise and wants the heading's own left
 * margin instead.
 * ========================================================================== */

@media (max-width: 760px) {
  .go-chip-row {
    flex-wrap: wrap;
    overflow: visible;
    margin-inline: 0;
    padding-inline: 0;
  }
}

/* …and the chip may not be wider than the line it sits on.
 *
 * The scroller was containing something else as well, and this is the half of
 * it that outlives the scrolling. `.go-chip` is `flex: none` with
 * `white-space: nowrap` (`gameon.css`), so a chip does not wrap and does not
 * shrink: a label longer than the row is a box wider than the row. The
 * scroller hid that inside itself. A visible row hands it to the document, and
 * the whole page scrolls sideways — measured at 390px, a 70-character channel
 * name took `documentElement.scrollWidth` to 608.
 *
 * The labels are database text. A YouTube channel title is whatever the
 * channel called itself, up to YouTube's own hundred characters; `MeetupGroup`
 * and the city names on «Eventos» have no length rule at all. So this is not a
 * fixture that needs shortening, it is a width a chip has to survive.
 *
 * Capping the chip and letting the label ellipsize is what a scroller could
 * not do: the reader sees where the name stops rather than a strip that ran
 * off the screen, and the full name stays in the DOM, so the button's
 * accessible name is still the whole thing.
 *
 * `min-width: 0` on the label, because it is a flex item of the chip and a
 * flex item's `min-width: auto` is its min-content width — for a nowrap run
 * that is the whole label, and the cap above it would have nothing to give.
 * Not scoped to a phone: a name long enough to do this does it at any width.
 * ------------------------------------------------------------------------ */
.go-chip { max-width: 100%; }

/* Location is a recommendation; aria-current/pressed still owns selection. */
.go-chip[data-nearest-city="true"] {
  border-color: var(--go-color-ink);
  box-shadow: inset 0 0 0 1px var(--go-color-ink);
}

.nearest-city-label {
  flex: none;
  border-radius: var(--go-radius-sm);
  padding-inline: var(--go-space-1);
  background: var(--go-color-brand);
  color: var(--go-color-ink);
}

.go-chip__label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ==========================================================================
 * Public phone «Más»
 *
 * The fourth tab is a <details> whose summary is a tab item. Without JS the
 * sheet opens above the bar. With JS, nav-dialog intercepts the click and
 * opens a native <dialog> instead. App-level because gameon.css is vendored.
 * ========================================================================== */

.go-tabbar__more {
  flex: 1 1 0;
  min-width: 0;
  height: var(--go-space-14);
  position: relative;
}

/* No button reset here, and `font: inherit` least of all. A <summary> is not
 * a form control: the browser gives it no font, no background, no border and
 * no appearance of its own, so every one of those declarations is either
 * inert or harmful. `font: inherit` was the harmful one — it beat
 * .go-tabbar__item, the single rule that gives every tab its type, and «Más»
 * rendered at the body ramp, 15/24 at weight 400, beside three labels at
 * 11/16 at weight 600. This block now holds only what a <summary> needs, so
 * it no longer reads like a button reset inviting the next declaration.
 *
 * test/system/tabbar_test.rb measures the rendered font of all four items.
 * CI runs no system job — the runner's chromium has no sandbox — so
 * `bin/rails test:system` is what catches the reset coming back. */
.go-tabbar__more > summary.go-tabbar__item {
  width: 100%;
  flex: none;
  list-style: none;
  cursor: pointer;
}

.go-tabbar__more > summary.go-tabbar__item::-webkit-details-marker {
  display: none;
}

.go-tabbar__more > summary.go-tabbar__item::marker {
  content: none;
}

/* Chromium collapses a closed details body with `content-visibility` rather
 * than removing its absolute box. That is visually quiet, but descendants
 * still report rectangles and can be mistaken for controls under the page.
 * The no-JS sheet has no job while closed, so give it literal no geometry; it
 * returns as soon as the native details opens. */
.go-tabbar__more:not([open]) > .go-tabbar__sheet { display: none; }

.go-tabbar__sheet {
  position: absolute;
  right: var(--go-space-2);
  bottom: calc(100% + var(--go-space-2));
  z-index: var(--go-z-dropdown);
  width: min(20rem, calc(100vw - 2 * var(--go-space-4)));
  padding: var(--go-space-2);
  background: var(--go-color-surface);
  border: var(--go-border);
  border-radius: var(--go-radius-xl);
  box-shadow: var(--go-shadow-lg);
}

/* Sit above the tab bar rather than vertically centred. A short centred
 * go-dialog in a 900px viewport lands 2px off the absolute grid. */
.go-tabbar .go-dialog {
  inset: auto var(--go-space-4) calc(var(--go-space-14) + var(--go-space-4)) var(--go-space-4);
  width: auto;
  margin: 0;
}

/* The overflow row's mark, in the section's own category colour. Three
 * destinations that all look alike is a list a reader has to read; the colour
 * is what makes «Video» findable at a glance, and it is the same token the
 * section's own tiles and chips use.
 *
 * It reads --go-cat off the .go-cat-* class beside it, so this needs no colour
 * of its own. The hook has to sit on this span and not on the glyph, because
 * .go-icon declares `color: inherit` at equal specificity and later in the
 * cascade, so a .go-text-cat on the <svg> loses. `display: flex` keeps the
 * block-level glyph out of inline layout, where its line box would sit the
 * mark a fraction below the label.
 *
 * The app bar's phone menu, layouts/_nav_rail, deliberately does NOT use this.
 * That list holds every section, and both «Tiendas» and «Hoy» are brand yellow
 * — #F9C901 on the pale yellow an aria-current row paints is 1.5:1, an
 * invisible mark on the row a reader is most likely to be looking at. The
 * three sections behind «Más» are coral, blue and green, which clear 3:1 on
 * both surfaces. */
.go-railitem__mark {
  display: flex;
  color: var(--go-cat);
}

/* ==========================================================================
 * A detected name's linker, closed until it is needed
 *
 * A «Lista» draft names twelve games. The ones the catalogue already answers
 * to are chips; the rest each carry the catalogue search and the BGG import,
 * and twelve of those open at once is a card that has to be scrolled past
 * rather than read. So each one is a <details> and the summary is the question
 * it answers.
 *
 * No button reset, for the reason the tab bar's own summary block gives: a
 * <summary> is not a form control and every declaration a reset would add is
 * either inert or beats the class that gives the line its type. What is here
 * is the marker — which the system has no glyph for — and the room the open
 * body needs under it.
 * ========================================================================== */

.admin-mention > summary {
  list-style: none;
  cursor: pointer;
  color: var(--go-color-ink-subtle);
  text-decoration: underline;
  text-underline-offset: 2px;
}

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

.admin-mention > summary::marker { content: none; }

.admin-mention > summary:hover,
.admin-mention[open] > summary { color: var(--go-color-ink); }

.admin-mention__body { margin-block-start: var(--go-space-3); }

/* ============================================================================
 * A named game's state, inside «Lo que dijo el modelo»
 * ----------------------------------------------------------------------------
 * That `games` row holds three states and shows three. A name with a ficha is
 * a link to it, a name without one is a link to BGG's search, and a name the
 * operator dismissed is neither — «No es un juego» is an answer, so nothing
 * there invites them to give it again.
 *
 * With two of the three linked, «is it a link?» does not tell those two apart.
 * Two cues do, and neither is colour: the weight drops back to regular, so a
 * name with a ficha is the bolder of the two, and the underline goes dashed
 * and thin. Both survive a screenshot printed in grey. The third state reads
 * as struck off, which is the same sentence said with a line rather than a
 * word. What a pointer and a screen reader get on top of that is written in
 * `Admin::PayloadsHelper::DETECTED_STATE`, once for both.
 *
 * Text decoration, colour and font weight, and nothing else, because this sits
 * in a `.go-deflist__row`. That row is `align-items: baseline`, so a child
 * with metrics of its own is measured against the text's baseline rather than
 * boxed beside it: DESIGN-SYSTEM.md has the measurement — a `.go-badge` in a
 * `<dd>` made the row 21.84px against a 20px line-height, and every row below
 * it inherited the fraction. Deflists take text. Not one declaration here
 * costs the line box a pixel.
 * ========================================================================== */

.admin-detected {
  font-weight: var(--go-weight-regular);
  text-decoration-style: dashed;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--go-color-ink-subtle);
}

.admin-detected:hover { text-decoration-color: var(--go-color-ink); }

.admin-detected--dismissed {
  color: var(--go-color-ink-subtle);
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* ==========================================================================
 * «Noticias», set as a front page
 *
 * The one PAGE LAYOUT this app owns, and the third thing in this file that a
 * reader sees. Everything else public in here fixes a height; this composes a
 * whole screen.
 *
 * WHY IT IS NOT IN THE DESIGN SYSTEM. A folio rule, a drop cap, a three-column
 * text flow and an index box are not primitives — they are one newspaper, and
 * the system's rule is that a component exposes no padding, height or colour
 * option because every screen that uses it wants the same one. Nothing else on
 * this site wants a drop cap. If a second section ever asks for this shape,
 * that is the day it goes to ../design-system and comes back through
 * bin/sync-design-system.
 *
 * NOT ONE INVENTED VALUE. Every size, colour, weight and space below is a
 * `--go-*` token, so the page re-themes with the system and lines up with the
 * chrome above it for free. The one place a number is typed is where a border
 * is paid for out of the padding under it — `calc(space - border)` — which is
 * the system's own idiom for keeping a bordered box on the 4px grid (see
 * .go-tile__band and .go-list in gameon.css).
 *
 * WHAT THE ALIGNMENT GATE CAN AND CANNOT SEE HERE. audit.js measures elements
 * carrying `go-` classes, and this page has almost none: no rows, no tiles, no
 * badges. So the gate holds this page to horizontal overflow and to its own
 * check floor, and the 4px rhythm below is kept by hand — every line-height is
 * a `--go-leading-*` token and every margin a `--go-space-*` one, which is
 * what makes that possible to do by hand at all.
 * ========================================================================== */

/* --- the folio: the rule under the masthead ------------------------------- */

.news-folio {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--go-space-3);
  margin-bottom: var(--go-space-6);
  padding-block: calc(var(--go-space-2) - var(--go-border-width));
  border-block: var(--go-border);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
}

.news-folio b {
  color: var(--go-color-ink);
  font-weight: var(--go-weight-bold);
}

/* The separator is drawn by the rule and not typed into the template, so a
 * clause that stops rendering — the edition has no dated note yet — takes its
 * own dot with it instead of leaving a stray one behind. */
.news-folio span + span::before {
  content: "·";
  margin-inline-end: var(--go-space-3);
  color: var(--go-color-line-medium);
}

/* The class is repeated to out-specify the sibling rule above, which is
 * (0,2,1) and would otherwise keep drawing a dot in front of a clause that has
 * nothing to its left. */
.news-folio .news-folio__end { margin-inline-start: auto; }
.news-folio .news-folio__end::before { content: none; }

/* --- the kicker: who said it, above the headline --------------------------
 * This is what replaced the attribution pill that used to sit in a lane of its
 * own at the far right of all sixteen rows. The publisher's name belongs above
 * the headline, and nothing under the story repeats where the words came
 * from: the site prints no provenance notice at all.
 *
 * FLEX AND NOT AN INLINE ::before. The square is 10px tall (8 plus its
 * hairline) inside a 16px line, and an inline-block that size can grow the
 * line box on some fonts and not others. A flex line's height is the tallest
 * ITEM, which is the 16px text, so the box is 16 or a multiple of it — always,
 * on any width, wrapped or not. */
.news-kicker {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--go-space-2);
  margin: 0 0 var(--go-space-2);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
}

.news-kicker::before {
  content: "";
  flex: none;
  width: var(--go-space-2);
  height: var(--go-space-2);
  background: var(--go-cat);
  border: var(--go-border-width-hair) solid var(--go-color-line);
}

.news-kicker b {
  color: var(--go-color-ink);
  font-weight: var(--go-weight-bold);
}

/* --- the front page: two stories and the index ---------------------------- */

.news-front {
  display: grid;
  grid-template-columns: minmax(0, 2.1fr) minmax(0, 1fr);
  gap: var(--go-space-8);
  padding-bottom: calc(var(--go-space-8) - var(--go-border-width));
  border-bottom: var(--go-border);
}

/* An edition small enough to have nothing the index could point at drops the
 * index entirely — see sections/_news — and the stories take the whole width
 * rather than leaving a column of nothing beside them. */
.news-front > :only-child { grid-column: 1 / -1; }

/* With nothing published beyond the two stories there are no bands under this,
 * and the rule that separates the front page from them would be a line drawn
 * to nowhere. */
.news-front:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

.news-front__stack { min-width: 0; }

.news-lead__title {
  margin: 0;
  font-size: var(--go-text-3xl);
  line-height: var(--go-leading-3xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-tight);
  max-width: 20ch;
}

/* ONE COLUMN, AND IT WAS TWO. A lead set in two columns is the newspaper
 * shape and it is not survivable: a summary short enough to fit on one line at
 * this width leaves Chrome balancing a single line box into the first column
 * while the drop cap's float lands at the top of the SECOND — the story reads
 * «tonemaier anuncia…» with a stranded S beside it. Measured, on a real
 * one-sentence note. Nothing in CSS can say «only use columns when there is
 * enough text», and a character count in the template would be a guess about
 * type that changes with the width it is set at.
 *
 * So the lead runs one measure. It is the measure this whole redesign is
 * about — the page it replaced ran ~140 characters a line — and the columns
 * are still the page's shape, three of them, under the bands. */
.news-lead__body {
  /* The drop cap's float belongs to THIS block. Without a containing context
   * it reaches past the paragraph and indents whatever follows: a lead of one
   * line put what came under it 36px in from the margin. `flow-root` costs
   * nothing on a lead of three lines, where the float is shorter than the text
   * anyway, and on a lead of one it makes the block the float's own height —
   * 52px, still a multiple of four. */
  display: flow-root;
  margin-top: var(--go-space-4);
  max-width: 62ch;
  font-size: var(--go-text-lg);
  line-height: var(--go-leading-xl);
  color: var(--go-color-ink-muted);
}

.news-lead__body p { margin: 0; }

/* The drop cap, and the one place on this site that has one.
 *
 * ITS BOX IS SMALLER THAN THE LINES IT SPANS, which is what keeps it free: 48px
 * of float plus 4px of margin against two 28px lines is 52 against 56, so the
 * float never reaches past the text it is set into and never adds a pixel to
 * the paragraph. Grown any further it starts pushing the block taller than its
 * own line count, and every margin below it on the page inherits the
 * difference. */
.news-lead__body p::first-letter {
  float: left;
  margin: var(--go-space-1) var(--go-space-2) 0 0;
  font-size: var(--go-text-5xl);
  line-height: var(--go-leading-4xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-tight);
  color: var(--go-color-ink);
}

/* The second story: another voice, under the same rule the folio draws. */
.news-second {
  margin-top: var(--go-space-6);
  padding-top: calc(var(--go-space-5) - var(--go-border-width));
  border-top: var(--go-border);
}

.news-second__title {
  margin: 0;
  font-size: var(--go-text-2xl);
  line-height: var(--go-leading-2xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
  max-width: 24ch;
}

.news-second__body {
  margin-top: var(--go-space-3);
  max-width: 68ch;
  font-size: var(--go-text-md);
  line-height: var(--go-leading-md);
  color: var(--go-color-ink-muted);
}

.news-second__body p { margin: 0; }

/* --- «En esta edición» ---------------------------------------------------- */

.news-index {
  align-self: start;
  padding: calc(var(--go-space-4) - var(--go-border-width));
  border: var(--go-border);
  border-radius: var(--go-radius-lg);
  background: var(--go-color-surface);
  box-shadow: var(--go-shadow-cat);
}

.news-index__title {
  margin: 0 0 var(--go-space-3);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  font-weight: var(--go-weight-medium);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
}

.news-index__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The type is on the ROW and not on the anchor: a note with no video and no
 * url of its own is a heading rather than a link (see NewsHelper), and styling
 * only the `a` would set that one in the browser's default. */
.news-index__list li {
  display: flex;
  gap: var(--go-space-3);
  padding-block: calc(var(--go-space-2) - var(--go-border-width-hair));
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  font-weight: var(--go-weight-semibold);
  letter-spacing: var(--go-tracking-snug);
}

.news-index__list li + li {
  border-top: var(--go-border-width-hair) solid var(--go-color-line-soft);
}

.news-index__n {
  flex: none;
  width: var(--go-space-5);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  font-weight: var(--go-weight-medium);
  color: var(--go-color-ink-subtle);
}

.news-index__foot {
  margin: var(--go-space-3) 0 0;
  padding-top: calc(var(--go-space-3) - var(--go-border-width-hair));
  border-top: var(--go-border-width-hair) solid var(--go-color-line);
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  font-weight: var(--go-weight-bold);
}

/* --- the bands and the columns under them --------------------------------- */

.news-band {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--go-space-3);
  margin-top: var(--go-space-8);
  margin-bottom: var(--go-space-4);
  padding-bottom: calc(var(--go-space-2) - var(--go-border-width));
  border-bottom: var(--go-border);
}

.news-band__title {
  margin: 0;
  font-size: var(--go-text-xl);
  line-height: var(--go-leading-lg);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
}

.news-band__meta {
  margin: 0;
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
}

/* Real columns and not a grid of cards: `columns` is the only thing that flows
 * ONE list of notes down a column and across to the next, which is how a
 * newspaper is read and the reason this direction was chosen. The price is
 * that the browser balances by height and cannot split a note, so the last
 * column ends short of the others. Nothing is clamped to hide it — the summary
 * IS the note, and this site has no page that would show the rest of a
 * truncated one. */
.news-cols {
  columns: 3;
  column-gap: var(--go-space-8);
  column-rule: var(--go-border-width-hair) solid var(--go-color-line-medium);
}

.news-art {
  break-inside: avoid;
  margin: 0 0 var(--go-space-5);
  padding-bottom: calc(var(--go-space-5) - var(--go-border-width-hair));
  border-bottom: var(--go-border-width-hair) solid var(--go-color-line-medium);
}

.news-art__fig { margin: 0 0 var(--go-space-3); }

.news-art__title {
  margin: 0;
  font-size: var(--go-text-xl);
  line-height: var(--go-leading-lg);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
}

.news-art__sum {
  margin: var(--go-space-2) 0 0;
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  color: var(--go-color-ink-muted);
}

/* The date, and nothing else: where the words came from is said once, in the
 * kicker over the headline. In 11px mono under the story it is there for
 * whoever looks and gone for whoever does not. Lower case deliberately: the
 * kicker above already carries the uppercase. */
.news-art__foot {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--go-space-2);
  margin: var(--go-space-2) 0 0;
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  color: var(--go-color-ink-subtle);
}

/* The dot between the date and «N fuentes», drawn the way .news-folio draws
 * its own: on the SECOND child, so a note with one source takes the separator
 * with it and the foot reads as a bare date. */
.news-art__foot > * + *::before {
  content: "·";
  margin-inline-end: var(--go-space-2);
  color: var(--go-color-line-medium);
}

/* --- how many outlets carried a note -------------------------------------
 * A count that opens into names. It is never a sentence about where the words
 * came from — no page here says that — it is WHO, which is the same fact the
 * kicker and a row's meta line already carry, said once for the sources the
 * kicker has no room to name.
 *
 * On every surface it sits BELOW the title and the meta, never inside them.
 * Two reasons, and both are measured rather than reasoned about: a <details>
 * inside a <p> is closed out of it by the parser, and the peek badge owns the
 * top right corner of every one of these hosts — a control that never rises
 * into the first two lines cannot be covered by it at any width, so nothing
 * has to drop or step aside and a row measures the same for an operator as
 * for a reader.
 *
 * Type is the 2xs mono ramp the kicker and the foot already use, so the line
 * this adds is one the 4px rhythm of these columns already knows about. */
.news-sources {
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  color: var(--go-color-ink-subtle);
}

/* Chromium collapses a closed <details> with content-visibility rather than
 * removing its box, so the list's own anchors keep reporting rectangles and
 * are read as live targets lying under the page — .go-tabbar__sheet documents
 * the same trap at the head of this file, and here it would be three phantom
 * links per note under the alignment audit and the peek badge's cover probe.
 * A closed disclosure has no list. */
.news-sources:not([open]) > .news-sources__list { display: none; }

/* No button reset and no `font: inherit` on a <summary>: it is not a form
 * control, and that declaration is exactly what once broke the tab bar's
 * type.
 *
 * The affordance is an UNDERLINE and not a caret, which is the answer
 * `.admin-mention` already reached on the same question: the system has no
 * disclosure glyph, and the one this first drew — a `▾` in the mono face at
 * 2xs — rendered as a grey speck nobody would take for a control. The
 * underline is deliberately not `.go-link`'s: that one is the yellow rule a
 * headline wears, and this is not a link off the page. */
.news-sources > summary {
  cursor: pointer;
  list-style: none;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--go-color-line-medium);
}

.news-sources > summary::-webkit-details-marker { display: none; }
.news-sources > summary::marker { content: none; }

.news-sources > summary:hover,
.news-sources[open] > summary { color: var(--go-color-ink); }

.news-sources__list {
  margin: var(--go-space-2) 0 0;
  padding: 0;
  list-style: none;
}

.news-sources__list li + li { margin-top: var(--go-space-1); }

/* In a row the disclosure follows the meta line, which already carries its
 * own top margin; on the front page it follows a paragraph. Both want the
 * same gap and neither wants it collapsing into the summary above. */
.go-row-item__body > .news-sources,
.news-lead > .news-sources,
.news-second > .news-sources { margin-top: var(--go-space-2); }

/* --- the ficha a note points at -------------------------------------------
 * The one line under a story that is NOT about where the words came from:
 * where the reader goes next. The headline leads off the site — to the page
 * the publisher or the outlet announced it on — and this stays on it, at this
 * site's own page about the box being announced.
 *
 * The 2xs mono ramp the kicker, the foot and the disclosure already use, so
 * the line it adds is one the 4px rhythm of these columns already knows about.
 * Lower case and no tracking, unlike the kicker: what it holds is a game's
 * NAME when a note names several, and a name is not furniture.
 *
 * Its own <p> on every surface, below the title and the meta — the shape
 * `.news-sources` documents above, and for the same two measured reasons. */
.news-fichas {
  margin: var(--go-space-2) 0 0;
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  color: var(--go-color-ink-subtle);
}

/* --- the two widths this page changes shape at ----------------------------
 * 1080 is where three text columns stop being readable at this container's
 * width; 760 is the system's own phone breakpoint, the one the app bar's pill
 * nav and .go-aspect--h-xl already fold at. */
@media (max-width: 1080px) {
  .news-front {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--go-space-6);
  }

  .news-cols { columns: 2; }
}

@media (min-width: 761px) {
  .news-lead__title {
    font-size: var(--go-text-5xl);
    line-height: var(--go-leading-5xl);
  }
}

@media (max-width: 760px) {
  .news-cols { columns: 1; }

  .news-folio__end { margin-inline-start: 0; }
}

/* ==========================================================================
 * «Tiendas» — la vitrina
 *
 * The second page-specific LAYOUT this app owns, and the same bargain as
 * `.news-*` above: a composition of the system's tokens, not a component the
 * system could hold. Every value below is a `--go-*` token or a multiple of 4
 * derived from one; nothing here invents a colour, and the category tint comes
 * from `.go-cat-tiendas` on <main>, inherited.
 *
 * WHAT IT REPLACED, measured on the live page at 1,357px over its sixty
 * listings: a row 1,269px wide whose price sat 738px from the end of its own
 * title on average (645 at the tightest, 1,005 at the widest), with a 48×64
 * cover — the smallest thing on a page about boxes — and five days of arrivals
 * flattened into one list whose date was the last of seven fragments in the
 * palest line on the row.
 *
 * See app/views/sections/_releases.html.erb for what moved where.
 * ========================================================================== */

/* --- the day band ---------------------------------------------------------
 * The separator, and the only thing on this page that says WHEN. Sticky,
 * because a grid of sixty cards takes the heading off screen in the first
 * scroll and the day it belongs to is half of what this direction proposes.
 *
 * `--go-appbar-h` and not 0: `.go-appbar` is `position: sticky; top: 0`, so a
 * band pinned to the viewport's top parks itself underneath it and is never
 * seen again. Same token, and the same reason, as `.go-aside-sticky`.
 *
 * The bottom border is paid for out of the padding under it — 12 - 2 = 10 —
 * exactly as `.go-tile__band` and `.go-list` pay for their rules, so the band
 * is 4px-grid tall with the line and not two pixels past it.
 *
 * An opaque background is not decoration here: cards scroll UNDER this, and a
 * transparent band would print the day over the artwork moving behind it. */
.vitrina__day {
  position: sticky;
  top: var(--go-appbar-h);
  z-index: var(--go-z-sticky);
  display: flex;
  align-items: baseline;
  gap: var(--go-space-3);
  flex-wrap: wrap;
  margin: 0 0 var(--go-space-4);
  padding-block: var(--go-space-3) calc(var(--go-space-3) - var(--go-border-width));
  border-bottom: var(--go-border);
  background: var(--go-color-canvas);
}

.vitrina__grid + .vitrina__day { margin-top: var(--go-space-8); }

.vitrina__day-label {
  font-size: var(--go-text-2xl);
  line-height: var(--go-leading-2xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
}

/* The category square the rest of the site draws beside a heading, so the band
 * belongs to «Tiendas» rather than looking like chrome from another page. It is
 * a pseudo-element and not a `.go-hex`: `.go-hex` is `display: block` and would
 * take the label onto its own line inside a baseline-aligned flex row. */
.vitrina__day-label::before {
  content: "";
  display: inline-block;
  width: 10px;
  height: 10px;
  margin-inline-end: var(--go-space-2);
  background: var(--go-cat);
  border: var(--go-border-width-hair) solid var(--go-color-line);
}

/* How big the day was, and across how many shops — the half that tells one
 * shop's bulk upload apart from six shops' ordinary Tuesday. Pushed to the far
 * end while there is room, and it takes its own line when there is not. */
.vitrina__day-count {
  margin-inline-start: auto;
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  font-weight: var(--go-weight-medium);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
  white-space: nowrap;
}

/* --- the grid -------------------------------------------------------------
 * `auto-fill` and NOT `auto-fit`: the last day on a page is routinely one or
 * two listings, and `auto-fit` collapses the empty tracks and stretches those
 * two across the whole container — a 600px-wide card with a 160px cover, which
 * is a box stretched out of shape rather than a shop window. */
.vitrina__grid {
  display: grid;
  gap: var(--go-grid-gap);
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.vitrina__cell { min-width: 0; }

/* --- the card: a fixed height ---------------------------------------------
 * 296 = 160 (cover) + 92 (body) + 40 (foot) + 4 of border, and the body is
 * 12 + 48 + 4 + 16 + 12. The two lines of title are reserved whether the title
 * fills them or not, and THAT is what the fixed height buys: the meta line and
 * the price land at the same height in all six cards of a row, so a reader
 * scans one line of prices instead of six.
 *
 * WHAT IT COSTS, stated rather than discovered: a title longer than two lines
 * is cut. «Carolyn Fern Mazo de investigador – Arkham Horror El juego de
 * cartas» loses its tail on this page and keeps it on the ficha the card opens.
 *
 * The middle track is `minmax(0, 1fr)` and not `auto` so the body can never
 * push the foot past the card's own height; with the numbers above it resolves
 * to exactly 92 and there is no slack to distribute. */
.vitrina-card {
  display: grid;
  grid-template-rows: 160px minmax(0, 1fr) auto;
  height: 296px;
  border: var(--go-border);
  border-radius: var(--go-radius-lg);
  background: var(--go-color-surface);
  box-shadow: var(--go-shadow-cat);
  overflow: hidden;
  color: inherit;
  text-decoration: none;
}

a.vitrina-card:hover { box-shadow: var(--go-shadow-md); }
a.vitrina-card:hover .vitrina-card__title { text-decoration: underline; }

/* `.go-aspect` owns the box — the sand background, the clip, and an <img> laid
 * inside it as `object-fit: cover`. All this does is take the RATIO away, for
 * the reason the card's own rule gives: a ratio on a fluid column is a fluid —
 * and therefore usually fractional — height, and the card's height is fixed.
 *
 * And it takes the ratio away WITHOUT putting a height back. A grid item
 * stretches to its own track, so the cover is 160px at 768 and up and 112 on a
 * phone because the CARD's `grid-template-rows` says so, in one place. A height
 * written here as well was the phone's bug: the track went to 112 and the cover
 * stayed at 160, hanging into the body under it.
 *
 * The radius goes because the card already clips to its own, and a rounded
 * cover inside a clipped card prints sand in the two top corners. */
.vitrina-card__media {
  aspect-ratio: auto;
  border-radius: 0;
  border-bottom: var(--go-border);
}

/* A shop's name is a shop's name — «Con T de Tlacuache» is eighteen characters
 * and this app does not invent a short one. The tag is capped at the cover's
 * width less its two insets and ellipsized. */
.vitrina-card__media > .go-media-tag {
  max-width: calc(100% - var(--go-space-4));
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The badges, along the cover's bottom edge and side by side. `.go-badge` is
 * opaque — a surface fill and a hairline border — so two of them read over
 * artwork as well as over the empty sand box. */
.vitrina-card__badges {
  position: absolute;
  inset-inline-start: var(--go-space-2);
  inset-block-end: var(--go-space-2);
  max-width: calc(100% - var(--go-space-4));
}

.vitrina-card__body {
  display: block;
  padding: var(--go-space-3);
  min-width: 0;
}

.vitrina-card__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  height: calc(var(--go-leading-md) * 2);
  font-size: var(--go-text-md);
  line-height: var(--go-leading-md);
  font-weight: var(--go-weight-bold);
  letter-spacing: var(--go-tracking-snug);
}

.vitrina-card__meta {
  display: block;
  margin-top: var(--go-space-1);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  color: var(--go-color-ink-subtle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The price, alone, at a control height.
 *
 * THE INLINE PADDING IS THE BODY'S, exactly — `--go-space-3` and not
 * `calc(--go-space-3 - --go-border-width)`. The system's inset idiom subtracts
 * a border because a card's padding sits inside one; this foot's border is on
 * its TOP edge and buys its sides nothing, so subtracting there put the price
 * 2px to the left of the title it belongs under. Same number, one column —
 * which is the whole point of moving the price off the row's right-hand lane,
 * and test/system/vitrina_test.rb measures it at three widths. */
.vitrina-card__foot {
  display: flex;
  align-items: center;
  min-height: var(--go-control-h-md);
  padding-inline: var(--go-space-3);
  border-top: var(--go-border);
  background: var(--go-color-surface-sunk);
}

.vitrina-card__foot .go-price { font-size: var(--go-text-lg); }

/* --- the phone ------------------------------------------------------------
 * Two columns and not one. At 390px a single column is sixty cards of 296px —
 * about 19,000px of scroll, three times the list this replaced. Two columns of
 * a 232px card (112 + 84 + 32 + 4, with the title at 13/20) is 8,846px against
 * the old list's 6,009.
 *
 * That is still 47% more page than before, and it is the direction's price
 * rather than a defect: below about 1,000px a grid of covers cannot beat a
 * one-line row on height, and the cover is the point. The card is already as
 * short as it goes without dropping a fact from it. */
@media (max-width: 560px) {
  .vitrina__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--go-space-3);
  }

  .vitrina-card {
    grid-template-rows: 112px minmax(0, 1fr) auto;
    height: 232px;
  }

  .vitrina-card__title {
    font-size: var(--go-text-sm);
    line-height: var(--go-leading-sm);
    height: calc(var(--go-leading-sm) * 2);
  }

  .vitrina-card__foot { min-height: var(--go-control-h-sm); }
  .vitrina-card__foot .go-price { font-size: var(--go-text-md); }

  .vitrina__day-label {
    font-size: var(--go-text-xl);
    line-height: var(--go-leading-xl);
  }

  /* The count stops being an end-aligned trailer and takes its own line: at
   * this width «7 novedades · 2 tiendas» beside «Miércoles 26 de agosto» wraps
   * to two lines anyway, and `margin-inline-start: auto` on a wrapped flex item
   * pushes it to the far edge of a line it is alone on. */
  .vitrina__day-count {
    margin-inline-start: 0;
    width: 100%;
  }
}

/* ============================================================================
 * The queue's own text, editable where it is read
 * ----------------------------------------------------------------------------
 * «Cola de decisiones» prints a card and asks a question about it, and on three
 * of its steps part of that card is PROSE somebody may be right about: a
 * draft's headline and summary, a shop's cleaned-up title, a platform's blurb.
 * Those are textareas now (admin/queue/_texto.html.erb), and this is what stops
 * them looking like a form.
 *
 * It invents no face. `font: inherit` is the whole trick — the box takes the
 * type of whatever it is standing in, so the same rule serves a .go-h4 title
 * and a .go-caption note — and the two states it does draw are the system's own
 * surface tokens.
 *
 * NO PADDING AND NO BORDER, and that is not a style choice. The panel is
 * measured against a 4px grid at three widths (test/system/alignment_test.rb,
 * which checks the absolute offset of every button on the page), every
 * line-height token is a multiple of 4, and autosize_controller.js sizes the
 * box in whole lines. A box that is a whole number of lines tall and nothing
 * else therefore occupies exactly what the paragraph it replaced did. Give it
 * 8px of padding and every card grows 16px — fine — but give it 6 and every
 * control below it on the page lands 4px off the grid, and the gate fails on a
 * page nobody touched.
 *
 * So the affordance is drawn with what costs no layout: a background on hover,
 * an outline on focus (outlines are painted outside the box), and a box-shadow
 * spread that fakes the padding the box may not have.
 * ========================================================================== */
.admin-write {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  resize: none;
  /* Never `hidden`: with JavaScript off the box keeps its `rows` and the text
   * past them has to stay reachable. Once it is sized to its content there is
   * nothing to scroll and no bar appears. */
  overflow: auto;
  border-radius: var(--go-radius-sm);
  transition: background-color var(--go-duration-fast) var(--go-ease),
              box-shadow var(--go-duration-fast) var(--go-ease);
}

.admin-write::placeholder { color: var(--go-color-ink-subtle); }

.admin-write:hover {
  background: var(--go-sand-50);
  box-shadow: 0 0 0 var(--go-space-2) var(--go-sand-50);
}

.admin-write:focus {
  background: var(--go-color-surface);
  box-shadow: 0 0 0 var(--go-space-2) var(--go-color-surface);
  outline: 2px solid var(--go-color-brand);
  outline-offset: var(--go-space-2);
}

/* The weekly signup is a flex item on the home CTA even though Turbo needs a
 * frame around it. The frame takes the sizing contract the form held before
 * enhancement, and the success callout can replace it without moving the
 * neighbouring copy. */
.go-cta > turbo-frame {
  flex: 1 1 320px;
  min-width: 0;
  max-width: 480px;
}

/* A conventional honeypot: present to form-filling bots, outside the visible
 * and keyboard layout for readers. */
.digest-honeypot {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.digest-note {
  padding: calc(var(--go-space-5) - var(--go-border-width));
  border: var(--go-border);
  /* --xl, and not --lg: the note sits directly on top of the first section
   * tile, and two adjacent cards with 16px and 20px corners read as a mistake
   * at exactly the size where nobody can name what is wrong. */
  border-radius: var(--go-radius-xl);
  background: var(--go-color-ink);
  color: var(--go-color-surface);
  box-shadow: var(--go-shadow-cat);
}

.digest-note h2 {
  margin-bottom: var(--go-space-3);
  font-size: var(--go-text-lg);
  line-height: var(--go-leading-lg);
}

/* The signature is the note's own voice, so it is set apart from the note the
 * way the mail sets it apart: a line of its own, in the same ink. */
.digest-note__sign {
  margin-top: var(--go-space-3);
  font-weight: var(--go-weight-bold);
}

/* An edition's sections are ordinary tiles: a tile carries its own category
 * colour through --go-cat, so the hex marker, the tinted shadow and the footer
 * link all follow from `category:` at the call site. That is where the six
 * per-slug border rules that used to live here went.
 *
 * Nothing here sets `scroll-margin-top`. The contents chips jump to these
 * tiles, and the page above already answers that for every anchored element:
 * `[id]` reserves 64px of app bar plus 16 of air. A `.digest-section` rule
 * would have the same specificity and win by being later in the file — it was
 * written, it set 24px, and it put every heading the chips aimed at back under
 * the app bar it was supposed to clear. */

/* ONE right edge for the text of a row, at a measure somebody can read.
 *
 * A `.go-tile` inside the narrow column is ~700px, and a 13px paragraph across
 * 700px runs 100 characters — `68ch` does not cap that, because `ch` is the
 * advance of "0" (8.5px in Outfit) while lowercase Outfit averages ~5.6. The
 * cap is therefore in rem and MEASURED, not derived: 26rem renders 70-76
 * characters per line, inside the 45-75 a reader is comfortable with.
 *
 * It goes on the BODY rather than on the paragraph because the title, the meta
 * line and the summary are three font sizes that were ending at three
 * different x — one block of text with one ragged edge, not three. */
.digest-section .go-row-item__body { max-width: 26rem; }

/* A summary is prose in a row that is otherwise labels: it reads at the meta
 * line's size but in the body's ink, because a paragraph set in the subtle
 * grey the meta line uses is the longest and faintest thing on the row. */
.digest-item__summary {
  margin-top: var(--go-space-1);
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  color: var(--go-color-ink-muted);
}

/* Campaigns and BGA arrivals are a short name under a picture — the shape the
 * registry calls `layout: :cards`, and the shape the letter has always given
 * them. As rows they were the weakest block on the page: six one-word names,
 * each alone in a 637px line box. `auto-fill` and not `auto-fit` so a section
 * with two cards keeps the card size the section with six has. */
.digest-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--go-space-4);
}

.digest-card {
  display: flex;
  flex-direction: column;
  gap: var(--go-space-2);
  min-width: 0;
  color: inherit;
  text-decoration: none;
}

/* A FIXED height and not a ratio box, which is the system's own rule for media
 * that has content under it (gameon.css, `.go-aspect--h-*`): a ratio on a fluid
 * column gives a fractional height, and everything stacked below inherits the
 * fraction and leaves the grid.
 *
 * `contain` and not the system's `cover`: these are a campaign's promo card and
 * a Board Game Arena icon, two shapes that are not this box's shape, and
 * cropping either one cuts the thing the reader came to recognise. The sand
 * ground the system paints behind an `.go-aspect` is what the letterbox lands
 * on, and it is the same ground every empty `.go-thumb` on the site shows. */
.digest-card__cover.go-aspect { aspect-ratio: auto; height: 96px; }
.digest-card__cover img { object-fit: contain; }

.digest-card__name {
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  font-weight: var(--go-weight-bold);
  color: var(--go-color-ink);
}

.digest-card__meta {
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  color: var(--go-color-ink-subtle);
}

/* An edition's rows are read, not scanned, and a headline clipped to one line
 * with an ellipsis is the same truncation this page exists to be rid of, one
 * element higher up. `.go-row-item__title--wrap` is the system's own answer and
 * the component exposes no way to ask for it, so the rule is written the way
 * `.notification-row` above writes it. Nothing pays for the wrap: these rows
 * are already variable height, since most of them carry a summary. */
.digest-section .go-row-item__title,
.digest-section .go-row-item__meta { white-space: normal; }

/* The price in a line about the reader's own games — see
 * DigestHelper#digest_amounts. <strong> already carries the weight; what the
 * class adds is the ink, so the amount steps out of the meta line's grey
 * without becoming a second colour on the row. */
.digest-amount { color: var(--go-color-ink); }

/* The account controls are the app bar's fixed-size tail. The navigation and
 * search are what may shrink; the brand and these controls keep their names.
 * The menu is an overlay on desktop so opening it cannot change the bar's
 * measured 64px height or push every public page down. */
.reader-actions {
  display: flex;
  align-items: center;
  gap: var(--go-space-2);
  flex: 0 0 auto;
  margin-inline-start: auto;
}

.reader-menu {
  position: relative;
  display: flex;
  flex: 0 0 auto;
}

.reader-menu__panel {
  position: absolute;
  z-index: var(--go-z-dropdown);
  inset-block-start: calc(100% + var(--go-space-2));
  inset-inline-end: 0;
  inline-size: 264px;
  padding: var(--go-space-2);
}

.reader-menu__logout-form { display: contents; }

.reader-menu__logout {
  inline-size: 100%;
  border: 0;
  background: transparent;
  text-align: start;
  cursor: pointer;
}

.reader-menu__mobile {
  margin-block-start: var(--go-space-2);
  padding-block-start: var(--go-space-2);
  /* A structural 2px border would put every account row that follows it two
   * pixels off the page's 4px rhythm. Paint the same divider inside the box so
   * it costs no flow height. */
  box-shadow: inset 0 var(--go-border-width) 0 var(--go-color-line);
}

.go-choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: var(--go-space-2);
}

.notification-row {
  --go-lead-w: var(--go-space-5);
}

.notification-row .go-row-item__title,
.notification-row .go-row-item__meta,
.subscribed-game-row .go-row-item__meta {
  white-space: normal;
}

.notification-row--unread {
  background: color-mix(in srgb, var(--go-color-brand) 8%, transparent);
  box-shadow: inset var(--go-border-width) 0 0 var(--go-color-brand);
}

.subscription-split { display: inline-flex; align-items: start; }
.subscription-split > form { display: flex; }
/* The split is «Te avisaremos» and its «Ajustar avisos» half: a form followed
   by a link. Keyed on `form + a` and not on any link, because the signed-out
   «Avísame» is itself a lone link in here — `> a` squared its start corners
   and pulled it a border's width left, a pill cut off on one side. */
.subscription-split:has(> form + a) > form .go-btn {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.subscription-split > form + a {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  margin-inline-start: calc(-1 * var(--go-border-width));
}
.subscription-explanation {
  max-inline-size: min(400px, calc(100vw - var(--go-space-8)));
  padding: calc(var(--go-space-4) - var(--go-border-width));
  margin: auto;
}
.subscription-explanation:not(:popover-open) { display: none; }
/* Beside the «?» that opened it, not adrift in the middle of the screen. The
   popover is in the top layer, so only anchor positioning can tie it to the
   button; a browser without it keeps the centred popover above, which is the
   old behaviour and still readable. */
@supports (position-area: block-end) {
  .subscription-explanation {
    inset: auto;
    margin: 0;
    /* The «?» sits at the right end of the section head, so a popup centred on
       it would hang over the page gutter. These margins are the page column's
       own edges, which is where it stops instead — anchor-center shifts the box
       to stay inside them. */
    margin-inline: calc(max(0px, (100% - var(--go-container-max)) / 2) + var(--go-container-pad));
    margin-block-start: var(--go-space-2);
    position-area: block-end span-all;
    justify-self: anchor-center;
    position-try-fallbacks: flip-block;
  }
}
.subscription-dialog { max-block-size: calc(100dvh - var(--go-space-8)); overflow-y: auto; }
.reader-games-table { --go-table-min: 720px; border-collapse: separate; border-spacing: 0; }
.reader-games-table thead th { border-bottom: 0; box-shadow: inset 0 -2px 0 var(--go-color-ink); }
.reader-games-table td {
  vertical-align: top;
  padding-block: var(--go-space-4);
  border-bottom: 0;
  box-shadow: inset 0 -2px 0 var(--go-color-line);
}
.reader-games-table tr:last-child td { box-shadow: none; }
.reader-games-table .game-alert { display: block; min-inline-size: 220px; }
.reader-game-name { position: relative; min-inline-size: 200px; flex-wrap: nowrap; }
.admin-rail-separator {
  block-size: var(--go-space-3);
  margin-block: var(--go-space-1);
  box-shadow: inset 0 var(--go-border-width) 0 var(--go-color-line);
}

/* A price-drop row can carry two badges. On the narrowest phones those
 * facts take their own line so they cannot squeeze the title out of the row. */
@media (max-width: 359px) {
  .home-release { display: grid; grid-template-columns: max-content minmax(0, 1fr); }
  .home-release .go-row-item__aside { grid-column: 1 / -1; }
}

@media (max-width: 700px) {
  .go-cta > turbo-frame { max-width: none; }
  .go-cta > .go-cta__body,
  .go-cta > turbo-frame,
  .go-cta__actions { flex-basis: auto; flex-grow: 0; }

  /* The alert's stock sentence can be wider than the viewport at its
   * max-content size. On a phone it gets its own row under «Volver», which
   * gives the sentence a real wrapping width and leaves the section head no
   * 606px intrinsic minimum. */
  .game-subscription-actions {
    flex: 1 1 100%;
    inline-size: 100%;
    min-width: 0;
  }

  .game-subscription-actions > .game-alert {
    flex: 1 1 100%;
    min-width: 0;
    max-width: 100%;
  }
}

/* The stored email is an isolated document, with its own email styles. */
.admin-digest-preview {
  width: 100%;
  height: 960px;
  border: var(--go-border);
  border-radius: var(--go-radius-md);
}

/* No link in an edition is underlined, in any state (Iván, 2026-09-18) — the
 * letter's links carry `text-decoration:none` inline, and «Ver en el navegador»
 * must not be the one place they are. `a` and not a list of classes, so the
 * `.go-link` pair under the edition and whatever a row component draws next
 * year are covered too; a row still answers hover with its sand ground. */
.digest-public a,
.digest-public a:hover { text-decoration: none; }

/* Admin inventories fit their content column, including the tablet rail.
 * Separate borders keep inline controls on whole-pixel row boundaries. */
.admin-inventory-wrap { container-type: inline-size; }
.admin-inventory {
  --go-table-min: 0px;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
}
.admin-inventory td {
  padding-block: var(--go-space-3) calc(var(--go-space-3) - var(--go-border-width-hair));
  overflow-wrap: anywhere;
}
.admin-inventory tbody tr:last-child td { padding-block-end: var(--go-space-3); }
.admin-inventory .admin-rowlink { font-weight: var(--go-weight-semibold); }
.admin-inventory--channels th:nth-child(1) { width: 32%; }
.admin-inventory--channels th:nth-child(2) { width: 21%; }
.admin-inventory--channels th:nth-child(3) { width: 10%; }
.admin-inventory--channels th:nth-child(4) { width: 23%; }
.admin-inventory--channels th:nth-child(5) { width: 14%; }
.admin-inventory--stores th:nth-child(1) { width: 25%; }
.admin-inventory--stores th:nth-child(2) { width: 21%; }
.admin-inventory--stores th:nth-child(3) { width: 13%; }
.admin-inventory--stores th:nth-child(4) { width: 22%; }
.admin-inventory--stores th:nth-child(5) { width: 19%; }
.admin-inventory--dictionary th:nth-child(1),
.admin-inventory--dictionary th:nth-child(2) { width: 38%; }
.admin-inventory--dictionary th:nth-child(3) { width: 24%; }
.admin-inventory--editions th:nth-child(1) { width: 28%; }
.admin-inventory--editions th:nth-child(2) { width: 22%; }
.admin-inventory--editions th:nth-child(3) { width: 14%; }
.admin-inventory--editions th:nth-child(4) { width: 22%; }
.admin-inventory--editions th:nth-child(5) { width: 14%; }
/* The two newsletter editor tables. «Orden» holds an input and a drag handle;
 * the control columns are sized around them; the text column takes what is left,
 * which is the whole point of editing a line here rather than in a card. */
.admin-inventory--digest th:nth-child(1) { width: 136px; }
.admin-inventory--digest th:nth-child(3) { width: 128px; }
.admin-inventory--digest th:nth-child(4) { width: 96px; }
.admin-inventory--digest-options th:nth-child(1) { width: 96px; }
.admin-inventory--digest-options th:nth-child(3) { width: 128px; }
.admin-inventory-wrap-text { overflow-wrap: anywhere; }

.admin-digest-handle { cursor: grab; touch-action: none; user-select: none; }
.admin-inventory--digest tr[data-dragging] .admin-digest-handle { cursor: grabbing; }
.admin-inventory--digest tr[data-dragging] td { background: var(--go-color-brand); }

/* Supplementary facts stay available on hover, keyboard focus and a tap.
 * No scroller ancestor: it would clip the tooltip along with the table. */
.admin-hint {
  position: relative;
  display: inline-block;
  max-width: 100%;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  cursor: help;
}
.admin-hint__text {
  position: absolute;
  z-index: 10;
  inset-inline-end: 0;
  inset-block-start: 100%;
  width: max-content;
  max-width: min(280px, calc(100vw - 48px));
  padding: var(--go-space-3);
  border: var(--go-border);
  border-radius: var(--go-radius-sm);
  background: var(--go-color-surface);
  color: var(--go-color-ink);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  font-weight: var(--go-weight-regular);
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: start;
  text-decoration: none;
  box-shadow: var(--go-shadow-sm);
}
.admin-hint:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.admin-inventory td:first-child .admin-hint__text { inset-inline-start: 0; inset-inline-end: auto; }
/* The tablero's stat row explains each «+N» with a hint. `.go-stats` clips
 * to its radius, which would cut the bubble off at the row's bottom edge, and
 * every hint sits just right of a short figure at a cell's start, so the
 * bubble opens rightward from there (tooltip_controller slides one that would
 * leave the viewport back inside).
 *
 * The cells take their width from their labels: seven equal 160px cells do not
 * fit the 980px column beside the rail at 1280, so the system's basis wrapped
 * «Usuarios» onto a row of its own, while the seven need about 800 at their
 * content width (830 with every figure at five digits; 2026-09-30). */
.admin-stats { overflow: visible; }
.admin-stats .go-stat { flex-basis: auto; }
.admin-stats .admin-hint__text { inset-inline-start: 0; inset-inline-end: auto; }

/* Same labelled stacks as the system, based on available width rather than
 * viewport width: at 768px the open rail leaves only half a table's measure. */
@container (max-width: 900px) {
  .admin-inventory thead { display: none; }
  .admin-inventory, .admin-inventory tbody, .admin-inventory tr, .admin-inventory td {
    display: block;
    width: auto;
  }
  .admin-inventory tr {
    padding: var(--go-space-3) 0 calc(var(--go-space-3) - var(--go-border-width));
    border-bottom: var(--go-border-width) solid var(--go-color-line-soft);
  }
  .admin-inventory tbody td {
    height: auto;
    min-height: var(--go-control-h-sm);
    border: 0;
    padding: var(--go-space-1) 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--go-space-3);
  }
  .admin-inventory td::before {
    content: attr(data-label);
    flex: none;
    font-size: var(--go-text-2xs);
    line-height: var(--go-leading-2xs);
    font-weight: var(--go-weight-extrabold);
    letter-spacing: var(--go-tracking-caps);
    text-transform: uppercase;
    color: var(--go-color-ink-subtle);
  }
  .admin-inventory tbody tr:last-child td { padding-block-end: var(--go-space-1); }
  .admin-inventory td:first-child::before { display: none; }
  .admin-inventory td > * { min-width: 0; }
  .admin-inventory--dictionary td:first-child::before { display: block; }
  .admin-inventory--dictionary td::before { width: 88px; }
  .admin-inventory--dictionary td:last-child > * { margin-inline-start: auto; }
  /* The newsletter tables open on a control, not on a title, so their first
   * cell keeps its label: an unlabelled number box at the top of a stacked
   * card says nothing about what the number orders. */
  .admin-inventory--digest td:first-child::before,
  .admin-inventory--digest-options td:first-child::before { display: block; }
  .admin-inventory--digest td::before,
  .admin-inventory--digest-options td::before { width: 88px; }
  .admin-inventory--digest td:last-child > *,
  .admin-inventory--digest-options td:last-child > * { margin-inline-start: auto; }
  /* The text of the line is what someone came to this card to edit: it takes
   * the width the label does not, instead of sitting at its intrinsic size. */
  .admin-inventory--digest td:nth-child(2) > *,
  .admin-inventory--digest-options td:nth-child(2) > * { flex: 1; }
}

/* Community marketplace: the seller's shelf, within the Tablero system. */
.market-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  border: var(--go-border-width) solid var(--go-color-line);
  border-radius: var(--go-radius-lg);
  overflow: hidden;
  background: var(--go-color-brand);
  box-shadow: 4px 4px 0 var(--go-color-line);
}
.market-hero__identity { padding: 30px; position: relative; background: var(--go-color-surface); }
.market-hero__contact { padding: 30px; background: var(--go-color-brand); }
.market-contact { padding: var(--go-space-4); background: var(--go-color-brand); border-radius: var(--go-radius-md); }
.market-contact__text { font-size: var(--go-text-lg); line-height: var(--go-leading-lg); }
.market-hero__contact .market-contact { padding: 0; border-radius: 0; }
.market-hero h1 { overflow-wrap: anywhere; }
.market-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.market-card { position: relative; min-width: 0; }
.market-card__visual {
  display: flex; align-items: center; justify-content: center;
  height: 240px; background: var(--go-color-surface-sunk); overflow: hidden;
  border-radius: var(--go-radius-md) var(--go-radius-md) 0 0;
}
.market-card__photo { display: block; width: 100%; height: 100%; object-fit: cover; }
.market-card__visual .go-thumb { width: 160px; height: 192px; }
.market-card__body { padding: 22px; flex: 1; }
.market-card__title { color: inherit; text-decoration: none; overflow-wrap: anywhere; }
.market-card__title:hover { text-decoration: underline; }
.market-card__notes { overflow-wrap: anywhere; }
.market-card__bottom { margin-top: auto; display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }
.market-price { display: flex; align-items: baseline; flex-wrap: wrap; column-gap: 8px; row-gap: 4px; }
.market-card[data-sold="true"] .market-card__visual { filter: grayscale(1); }
.market-detail { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 40px; }
.market-detail__photo { display: block; width: 100%; height: 480px; object-fit: contain; background: var(--go-color-surface-sunk); border-radius: var(--go-radius-lg); }
.market-detail__fallback { height: 400px; display: flex; align-items: center; justify-content: center; background: var(--go-color-surface-sunk); border-radius: var(--go-radius-lg); }
.market-detail__fallback .go-thumb { width: 240px; height: 280px; }
.market-record { position: relative; overflow-wrap: anywhere; }
.market-form, .market-detail, .market-card { overflow-wrap: anywhere; }
.market-prose { white-space: pre-wrap; overflow-wrap: anywhere; }
.market-form { max-width: 800px; }
.market-form__prices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.market-file { display: block; max-width: 100%; min-height: 40px; font: inherit; }
.market-photo-preview { display: block; width: 96px; height: 96px; object-fit: cover; border-radius: var(--go-radius-md); }
.market-honeypot { display: none; }
@media (max-width: 1000px) { .market-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px) {
  .market-hero, .market-detail, .market-form__prices { grid-template-columns: minmax(0, 1fr); }
  .market-hero__identity { padding: 22px 22px 24px; }
  .market-hero__contact { padding: 24px 22px 22px; }
  .market-grid { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .market-detail { gap: 24px; }
  .market-detail__photo { height: 320px; }
}

.market-card__monogram { font-size: 64px; line-height: 80px; font-weight: 800; color: var(--go-color-ink-subtle); }

.market-upload { padding: calc(var(--go-space-4) - 2px); border: 2px dashed var(--go-color-line-medium); border-radius: var(--go-radius-md); }

/* Notes can wrap to several lines. Separate borders keep these variable-height
   rows on the grid, and stacked cells grow with their content on phones. */
.market-sales-table { border-collapse: separate; border-spacing: 0; overflow-wrap: anywhere; }
.market-sales-table thead th { border-bottom: 0; box-shadow: inset 0 -2px 0 var(--go-color-line); }
.market-sales-table tbody td { padding-block: var(--go-space-3); border-bottom: 0; vertical-align: top; box-shadow: inset 0 -2px 0 var(--go-color-line-soft); }
.market-sales-table tbody tr:last-child td { box-shadow: none; }
@media (max-width: 700px) {
  .market-sales-table caption { display: block; width: 100%; }
  .market-sales-table tbody td { height: auto; min-height: var(--go-control-h-sm); padding-block: var(--go-space-2); box-shadow: none; }
}

/* The invited manager's banner. `.go-callout` is already a flex row, so the
   sentence and the way out of delegated mode sit side by side and the button
   drops below the text on a phone rather than squeezing into it. */
.market-delegation__banner { flex-wrap: wrap; align-items: center; }
.market-delegation__banner p { flex: 1 1 320px; min-width: 0; }
.market-delegation__banner form { flex: none; }

/* «Imágenes para redes» (`my_marketplace/images`, `collage_controller.js`).
 * The pictures themselves are drawn on canvases by `lib/collage.js`; this is
 * the screen around them. Every height in the page rhythm is a multiple of 4. */
.market-collage {
  /* Read by `collage_controller.js`: 1 while the screen is one stacked
   * column, which is when the corner thumbnail is worth showing. */
  --market-collage-stacked: 1;
  display: grid;
  gap: var(--go-space-5);
  grid-template-areas: "preview" "settings" "games";
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}
.market-collage__preview { grid-area: preview; min-width: 0; }
.market-collage__settings { grid-area: settings; min-width: 0; }
.market-collage__games-card { grid-area: games; min-width: 0; }
@media (min-width: 960px) {
  .market-collage {
    --market-collage-stacked: 0;
    grid-template-areas: "preview preview" "settings games";
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  }
  /* The settings are short; they stay in view beside the long list. */
  .market-collage__settings { position: sticky; top: calc(var(--go-appbar-h) + var(--go-space-6)); }
}

/* Fixed-width columns, not fractions: a square of a fractional width has a
 * fractional height, and everything below it would leave the 4px grid. */
.market-collage__pages {
  --market-collage-page: 160px;
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--market-collage-page));
  gap: var(--go-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 768px) { .market-collage__pages { --market-collage-page: 224px; } }
@media (min-width: 1200px) { .market-collage__pages { --market-collage-page: 272px; } }
.market-collage__page { display: grid; gap: var(--go-space-2); }

/* The frame opens a picture big. No border, radius or shadow of its own: the
 * picture is shown as the PNG will be, its transparent corners included. */
.market-collage__frame {
  display: block;
  width: var(--market-collage-page);
  height: var(--market-collage-page);
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}
.market-collage__frame:focus-visible { outline: var(--go-focus-width) solid var(--go-focus-color); outline-offset: var(--go-focus-offset); }
.market-collage__frame canvas { display: block; width: 100%; height: 100%; }
.market-collage__pagefoot { display: flex; align-items: center; justify-content: space-between; gap: var(--go-space-2); min-width: 0; }

.market-collage__notice {
  padding: var(--go-space-2) var(--go-space-3);
  background: var(--go-color-warning-surface);
  border-radius: var(--go-radius-sm);
  color: var(--go-color-ink);
}

.market-collage__fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
.market-collage__fieldset legend { padding: 0; margin-bottom: var(--go-space-2); }
.market-collage__per { position: relative; cursor: pointer; }
.market-collage__per input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.market-collage__per input:checked + .go-chip { background: var(--go-color-brand); }
.market-collage__per input:focus-visible + .go-chip,
.market-collage__pick input:focus-visible + .market-collage__tick {
  outline: var(--go-focus-width) solid var(--go-focus-color);
  outline-offset: var(--go-focus-offset);
}

.market-collage__games { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--go-space-2); margin: 0; padding: 0; list-style: none; }
.market-collage__game {
  border-radius: var(--go-radius-md);
  background: var(--go-color-surface);
  box-shadow: inset 0 0 0 2px var(--go-color-line-soft);
}
.market-collage__game[data-selected] { background: var(--go-color-brand-surface); box-shadow: inset 0 0 0 2px var(--go-color-line); }
.market-collage__pick {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--go-space-3);
  min-height: 72px;
  padding: var(--go-space-2) var(--go-space-3);
  cursor: pointer;
}
.market-collage__pick input { position: absolute; opacity: 0; width: 24px; height: 24px; margin: 0; left: 12px; }
/* An empty circle when left out; a tick on yellow when in. */
.market-collage__tick {
  display: grid;
  place-items: center;
  flex: none;
  width: 24px;
  height: 24px;
  border: var(--go-border);
  border-radius: 50%;
  background: var(--go-color-surface);
}
.market-collage__game[data-selected] .market-collage__tick { background: var(--go-color-brand); }
.market-collage__game[data-selected] .market-collage__tick::after { content: "✓"; font-size: var(--go-text-xs); line-height: var(--go-leading-xs); font-weight: 800; }
.market-collage__thumb {
  display: block;
  flex: none;
  width: 48px;
  height: 56px;
  object-fit: cover;
  border-radius: var(--go-radius-xs);
  background: var(--go-color-surface-sunk);
}
.market-collage__gamebody { display: flex; flex-direction: column; min-width: 0; }
.market-collage__gamename {
  font-weight: 600;
  font-size: var(--go-text-md);
  line-height: var(--go-leading-md);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The rename field: only for a ticked game, and only while the switch above
 * the list is on. */
.market-collage__label { display: none; padding: 0 var(--go-space-3) var(--go-space-3) calc(var(--go-space-3) + 24px + var(--go-space-3)); }
.market-collage__games[data-editing] .market-collage__game[data-selected] .market-collage__label { display: flex; }
.market-collage__label .go-input-group { flex: 1; min-width: 0; }
.market-collage__label .go-input { min-width: 0; }

.market-collage__zoom {
  max-width: min(96vw, 1080px);
  max-height: 96vh;
  padding: var(--go-space-3);
  border: var(--go-border);
  border-radius: var(--go-radius-lg);
  background: var(--go-color-surface);
}
.market-collage__zoom::backdrop { background: rgb(23 21 15 / 0.7); }
.market-collage__zoom > p { margin: 0 0 var(--go-space-2); text-align: center; }
.market-collage__zoom img {
  display: block;
  max-width: 100%;
  /* The title line above and the button row below, with their gaps. */
  max-height: calc(96vh - 136px);
  margin: 0 auto var(--go-space-3);
}

/* The phone's corner thumbnail while the pictures are scrolled away, above
 * the 56px tab bar. */
.market-collage__peek {
  position: fixed;
  right: var(--go-space-4);
  bottom: calc(var(--go-space-14) + var(--go-space-4));
  z-index: var(--go-z-sticky);
  width: 88px;
  height: 88px;
  padding: 0;
  border: var(--go-border);
  border-radius: var(--go-radius-sm);
  background: var(--go-color-surface);
  box-shadow: var(--go-shadow-md);
  overflow: hidden;
}
.market-collage__peek[hidden] { display: none; }
.market-collage__peek img { display: block; width: 100%; height: 100%; }
.market-collage__peekcount {
  position: absolute;
  right: 4px;
  bottom: 4px;
  padding: 0 6px;
  border-radius: var(--go-radius-pill);
  background: var(--go-color-ink);
  color: var(--go-color-ink-invert);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  font-weight: 700;
}
.market-collage__peekcount:empty { display: none; }

/* ============================================================================
 * «Lugares para jugar» — /lugares
 * ----------------------------------------------------------------------------
 * La dirección «Guía» de las maquetas, simplificada: la ciudad es el eje, la
 * imagen es lo primero y no hay precios, mesas, ludoteca, etiquetas ni
 * categorías. Ningún valor está inventado; todo sale de los tokens --go-*
 * salvo las anchuras de `@media`, donde un `var()` no es legal.
 * ========================================================================== */

.place-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--go-space-5);
}

.place-city { margin-bottom: var(--go-space-10); }

.place-city__head {
  display: flex;
  align-items: baseline;
  gap: var(--go-space-3);
  margin-bottom: var(--go-space-4);
  padding-bottom: var(--go-space-2);
  border-bottom: var(--go-border);
}

.place-city__name {
  margin: 0;
  font-size: var(--go-text-2xl);
  line-height: var(--go-leading-2xl);
  font-weight: var(--go-weight-black);
  letter-spacing: var(--go-tracking-tight);
}

.place-city__count {
  margin-left: auto;
  color: var(--go-color-ink-subtle);
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
}

.place-cities { margin-bottom: var(--go-space-6); }

/* --- la ficha ------------------------------------------------------------ */

/* `padding:0` y el cuerpo pone el suyo: la imagen llega a los bordes, que es
   lo único que esta dirección tiene en lugar de una foto. */
.place-card { padding: 0; overflow: hidden; gap: 0; }

.place-card__media {
  display: block;
  border-bottom: var(--go-border);
  background: var(--go-sand-200);
}

/* La fachada generada y el logo subido ocupan la MISMA caja, y la caja la fija
   la proporción: sin esto una rejilla de tarjetas con logos de formas
   distintas deja de tener una línea de títulos. */
.place-card__logo,
.place-card .place-facade,
.place-show__logo,
.place-show .place-facade {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 20 / 11;
  object-fit: cover;
}

.place-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--go-space-2);
  padding: var(--go-card-inset);
}

.place-card__title {
  margin: 0;
  font-size: var(--go-text-xl);
  line-height: var(--go-leading-xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
}

/* Toda la tarjeta es el blanco, no sólo las letras del título. El seudoelemento
   se queda por DEBAJO de la insignia de `admin_peek`, que es absoluta y vive en
   la esquina: un operador tiene que poder pulsarla sin abrir la ficha. */
.place-card__link { color: inherit; text-decoration: none; }
.place-card__link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.place-card { position: relative; }

.place-card__blurb {
  margin: 0;
  color: var(--go-color-ink-muted);
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
}

.place-card__address,
.place-card__hours {
  display: flex;
  align-items: flex-start;
  gap: var(--go-space-2);
  margin: 0;
  color: var(--go-color-ink-subtle);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
}

/* El icono mide 16 y la línea 16: no hay diferencia que compensar. */
.place-card__address .go-icon,
.place-card__hours .go-icon { color: var(--go-cat); }

/* --- la ficha completa --------------------------------------------------- */

.place-show {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--go-space-5);
  margin-bottom: var(--go-space-8);
}

.place-show__media {
  border: var(--go-border);
  border-radius: var(--go-radius-xl);
  overflow: hidden;
  box-shadow: var(--go-shadow-md);
  background: var(--go-sand-200);
}

.place-defs { display: grid; gap: var(--go-space-4); margin: 0; }
.place-def { min-width: 0; }

.place-def__k {
  display: block;
  color: var(--go-color-ink-subtle);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  font-weight: var(--go-weight-bold);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
}

.place-def__v {
  display: block;
  margin: 0;
  font-size: var(--go-text-md);
  line-height: var(--go-leading-md);
  font-weight: var(--go-weight-medium);
}

.place-nearby { margin-bottom: var(--go-space-10); }

/* --- el formulario ------------------------------------------------------- */

.place-form__errors { margin: 0; padding-left: var(--go-space-5); }

/* El panel se ancla al CAMPO y no al diálogo: un <dialog> es `overflow:auto` y
   recorta contra sí mismo cualquier cosa fuera del flujo. */
.place-form__address { position: relative; }
.place-form__panel { left: 0; right: 0; min-width: 0; }
.place-form__options { margin: 0; padding: 0; list-style: none; }

.place-form__two {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--go-space-4);
}

/* Envuelve. Tres controles en el pie de un diálogo de teléfono no caben en una
   línea, y el que envía es el que se sale.
   El filete se paga del padding —16 − 2 = 14— porque si no, el pie mide 2px de
   más y todo lo que hay dentro sale de la rejilla de 4: la compuerta de
   alineación lo cazó con el botón de enviar a un offset absoluto de 1066. */
.place-form__foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--go-space-2);
  margin-top: var(--go-space-6);
  padding-top: calc(var(--go-space-4) - var(--go-border-width));
  border-top: var(--go-border-soft);
}

.place-dialog { width: min(40rem, calc(100vw - 2 * var(--go-space-4))); }

@media (min-width: 900px) {
  .place-show { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
  .place-defs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 640px) {
  .place-form__two { grid-template-columns: 1fr; }
  .place-form__foot .go-push { display: none; }
  .place-form__foot .go-btn { flex: 1 1 auto; }
}

/* --- panel · lugares ------------------------------------------------------ */

.admin-place-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--go-space-4);
}

.admin-place-preview {
  max-width: 320px;
  margin-top: var(--go-space-2);
  border: var(--go-border);
  border-radius: var(--go-radius-md);
  overflow: hidden;
}
.admin-place-preview img,
.admin-place-preview .place-facade { display: block; width: 100%; height: auto; }

.admin-hours { display: grid; gap: var(--go-space-2); }
.admin-hours__row {
  display: grid;
  grid-template-columns: 3rem 1fr auto 1fr;
  align-items: center;
  gap: var(--go-space-2);
}
.admin-hours__day {
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  font-weight: var(--go-weight-bold);
}

.admin-suggestion__notes {
  margin: var(--go-space-2) 0 0;
  color: var(--go-color-ink-muted);
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
}
.admin-suggestion__facts {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--go-space-2) var(--go-space-3);
  margin-top: var(--go-space-2);
}

@media (max-width: 640px) {
  .admin-place-grid { grid-template-columns: 1fr; }
}

/* ---------------------------------------------------------------------------
 * «New» — the dot on a row a signed-in reader has not seen (ReaderNews,
 * seen_controller.js).
 *
 * A reader with no account never gets any of this: the rows carry no
 * `data-seen-key` and the controller is not on the page.
 *
 * The dot is a <button> the controller appends to the row's <li>, BESIDE the
 * row's link, and it is absolutely positioned over the top-left corner of the
 * row's figure — measured by the controller into --seen-x / --seen-y, so it
 * lands on the corner of a 36×48 box, a 64×36 video or a 48×48 date block alike
 * and costs the row no pixel of layout. The 24px box is the hit target (WCAG
 * 2.5.8); the 12px disc centred in it is what shows.
 * ------------------------------------------------------------------------ */
.seen-host { position: relative; }

.seen-dot {
  position: absolute;
  z-index: 2;
  left: calc(var(--seen-x, 0px) - 12px);
  top: calc(var(--seen-y, 0px) - 12px);
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}
.seen-dot:focus-visible {
  outline: var(--go-focus-width) solid var(--go-focus-color);
  outline-offset: 0;
}

.seen-dot__disc {
  width: 12px;
  height: 12px;
  border: var(--go-border-width) solid var(--go-color-line);
  border-radius: 50%;
  background: var(--go-color-brand);
  /* A 2px paper ring, so the disc keeps an edge on dark box art. */
  box-shadow: 0 0 0 2px var(--go-color-surface);
  transition: transform var(--go-duration-fast) var(--go-ease);
}
.seen-dot:hover .seen-dot__disc { transform: scale(1.25); }

/* The pop: the disc swells and vanishes, a ring spreads, six sparks fly. */
.seen-dot__ring,
.seen-dot__spark {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.seen-dot__ring {
  inset: 6px;
  border: var(--go-border-width) solid var(--go-color-brand);
  border-radius: 50%;
}
.seen-dot__spark {
  left: 50%;
  top: 50%;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: var(--go-color-ink);
}
.seen-dot__spark:nth-child(odd) { background: var(--go-color-brand-active); }

.seen-dot.is-popping { pointer-events: none; }
.seen-dot.is-popping .seen-dot__disc { animation: seen-pop 360ms cubic-bezier(.3, 1.6, .6, 1) forwards; }
.seen-dot.is-popping .seen-dot__ring { animation: seen-ring 420ms ease-out forwards; }
.seen-dot.is-popping .seen-dot__spark { animation: seen-spark 420ms ease-out forwards; }

@keyframes seen-pop {
  0% { transform: scale(1); }
  35% { transform: scale(1.6); opacity: 1; }
  100% { transform: scale(0); opacity: 0; }
}
@keyframes seen-ring {
  0% { opacity: 1; transform: scale(.6); }
  100% { opacity: 0; transform: scale(2.2); }
}
@keyframes seen-spark {
  0% { opacity: 1; transform: rotate(var(--r)) translateY(0); }
  70% { opacity: 1; }
  100% { opacity: 0; transform: rotate(var(--r)) translateY(-16px) scale(.5); }
}
@keyframes seen-fade { to { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .seen-dot.is-popping .seen-dot__disc { animation: seen-fade 150ms linear forwards; }
  .seen-dot.is-popping .seen-dot__ring,
  .seen-dot.is-popping .seen-dot__spark { animation: none; }
}

/* A new row's title a weight heavier. Weight only: the line box is the same
 * 24px, so the row is the height a seen one is. */
[data-seen-key] .go-row-item__title,
[data-seen-key] .go-video__title,
[data-seen-key] .go-shelfcard__title { font-weight: var(--go-weight-extrabold); }

/* The tile head's «n». After the title, before the actions. */
.seen-count { flex: none; }

/* «Hoy en la mesa»'s second half, for a signed-in reader. */
.reader-news-summary {
  font-weight: var(--go-weight-bold);
  color: var(--go-color-ink);
  text-decoration: underline;
  text-decoration-color: var(--go-color-brand);
  text-decoration-thickness: 3px;
  text-underline-offset: 3px;
}

/* Its ×. A 24px control in a 20px line, top-aligned so its box starts on the
 * line's top and therefore on the grid (the alignment gate checks every
 * .go-btn); the 4px it hangs below come back off the bottom margin, so the
 * line box keeps the 20px the text sets. The glyph lands on the lowercase
 * letters' middle. No inline margin: the button's own padding is the gap. */
.reader-news-clear {
  vertical-align: top;
  margin-block-end: -4px;
}

/* ---------------------------------------------------------------------------
 * «Lo nuevo para ti» (reader_news/show).
 * ------------------------------------------------------------------------ */

/* The figure slot. Every row's figure sits in the top-left corner of one
 * fixed 64×48 box — the widest figure (a 64×36 video) by the tallest (a 36×48
 * box, a 48×48 date) — so every title on the page starts on one left edge and
 * every «new» dot lands on one point, whatever the row is. A candidate for the
 * design system as a `.go-row-item__figure` modifier; until then it lives
 * here, because stylesheets/gameon/ belongs to bin/sync-design-system. */
.reader-news-slot {
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  width: 64px;
  height: 48px;
}

/* A note with no video has no image: a placeholder in the shape its figure
 * would have had, so the dot still has a corner to sit on. */
.reader-news-noimg {
  display: grid;
  place-items: center;
  color: var(--go-color-ink-subtle);
}

/* Already seen — shown only with «Mostrar lo que ya viste», or a row whose
 * dot was just popped, which keeps its place until the next load so the list
 * never moves under the pointer. */
.reader-news-row.is-seen > :not(.seen-dot) { opacity: .55; }

.reader-news-when {
  font-family: var(--go-font-mono);
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs, 16px);
  color: var(--go-color-ink-subtle);
  white-space: nowrap;
}

.reader-news-day + .reader-news-day { margin-top: var(--go-space-6); }
.reader-news-day__head {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--go-space-2);
  margin: 0 0 var(--go-space-1);
  font-family: var(--go-font-mono);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs, 16px);
  font-weight: var(--go-weight-bold);
  letter-spacing: var(--go-tracking-wide);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
}
.reader-news-day__head > :first-child { color: var(--go-color-ink); }

/* --- the bar -------------------------------------------------------------
 * It wraps rather than scrolls: nothing on this page scrolls sideways at any
 * width. The bar is the containing block for a dropdown's panel on a phone
 * (below), so the panel takes the bar's width and can never run off the right
 * edge. */
.reader-news-toolbar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--go-space-2) var(--go-space-3);
  /* The rules are paid for out of the padding, so the controls inside sit on
   * the 4px grid rather than 2px off it. */
  padding-block: calc(var(--go-space-3) - var(--go-border-width));
  margin-bottom: var(--go-space-4);
  border-block: var(--go-border-width) solid var(--go-color-line-soft);
}
.reader-news-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--go-space-2);
}
.reader-news-hidden-link {
  display: inline-flex;
  align-items: center;
  gap: var(--go-space-1);
  margin-inline-start: auto;
  font-size: var(--go-text-sm);
  line-height: var(--go-leading-sm);
  font-weight: var(--go-weight-bold);
  color: var(--go-color-ink-muted);
}
.reader-news-hidden-link:hover { color: var(--go-color-ink); text-decoration: underline; }

/* The switch is a link (it redraws the page), so it borrows the system's
 * track and paints the «on» state from aria-checked instead of :checked. */
.reader-news-seen-switch { color: inherit; }
.reader-news-seen-switch[aria-checked="true"] .reader-news-seen-switch__track { background: var(--go-color-brand); }
.reader-news-seen-switch[aria-checked="true"] .reader-news-seen-switch__track::after { transform: translateX(16px); }

/* --- a dropdown ------------------------------------------------------------ */
.reader-news-dd { position: relative; }
.reader-news-dd > summary { list-style: none; gap: var(--go-space-1); }
.reader-news-dd > summary::-webkit-details-marker { display: none; }
/* Flipped, not turned: a chevron caught mid-rotation is a box wider than its
 * icon slot, and there is nothing to animate that a reader needs to see. */
.reader-news-dd[open] > summary .go-icon { transform: scaleY(-1); }

.reader-news-dd__panel {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(100% + var(--go-space-2));
  z-index: 20;
  width: 340px;
  /* The card's 2px border comes out of the padding, so the rows inside
   * start on the grid. */
  padding: calc(var(--go-space-2) - var(--go-border-width));
}
/* A closed <details> still lays its content out in Chromium (it only hides
 * it), so a closed panel is taken out of layout by hand. */
.reader-news-dd:not([open]) > .reader-news-dd__panel,
.reader-news-menu:not([open]) > .reader-news-menu__panel { display: none; }
@media (max-width: 560px) {
  .reader-news-dd { position: static; }
  /* Offsets start inside the bar's 2px rule; adding it back keeps the panel's
   * top on the grid. */
  .reader-news-dd__panel {
    inset-inline: 0;
    width: auto;
    inset-block-start: calc(100% - var(--go-space-2) + var(--go-border-width));
  }
}
.reader-news-dd__search { margin-bottom: var(--go-space-2); }
/* Every step inside the panel is a multiple of 4px: rows with no gap between
 * them, and each 1px rule paid for out of the padding beside it. */
.reader-news-dd__list {
  display: grid;
  gap: 0;
  max-height: 340px;
  overflow-y: auto;
  overflow-x: hidden;
}
.reader-news-dd__list .go-choice {
  min-width: 0;
  padding-inline: var(--go-space-2);
  border-radius: var(--go-radius-sm);
}
.reader-news-dd__list .go-choice:hover { background: var(--go-sand-200); }
.reader-news-dd__list .go-choice__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reader-news-dd__group {
  margin-top: var(--go-space-1);
  padding-top: calc(var(--go-space-1) - 1px);
  border-top: 1px solid var(--go-color-line-soft);
}
.reader-news-dd__group-label { font-weight: var(--go-weight-bold); }
.reader-news-dd__sub { display: grid; gap: 0; padding-inline-start: var(--go-space-6); }
.reader-news-dd__source { display: flex; align-items: center; gap: var(--go-space-1); min-width: 0; }
.reader-news-dd__source > .go-choice { flex: 1 1 auto; }
.reader-news-dd__source > .go-btn { flex: none; }
.reader-news-dd__dot {
  flex: none;
  width: 10px;
  height: 10px;
  border: 1.5px solid var(--go-color-line);
  border-radius: 50%;
  background: var(--go-cat);
}
.reader-news-dd__foot {
  display: flex;
  justify-content: space-between;
  gap: var(--go-space-2);
  margin-top: var(--go-space-2);
  padding-top: calc(var(--go-space-2) - 1px);
  border-top: 1px solid var(--go-color-line-soft);
}

/* --- a row's ⋯ menu ------------------------------------------------------- */
.reader-news-menu { position: relative; }
.reader-news-menu > summary { list-style: none; }
.reader-news-menu > summary::-webkit-details-marker { display: none; }
.reader-news-menu__panel {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(100% + var(--go-space-1));
  z-index: 20;
  width: max-content;
  max-width: min(320px, calc(100vw - var(--go-space-8)));
  padding: calc(var(--go-space-2) - var(--go-border-width));
}
.reader-news-menu__form { display: block; }
/* One line per action, however long a shop's or a group's name is: the rail
 * item is a fixed 40px, and a wrapped label spills out of it. */
.reader-news-menu__panel .go-railitem__label { white-space: nowrap; }
.reader-news-menu__panel .go-railitem {
  width: 100%;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--go-text-sm);
  cursor: pointer;
  text-align: start;
}

/* «No me interesa» on a ficha: `button_to`'s form is a block, and a block
 * around an inline button is a line box a pixel taller than the button. */
.game-ignore { display: flex; }

/* ==========================================================================
 * A video's page: the frame in its two-thirds column
 * --------------------------------------------------------------------------
 * `.go-aspect--h-xl` is 360px, which was sized for a half-width tile. Beside a
 * rail the player's column is two thirds of the page, about 790px inside the
 * tile at 1280, and 360 of that is a 2.2:1 letterbox of a 16:9 thumbnail.
 * 440 is the 16:9 of that width, rounded to the 4px grid — fixed and not a
 * ratio for the reason the system gives: the summary and the button follow it
 * in the same column, and a fluid height would put them off the grid.
 *
 * Only above 1120px, where the column IS two thirds. Below it the bento halves
 * every column and the system's own 360/240 are right again.
 *
 * Keyed on the player's target and not on the facade, because the controller
 * replaces the facade with an iframe box it builds itself
 * (video_player_controller.js#mount), and the two must be one height or the
 * page jumps under the reader's pointer on the click that starts the video.
 * ========================================================================== */
@media (min-width: 1121px) {
  .video-page__frame > .go-aspect--h-xl { height: 440px; }
}

/* ==========================================================================
 * A list video's games: the band that closes the main column (videos/_game_band)
 * --------------------------------------------------------------------------
 * The rows are a grid read left to right, so the rules of neighbouring columns
 * share a line. `.go-list` draws a rule above every row but the first, which in
 * a grid leaves the first column bare while the second opens on one;
 * so the first row takes the same rule and pays for it out of its padding, the
 * way `.go-list > * + .go-list__item` pays for every other one. The same rule
 * opens the rows behind «Ver los N juegos», which is what makes them continue
 * the list rather than start a second one.
 *
 * The rank sits in the figure slot beside the cover (see _game_band_rows), so
 * that slot lays the two side by side. `--go-index-w` holds two digits at the
 * index's own type size; the longest list video has named 76 games.
 * ========================================================================== */
.video-games__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
  column-gap: var(--go-space-8);
}
/* A line of the grid is as tall as its tallest name, and every `<li>` in it
 * stretches to that height — but the row inside is only as tall as its own
 * content, so a short name sat at the top of its cell while the wrapped one
 * beside it was centred in its row, 8px apart. Filling the cell makes each row
 * centre in the same height. */
.video-games__list > .go-list__item > .go-row-item { height: 100%; }
.video-games__list > .go-list__item:first-child {
  border-top: var(--go-border-width) solid var(--go-color-line-soft);
}
.video-games__list > .go-list__item:first-child > .go-row-item {
  padding-top: calc(var(--go-space-2) - var(--go-border-width));
  min-height: calc(var(--go-space-12) - var(--go-border-width));
}
.video-games__list .go-row-item__figure {
  display: flex;
  align-items: center;
  gap: var(--go-space-2);
  --go-index-w: 24px;
}
.video-games__list .go-row-item__index { color: var(--go-color-ink-muted); }
/* A verdict quoted whole («recomendado por los tres») yields; a figure never
 * gets near this. */
.video-games__list .go-row-item__aside { max-width: 40%; }

.video-games__more > summary { list-style: none; cursor: pointer; }
.video-games__more > summary::-webkit-details-marker { display: none; }
.video-games__more:not([open]) { padding-top: var(--go-space-3); }
.video-games__more[open] > summary { display: none; }

/* ==========================================================================
 * A game's ficha: the cover at the top of its rail
 * --------------------------------------------------------------------------
 * The ficha's facts moved into a third-of-the-page rail, and a 60×80 thumb in
 * a 370px tile is a stamp in a corner. The system has no cover larger than
 * `.go-thumb--lg`, so the size is app-level, the way `.market-card__visual`
 * does it: the same element, the same border and sand ground, only bigger.
 *
 * `contain`, not the thumb's `cover`: box art carries the game's title, and a
 * crop to the tile's shape is what would cut it. 240px is on the 4px grid,
 * because the deflist follows it in the same stack.
 * ========================================================================== */
.game-facts__cover .go-thumb {
  width: 100%;
  height: 240px;
  object-fit: contain;
}

/* The rail's BGA chit (games/show) is the first WORDS the site sets on BGA
 * purple: the system's one category ink is dark, which is 3.35:1 there — short
 * of AA for 11px text — while the home tile's purple buttons are icon-only and
 * need just 3:1. The light ink is about 5.4:1, and `.go-btn--cat:hover` mixes
 * away from the ink, so a light ink darkens the hover and gains contrast too.
 * One chit and not `.go-cat-bga`, which would repaint every BGA surface: that
 * is a design-system decision (the home panels already carry a per-panel ink). */
.game-facts__bga { --go-cat-ink: var(--go-color-ink-invert); }

/* Its BGG neighbour wears BGG's own orange, which the palette's coral is within
 * a shade of. The system's dark ink stays: about 5.9:1 on it, AA at 11px. */
.game-facts__bgg { --go-cat: var(--go-coral-500); }

/* ==========================================================================
 * A game's ficha: «Calificaciones»
 * --------------------------------------------------------------------------
 * A strip of figures — one cell per score — above the reviews they came from
 * (games/_ratings). `.go-stats` is the system's own figure strip, but its
 * dividers are a right border per cell: a strip that wraps (four scores at
 * 390px) loses the rule between its rows and keeps a stray one at each row's
 * end. So the cells here draw their divider as a hairline SHADOW on their
 * right and bottom edges, and the strip clips whatever falls outside it: the
 * last column's and the last row's rules vanish at the frame, every rule
 * between two cells stays, and a short last row leaves blank surface, not
 * a grey slab where a cell would be.
 *
 * Every line box is a token pair on the 4px grid, so a cell's height is the
 * sum of its lines and two cells in one grid row stretch to the same height.
 * Reviewers and seals are source text, including names with no word breaks.
 * ========================================================================== */
.rating-scores {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  border: var(--go-border);
  border-radius: var(--go-radius-lg);
  background: var(--go-color-surface);
  overflow: hidden;
}

.rating-score {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: var(--go-space-3) var(--go-space-4);
  box-shadow:
    var(--go-border-width-hair) 0 0 var(--go-color-line-soft),
    0 var(--go-border-width-hair) 0 var(--go-color-line-soft);
}

.rating-score__who {
  font-size: var(--go-text-2xs);
  line-height: var(--go-leading-2xs);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-caps);
  text-transform: uppercase;
  color: var(--go-color-ink-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rating-score__figure {
  margin-top: var(--go-space-1);
  font-size: var(--go-text-3xl);
  line-height: var(--go-leading-3xl);
  font-weight: var(--go-weight-extrabold);
  letter-spacing: var(--go-tracking-snug);
}

.rating-score__scale {
  margin-inline-start: 2px;
  font-size: var(--go-text-md);
  /* Its own 36px line box, on its own baseline, would push the figure's to 41. */
  line-height: 1;
  font-weight: var(--go-weight-bold);
  letter-spacing: 0;
  color: var(--go-color-ink-subtle);
}

/* A word, not a number: two sizes down so «Recomendado» fits the 128px a
 * 160px cell leaves. One line of it is 8 + 24 + 4 = 36px, the figure's line
 * box, so a verdict and a score beside it start level. A long verdict wraps between words, and
 * breaks inside one only when that word cannot fit a line on its own. */
.rating-score__verdict {
  margin-top: var(--go-space-1);
  padding-block: var(--go-space-2) var(--go-space-1);
  font-size: var(--go-text-lg);
  line-height: var(--go-leading-lg);
  font-weight: var(--go-weight-extrabold);
  overflow-wrap: break-word;
}
.rating-score__verdict::first-letter { text-transform: uppercase; }

.rating-score__seal {
  display: flex;
  align-items: center;
  gap: var(--go-space-1);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  font-weight: var(--go-weight-bold);
  color: var(--go-color-ink);
  overflow-wrap: anywhere;
}
.rating-score__seal .go-hex { --go-cat: var(--go-color-brand); --go-hex-size: 12px; }

.rating-score__from {
  margin-top: auto;
  padding-top: var(--go-space-1);
  font-size: var(--go-text-xs);
  line-height: var(--go-leading-xs);
  color: var(--go-color-ink-subtle);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rating-review__avatar { overflow: hidden; }
.rating-review__avatar img { width: 100%; height: 100%; object-fit: cover; }

#calificaciones .go-row-item__body {
  overflow-wrap: anywhere;
}
.rating-review__summary { margin-top: var(--go-space-1); }
