/* screens.css — page/screen-specific styling that doesn't belong in a shared component. */

/* --- Dashboard: each section is a card, two-up from tablet width up ----- */
.dash-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
}

@media (min-width: 60rem) {
  .dash-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Cards that opt out of the two-up and take the whole row.
   Two-up is right for the five cards that hold short <ul> lists, and wrong
   for the two above them. "My games" holds the only table on the dashboard:
   its six columns need 800px, half the grid gave it 518, and .table-wrap
   scrolled the difference sideways while every date wrapped onto three
   lines. "My team" holds two lines of text and was stretched to the games
   table's height beside it -- 431px of card around 119px of content, which
   is the empty half of the screenshot this came from.
   Full width: the table needs 1086 and gets 1086, and the team card sizes to
   its content. The remaining five cards still pair off underneath. */
.dash-grid > .card--wide { grid-column: 1 / -1; }

/* --- Dashboard: accepted teams, grouped per game -----------------------
 * Each game is a panel of its own inside the card. Before this, .team-group
 * was a bare <div> with no rule anywhere, so a game's name and its team names
 * stacked with nothing between them and two games ran together into one list.
 *
 * auto-fit rather than a fixed two columns: the number of games is whatever
 * the author has, and auto-fit gives one game the full width, two a half
 * each, and four a quarter, without a breakpoint per count. The 16rem floor
 * is what stops a long team name from being squeezed into a column narrower
 * than it reads in.
 *
 * Single column below 48rem, matching where .table--cards gives up on
 * columns: side-by-side panels on a phone would be 160px wide apiece. */
.team-groups {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

@media (min-width: 48rem) {
  .team-groups { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }
}

/* --surface-2 on a --panel inside a --surface card, the same nesting as
   .team-panel on the operator screen. tokens.css already records the contrast
   checks against this surface. */
.team-group { background: var(--surface-2); }

/* The game's name is the panel's heading, so it needs to sit off its list
   rather than look like the first row of it. */
.team-group h3 { margin-bottom: var(--space-2); }

/* --- Team room: the controls that act on the team ----------------------
 * A band under the roster card, not part of it. See the comment in
 * team_room/index.html.erb for what these used to be tangled up with. */
.team-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

/* Edge to edge across the card's width: with two controls one sits at each
   end, which is what stops the band reading as a continuation of the member
   list above it. Wraps rather than overflowing when a phone cannot fit them
   side by side. */
.team-actions__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
}

/* button_to renders a <form> around its button. Without this, the form's
   default block margin opens a gap in the middle of the row. */
.team-actions__row > form { margin: 0; }

.team-actions__handover {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* Undoes the global `select { width: 100% }` in components.css, which is
   meant for stacked form fields. Here the select sits beside the button that
   submits it, and at 100% it spanned the whole card and pushed that button
   onto a line of its own -- the reported layout. min-width so a two-letter
   nickname still leaves a comfortable target. */
.team-actions__handover select {
  width: auto;
  min-width: 12rem;
}

/* A game row inside a card's list (dashboard "my games", games/index) --
   base.css strips the ul/li default spacing, so give rows back a readable
   rhythm without touching the markup's semantics. */
.game-list li {
  padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border);
}
.game-list li:last-child { border-bottom: 0; }

/* The second figure in the games list's "Команды" cell -- who actually
   started, under the registrations-against-the-cap figure it must not be
   confused with. Dimmed and smaller for the same reason .notice is: it is
   context for the number above it, not a number of equal standing. */
.participation {
  color: var(--text-dim);
  font-size: var(--text-sm);
}

/* --- Admin: the stats screen's figure grid ------------------------------
 * Each count is a labelled figure rather than a table row. */
.stat-grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}

.stat {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
}

.stat-value { font-size: var(--text-h1); font-weight: 650; font-variant-numeric: tabular-nums; }

.stat-label {
  color: var(--text-dim);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* --- Authoring: language tabs, translation gaps, draft/notice banners ----
 * These class names (language-tabs, missing-translations) are asserted on
 * literally by spec/requests/language_tabs_spec.rb and
 * spec/requests/publish_gate_spec.rb -- the markup that carries them
 * (app/views/shared/_language_tabs.html.erb, games/_missing_translations
 * .html.erb) is untouched by Task 8; only the CSS backing them, previously
 * master.css-only, moves here.
 */
ul.language-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

ul.language-tabs li a,
ul.language-tabs li .current {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: var(--surface-2);
  color: var(--text-dim);
}

ul.language-tabs li a:hover { color: var(--text); }

/* The tab currently being edited -- rendered as text, not a link (see the
   partial's own comment on why). */
ul.language-tabs li .current {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
}

/* A locale with fields still to fill in. */
ul.language-tabs li.incomplete a { color: var(--danger); border-color: var(--danger); }

ul.language-tabs .missing-count {
  margin-left: var(--space-1);
  font-size: var(--text-xs);
  opacity: 0.85;
}

.missing-translations {
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--danger);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--danger) 8%, transparent);
}
.missing-translations h3 { margin-bottom: var(--space-2); color: var(--danger); }
.missing-translations ul { margin-left: var(--space-4); list-style: disc; }

/* The AI-translation panel on the game edit screen (superadmin only). It
   shipped with no rule at all, so a label followed by a button_to -- which
   renders its own <form> -- laid out as a staircase: each row's button started
   wherever that row's locale name happened to end. Measured before this rule,
   at both 390px and 1280px: six buttons, six different left offsets.

   Alignment across rows needs one shared column definition: a grid on each <li>
   on its own cannot do it, because no li knows how wide its neighbours' labels
   are. Two tools can, and this uses SUBGRID rather than `display: contents`.

   Why not display: contents, which is the shorter spelling: it removes the <li>
   from the box tree, and Chrome, Safari and Firefox all used to drop the list's
   accessibility semantics along with it. That was fixed years ago, but it could
   not be VERIFIED here -- chrome-headless-shell exposes no computedRole and
   supports no --dump-accessibility-tree, so the measurement in
   spec/layout/translate_panel_layout_spec.rb could report nothing better than
   "unknown". Subgrid makes the question moot: every <li> stays a real, rendered
   list item and simply borrows the parent's column tracks. Same alignment, one
   fewer thing taken on trust.

   max-content, not a fixed width: the label column has to fit the widest
   endonym, and which one that is depends on the registered locale set and on
   the font. Беларуская and ქართული are the long ones today; a hard-coded rem
   value would silently clip or over-pad the day an eighth locale is added. */
.translate-locales {
  display: grid;
  grid-template-columns: max-content max-content;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}

.translate-locales li {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: center;
}

/* button_to renders a <form>, and a form carries default margin in some UA
   stylesheets. As a grid item that margin becomes visible column padding. */
.translate-locales form { margin: 0; }

.translate-panel { margin-top: var(--space-5); }
.translate-panel h3 { margin-bottom: var(--space-2); }

.is-draft-message {
  display: inline-block;
  margin-bottom: var(--space-4);
  color: var(--danger);
  font-weight: 650;
}

/* Informational asides: "this content isn't translated yet" on the play
   screen, "answers are shared across locales" on the question form. Neither
   is an error -- .flash's go/danger treatment would overstate them. */
.notice, .hint-text {
  color: var(--text-dim);
  font-size: var(--text-sm);
}

/* In-game locale switcher (shared/_content_language_switcher.html.erb) --
   distinct from the platform chrome switcher in the header (#locale-switcher). */
.content-language-switcher { margin-bottom: var(--space-4); }
.language-choice {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--space-3);
  margin-right: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.language-choice:hover { border-color: var(--text-dim); }

/* The author's delete action sits apart from the benign controls above it,
   so it is never the button next to the one meant to be pressed. */
.danger-row { margin-top: var(--space-4); }

/* --- Play screen: the answer bar ---------------------------------------
 * The countdown to the next hint, and the controls that submit an answer.
 * Sticky to the bottom of the viewport, so the distance from "stuck" to
 * "trying again" never grows as task text and hints accumulate above.
 *
 * THE HISTORY MATTERS, because two of the three designs this file has
 * carried were reactions to a constraint that no longer exists.
 *
 * 1. Fixed. Out of flow, so the room it needs had to be reserved by hand
 *    somewhere else -- a constant measured against a bar holding a code
 *    field (222px, 255px paused). A quiz bar carried a whole fieldset of
 *    options, measured 390px, and buried the captain's "Сойти с дистанции"
 *    button under the reserve, unreachable at any scroll position.
 * 2. A row of a viewport-height grid. The shell became exactly one screen
 *    (`height: 100dvh`) and every region negotiated against every other, so
 *    the bar could no longer impose its height on the task area. That held
 *    only while every region could be squeezed to a floor and scrolled
 *    inside itself. Attachments ended it: a photograph has a floor of its
 *    own. Measured at 390x680 with one photo on the task and one on a
 *    hint -- task area 170px around 479px of content, option list 76px
 *    around 228px, 0 of 4 answers tappable without scrolling a third nested
 *    scrollport. Nothing in that arrangement was over-generous; there was
 *    simply not enough screen, and the design had no way to say so.
 * 3. This one. The page scrolls like any other page, everything that grows
 *    with the author's content grows, and the bar is a bounded strip that
 *    sticks to the bottom. What made 1 and 2 necessary was that content
 *    could be buried under something out of flow -- sticky holds its own
 *    space in flow, so nothing is ever underneath it, and there is no
 *    position at which any control is unreachable.
 *
 * The option list is NOT in here any more (it is in .play-body, with the
 * question). That is what makes the bar bounded: nothing in it grows with
 * what an author wrote, so it needs no cap, no floor and no scrollport --
 * every one of which this file used to carry, and each of which existed to
 * ration a fixed-height column that is gone.
 */
.playbar {
  position: sticky;
  bottom: 0;
  /* Cancels .main's bottom padding, which sits INSIDE the sticky element's
     containing block. A sticky box stops at the end of its containing block,
     so at maximum scroll the bar came to rest 23px above the viewport's bottom
     edge with a strip of page background showing beneath it -- measured at
     390x680, bar top 554 + height 103 against a 680 viewport. The bar carries
     its own padding (including the iOS home-indicator inset below), so it
     needs none from .main. */
  margin-bottom: calc(-1 * var(--space-5));
  z-index: var(--z-sticky);
  padding: var(--space-3) var(--space-4);
  /* Keeps the bar clear of the iOS home indicator. */
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  /* In flow the bar is only as wide as .main's content box; these cancel
     .main's own horizontal padding so it still reaches both edges, the way
     the full-bleed fixed bar did. */
  margin-left: calc(-1 * var(--space-4));
  margin-right: calc(-1 * var(--space-4));
  background: var(--surface);
  border-top: 1px solid var(--border);
  /* No max-height, no min-height, no overflow. All three used to live here
     and all three were rationing devices for a fixed-height column: a cap so
     the bar could not starve the task area, a floor so the column could not
     collapse the bar past its own submit button, and a scrollport so the
     clipped remainder stayed reachable. With the option list moved out, the
     bar's tallest real state is the countdown, a code field and a button --
     bounded by the design rather than by a number, which is the only kind of
     bound that has held. Re-measure before adding anything that grows with
     author content; if something here ever can, it belongs in .play-body. */
}

/* Everything that grows with what an author wrote: task text, its
   photographs, every hint that has fired, and the answer options. It has no
   height of its own and no scrollport -- it is as tall as its content and
   the PAGE scrolls, which is the whole of this design.
   Was .play-scroll, a flex child with a 4rem floor and overflow-y: auto.
   Renamed as well as rewritten on purpose: a region that scrolls is a
   different thing from a region that does not, and leaving the old name on
   the new behaviour would have made every comment that cites it wrong. */
/* base.css's rhythm is scoped to DIRECT children of .main, so the blocks that
   live inside this wrapper would otherwise lose their spacing. */
.play-body > * + * { margin-top: var(--space-4); }

/* The captain's way out, last in the document. A row rather than a stack:
   the two forms are DIRECT children, because the <p> that used to wrap them
   in the template never existed at runtime -- a <form> is flow content and
   closes an open <p>, so the browser produced two empty paragraphs with the
   forms as siblings, and every rule ever written against `.play-pinned p`
   silently matched nothing. The template emits a <div> now; this selector
   would work either way, which is the point of writing it this way. */
.play-exit {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.play-exit > form { flex: 1 1 9rem; }
.play-exit .btn { width: 100%; }

/* --- Attachment strip (play screen) --------------------------------------
 * shared/_attachment_strip.html.erb, rendered once inside the level's card
 * and once inside each fired hint's fieldset -- both live inside
 * .play-body. An image displays at the "web" ActiveStorage variant, which
 * is resized to at most 1600x1600 (GameFile#web_variant) -- nowhere near
 * bounded for a 390px viewport (this project measures layout there, see the
 * .playbar comment above), so it needs its own cap rather than inheriting
 * one. Fixed box regardless of the source photo's aspect ratio, same
 * approach as .file-table .file-thumb-image in components.css; object-fit
 * crops rather than distorting. */
.attachment-strip {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.attachment-item { display: block; }

.attachment-image {
  display: block;
  width: 96px;
  height: 96px;
  max-width: 96px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

/* A PDF, a GIF (never resized -- see the comment in the partial), or an
   image with no existing variant record: same generic indicator, so a
   missing thumbnail never reads as a broken <img>. */
.attachment-item--generic {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 96px;
  height: 96px;
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: var(--text-xs);
  overflow: hidden;
  text-align: left;
}

.attachment-generic-icon { font-size: var(--text-lg); line-height: 1; flex: 0 0 auto; }

.attachment-generic-filename {
  overflow-wrap: anywhere;
  /* Clamped rather than left to grow the box -- a long author-chosen
     filename must not stretch the strip past the fixed-size image tiles
     beside it. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* From 52rem up the shell is a two-column grid, and the bar is a panel beside
   the task rather than a strip beneath it. */
@media (min-width: 52rem) {
  .playbar {
    /* Nothing to stick to in a column that is not the page's scroll axis, and
       a bar stuck to the bottom of a desktop window is nowhere near the
       content it belongs to. */
    position: static;
    /* The negative margins above cancel .main's padding so the bar reaches
       both edges of a phone screen. In the right-hand column it is a panel
       beside the task, not a bar under it, so it takes an edge of its own
       instead of bleeding into the gutter. The bottom one goes too: it exists
       to close the gap a sticky box leaves at the end of its containing block,
       and nothing is sticky here. */
    margin-left: 0;
    margin-right: 0;
    margin-bottom: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
  }
}

/* The bar used to carry a clamped copy of the newest hint, so a stuck team did
   not have to scroll a fixed-height task area to re-read it. It cost 86px of
   the most contested space on the screen -- measured -- to duplicate something
   that was already on the page, and the reason it could not simply be scrolled
   to was the fixed-height column. The page scrolls now, so the copy is gone
   along with .playbar-hint, .playbar-hint-label and .playbar-hint-text, and
   with the block in level_hint_updater.js that wrote into it. */

.playbar .countdown { display: block; margin-bottom: var(--space-2); }

.playbar-form { display: flex; gap: var(--space-2); align-items: center; }
.playbar-form input[type="text"] { flex: 1; }
/* Labels have no other styling in this system; this is what marks the code
   field's label as a label rather than plain text in the bar. */
.playbar-form label { font-weight: 600; }

/* On a pure quiz level the bar's form holds the submit button and nothing
   else, so there is no code field for the row to give its width to and the
   button would sit at its text width in a bar the width of the screen. The
   old quiz bar stretched it; keep that. Written as a :has() test for "no code
   field" rather than a class on the form, so it stays correct on a MIXED level
   -- where a code field IS present and the button must not take the room the
   field needs. Browsers without :has() (pre-Safari 15.4) simply get a
   natural-width button: narrower than intended, still a full tap target,
   nothing broken. */
.playbar-form:not(:has(input[type="text"])) .btn--go { flex: 1; }

/* Two rules used to live here and both are gone with the option list.
   .playbar-form--quiz stacked the bar's contents and gave the form an 8rem
   floor -- one option plus the submit button. That floor was the reason the
   option list was permanently 76px: the form never grew (flex-grow: 0) and
   never shrank, so "one option's worth" was not a minimum, it was the size.
   .playbar-form--quiz .btn--go was sticky, to keep the submit button on the
   bar's visible edge once the bar was scrolling past it. The bar no longer
   scrolls, and the bar itself is the sticky thing now. */

.quiz-question {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.quiz-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
}

/* Control first, sized as a real tap target -- not the browser default
   ~13px box -- and flex: none so the label text next to it cannot shrink it. */
.quiz-option input[type="radio"],
.quiz-option input[type="checkbox"] {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
}

.playbar-paused {
  color: var(--time);
  font-weight: 600;
  text-align: center;
}

/* Information, not an action -- same treatment as the hint countdown it sits
   next to, not the go/danger buttons either could be mistaken for. */
.playbar-penalty {
  margin-bottom: var(--space-2);
  color: var(--time);
  font-weight: 600;
}

/* --- Operator screen: live stats + the per-team intervention panel ------
 * The stats table stays a real <table id="stats"> (see .table--cards in
 * components.css for the responsive behaviour); each row's controls live
 * behind a <details> disclosure rather than inline, so nothing is armed
 * until the operator deliberately opens that team's panel.
 */
.team-panel {
  margin-top: var(--space-3);
  background: var(--surface-2);
}

.team-panel-actions { display: grid; gap: var(--space-2); }

/* Danger sits apart from the rest, so "return to the game" is never the
   button next to the one you meant to press. */
.team-panel-actions .btn--danger { margin-top: var(--space-3); }

.game-control {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  margin-bottom: var(--space-4);
}

/* .btn sets display:inline-flex, which suppresses the native <summary>
   marker (browsers only draw it at display:list-item). Supply our own, so a
   control that expands looks like one -- this screen is read at a glance, at
   night, at 390px. */
.team-disclosure::after {
  content: "▾";
  margin-left: var(--space-2);
}

details[open] > .team-disclosure::after { content: "▴"; }

/* Safari draws its own marker through a pseudo-element that display alone
   does not suppress. */
.team-disclosure::-webkit-details-marker { display: none; }

/* Rails' button_to renders a <form class="button_to"> around its submit
   button (actionview .../url_helper.rb), and <form> is block-level by
   default, so every button_to control -- the ↑/↓ level-order arrows, the
   dashboard game-entry accept/reject/recall/decline/reapply/apply controls,
   and others -- stacks onto its own line instead of sitting inline with its
   neighbors. This is not dead weight: it is the fix for every one of those
   sites at once. Do not remove it as unused just because no selector in this
   file matches ".button_to" by name -- Rails emits the class at render time,
   not anywhere grep can find it in app/views. */
form.button_to { display: inline-block; }

/* --- The manual: one long markdown document ------------------------------
   Authored as markdown, so the widest element on the page is a table nobody
   sized for a phone: six of them, three columns, some rows 135 characters of
   source wide.
   `display: block` is what makes overflow-x apply to a <table> at all -- the
   table becomes its own scrollport, so the TABLE scrolls sideways and the page
   never does. spec/layout/manual_layout_spec.rb asserts exactly that. */
.manual table {
  display: block;
  overflow-x: auto;
  max-width: 100%;
}

/* Same reasoning for code blocks: the deployment guide has 16 of them and some
   lines are long. */
.manual pre {
  overflow-x: auto;
  max-width: 100%;
}

/* A measure, not a width: unbroken prose across 1280px is unreadable, and
   nothing else on this page constrains it. */
.manual {
  max-width: 70ch;
}

/* Vertical rhythm. base.css:64's `.main > * + *` only reaches DIRECT children
   of .main -- every other view yields its blocks straight into .main, so it
   has never had to reach further. This is the first view to wrap its content
   in an element of its own (`<div class="manual">`), which puts every one of
   the ~211 rendered blocks one level too deep for that rule to see. Nothing
   else restores the spacing base.css:4-7 zeroed on h1-h4/p/ul/ol, or the
   `list-style: none` base.css:55 puts on every <ul> -- so without this, the
   manual is 43 headings flush against their neighbors, bullet lists
   indistinguishable from prose, and a 16 076px-tall document. Neither suite
   can see this: Capybara's rack_test driver parses no stylesheet, and a
   request spec sees markup only. spec/layout/manual_layout_spec.rb has an
   example that measures computed spacing for exactly that reason. */
.manual > * + * {
  margin-top: var(--space-4);
}
.manual h2, .manual h3 {
  margin-top: var(--space-5);
}
.manual ul {
  list-style: disc;
  padding-left: var(--space-5);
}
.manual ol {
  padding-left: var(--space-5);
}
.manual li + li {
  margin-top: var(--space-2);
}

/* --- Translation review (translation_proposals/index) --------------------
 * A superadmin reviewing a language they may not read: the flags are the
 * safety story (Translation::Flags), so a flagged proposal must be visible
 * before any text is read -- by shape as well as colour, per the palette's
 * standing rule. The table stacks into cards below 48rem (.table--cards). */
table.proposals td { vertical-align: top; }
table.proposals td.locale,
table.proposals td.field { color: var(--text-dim); font-size: var(--text-sm); }
table.proposals td.locale { white-space: nowrap; }
/* A quiz option's label embeds the author's option text, which is unbounded. */
table.proposals td.field { white-space: normal; overflow-wrap: anywhere; }
table.proposals td.source {
  width: 40%;
  background: var(--surface-2);
  border-radius: var(--radius-sm);
  /* The source's line breaks are content the translation must keep. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
/* Line breaks in the proposed and accepted wording are content. .proposed holds
   only its text, so pre-wrap sits on the <p>; .accepted also holds the "edited"
   label and the template's own indentation, so pre-wrap sits on the text's
   span and the <p> collapses whitespace as usual. */
table.proposals .proposed,
table.proposals .accepted-text { white-space: pre-wrap; }
table.proposals .proposed,
table.proposals .accepted { overflow-wrap: anywhere; }
table.proposals tr.flagged > td:first-child { box-shadow: inset 3px 0 0 var(--danger); }
table.proposals ul.flags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin: var(--space-2) 0;
  list-style: none;
}
table.proposals .accepted strong { color: var(--text-dim); font-size: var(--text-sm); }

@media (max-width: 47.99rem) {
  table.proposals td.source { width: auto; }
  table.proposals td.locale,
  table.proposals td.field { white-space: normal; }
  /* In the stacked layout the card itself carries the edge. */
  table.proposals tr.flagged { border-left: 3px solid var(--danger); }
  table.proposals tr.flagged > td:first-child { box-shadow: none; }
}

/* --- Home page (index/index) ---------------------------------------------
 * Players first: hero, upcoming games (card + compact rows), how-to-play
 * timeline, organizers panel. One column, capped for reading on desktop.
 * Design: Superdesign drafts 7eaa33ad (guest) and 659ef6ac (signed-in). */
.home { max-width: 44rem; margin-inline: auto; }
.home > * + * { margin-top: var(--space-6); }
.home-hero > p { margin-top: var(--space-3); color: var(--text-dim); }
.home-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); margin-top: var(--space-5); }
.home-actions .btn { flex: 1 1 12rem; }
.home-section-heading { font-size: var(--text-h2); line-height: 1.25; font-weight: 600; }
/* base.css zeroes block margins, so each section spaces its own children. */
.home-upcoming > * + *, #how-to-play > * + *, .home-organizers > * + * { margin-top: var(--space-4); }
.home .link-tap { display: inline-flex; align-items: center; min-height: var(--tap); }
.home-empty > * + * { margin-top: var(--space-2); }
/* Signed in, the card title is an h2 under the section's h1; base.css would
   size it like that heading. It belongs to the card, so it reads smaller. */
.home-empty :is(h2, h3) { font-size: var(--text-lg); }
.home-note { font-size: var(--text-sm); color: var(--text-dim); }
.home .tag { white-space: nowrap; flex-shrink: 0; }

.next-game :is(h2, h3) { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.next-game .when { margin-top: var(--space-2); font-size: var(--text-lg); font-weight: 600; }
.next-game .meta { font-size: var(--text-sm); color: var(--text-dim); }
.next-game-action:not(:empty) { margin-top: var(--space-4); }

.game-rows { list-style: none; }
.game-row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3);
            padding: var(--space-1) 0; border-bottom: 1px solid var(--border); }
.game-row > div { min-width: 0; }
.game-row .name { font-weight: 600; overflow-wrap: anywhere; }
.game-row .name, .next-game :is(h2, h3) a { display: inline-flex; align-items: center; min-height: var(--tap); }
.next-game :is(h2, h3) a { overflow-wrap: anywhere; min-width: 0; max-width: 100%; }
.game-row .when { font-size: var(--text-sm); color: var(--text-dim); }

.timeline { list-style: none; display: flex; flex-direction: column; gap: var(--space-4); }
.timeline li { position: relative; padding-left: calc(1.5rem + var(--space-3)); }
.timeline li:not(:last-child)::before { content: ""; position: absolute; left: calc(0.75rem - 0.5px);
            top: 1.5rem; bottom: calc(var(--space-4) * -1); width: 1px; background: var(--border-input); }
.timeline .num { position: absolute; left: 0; top: 0; width: 1.5rem; height: 1.5rem; border: 1px solid var(--go);
            border-radius: 50%; display: flex; align-items: center; justify-content: center;
            font-size: var(--text-sm); font-weight: 600; color: var(--go); background: var(--bg); }
.timeline h3 { font-size: var(--text-lg); }
.timeline p { margin-top: var(--space-1); }

.home-organizers ul { list-style: disc; padding-left: var(--space-4); }
.home-organizers li { font-size: var(--text-sm); }

/* A long unbroken team name wraps in its cell at every width. */
.standings .standings-team { overflow-wrap: anywhere; }

/* --- Operator standings on a phone (game_passings/index) -------------------
 * .ops wraps the table, the full-log link and the control bar. The bar is
 * last in the markup (so it can stick to the bottom on a phone) and is moved
 * back to the top from 48rem with flex order, so desktop does not change.
 * gap, not `> * + *` margins: with order changed, a sibling margin would
 * land on the wrong element. */
.ops { display: flex; flex-direction: column; gap: var(--space-4); }
.live-status:not([hidden]) { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
               font-size: var(--text-sm); color: var(--text-dim); }

.opbar .game-control { margin: 0; }
/* Visual order on desktop: bar, level-codes fieldset, table (as before the
   rework). Markup order is table, link, bar, fieldset so the bar can stick
   to the bottom on phones; phones put the fieldset before the bar. */
.ops > .ops-codes { order: 1; }
.ops > .opbar { order: 2; }

@media (min-width: 48rem) {
  .ops > .opbar { order: -2; }
  .ops > .ops-codes { order: -1; }
  .opbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }
}

@media (max-width: 47.99rem) {
  /* The play screen's .playbar treatment (see its comment above): sticky to
     the bottom, full bleed, clear of the iOS home indicator. .ops is the last
     thing in .main, so its negative bottom margin cancels .main's padding and
     the bar can rest on the viewport's bottom edge at maximum scroll. */
  .ops { margin-bottom: calc(-1 * var(--space-5)); }
  .opbar {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    margin-left: calc(-1 * var(--space-4));
    margin-right: calc(-1 * var(--space-4));
    padding: var(--space-2) var(--space-4);
    padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
    background: var(--surface);
    border-top: 1px solid var(--border);
  }

  /* One team = two lines: name, then level · time (inline cells). The row's
     right padding reserves the panel button's column (tap + 2 * space-2) for
     the row's full height, so neither line can run under the button, which
     is absolutely positioned in that padding. The summary shows only its
     arrow: the label is clipped and transparent but stays real text in the
     accessibility tree, so the control keeps its name. (font-size: 0 would
     do the same but the type-scale spec forbids a raw size.) */
  .standings tr { display: block; position: relative;
                  padding: var(--space-2) calc(var(--tap) + 2 * var(--space-2)) var(--space-2) var(--space-3);
                  min-height: calc(var(--tap) + 2 * var(--space-2));
                  margin-bottom: var(--space-2); }
  .standings .standings-team { display: block; font-weight: 600; }
  .standings .standings-level, .standings .standings-time { display: inline; }
  /* display: inline overrides .table--cards td::before { display: block }. */
  .standings .standings-time::before { content: " · "; display: inline; color: var(--text-dim); }
  .standings .standings-log { display: none; }
  /* The open panel spans the row's content edges as before: the row's right
     padding is (tap + 2 * space-2) but the content edge is space-3 from the
     border, so pull the cell out by the difference. */
  .standings .standings-actions { display: block; padding: 0;
                                  margin-right: calc(-1 * (var(--tap) + 2 * var(--space-2)) + var(--space-3)); }
  .standings .team-disclosure { position: absolute; top: var(--space-2); right: var(--space-2);
                                width: var(--tap); min-width: var(--tap); min-height: var(--tap);
                                padding: 0; overflow: hidden; white-space: nowrap; }
  .standings .team-disclosure .disclosure-label { position: absolute; width: 1px; height: 1px; overflow: hidden;
                                                  clip-path: inset(50%); white-space: nowrap; }
  .standings .team-disclosure::after { position: absolute; inset: 0; margin: 0; display: flex;
                                       align-items: center; justify-content: center;
                                       font-size: var(--text-lg); color: var(--text); }
  .standings details[open] .team-panel { margin-top: var(--space-2); }
  .standings .team-panel-actions .btn,
  .standings .team-panel-actions select,
  .standings .team-panel-actions input[type=submit],
  .standings .team-panel-actions button { min-height: var(--tap); width: 100%; }
}

/* --- Full log on a phone (logs/show_full_log) -------------------------------
 * Desktop keeps the levels x teams grid. Below 48rem the same markup
 * restacks: each level row is a section heading, each team cell an indented
 * block -- no inner sideways scroller, whatever the team count. */
.log-team { display: block; }
.log-ok { color: var(--go); font-weight: 600; margin-left: var(--space-1); }
.log-none { color: var(--text-dim); font-size: var(--text-sm); }

@media (max-width: 47.99rem) {
  .log-matrix, .log-matrix tbody, .log-matrix tr, .log-matrix td { display: block; width: auto; }
  .log-matrix td { border: 0; padding: var(--space-1) 0 var(--space-2) var(--space-3);
                   overflow-wrap: anywhere; }
  .log-matrix .log-level-row td { padding: var(--space-4) 0 var(--space-1);
                                  border-top: 1px solid var(--border); }
  .log-matrix ul { list-style: none; }

  /* Live channel as two lines: time · team / level — code. */
  #livechannel .lc-time, #livechannel .lc-team,
  #livechannel .lc-level, #livechannel .lc-code { display: inline; padding: 0; }
  /* components.css makes every card-cell ::before a block; the separators
     here must stay inline or each becomes its own line. */
  #livechannel td::before { display: inline; }
  #livechannel .lc-team::before { content: " · "; color: var(--text-dim); }
  #livechannel .lc-level::before { content: "\A"; white-space: pre; }
  #livechannel .lc-code::before { content: " — "; color: var(--text-dim); }
  #livechannel tr { padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2); }
}

/* --- Empty states (shared/_empty_state) ---------------------------------- */
.empty-state > * + * { margin-top: var(--space-2); }
.empty-state-title { font-size: var(--text-lg); }
.empty-state p, .empty-state-line { color: var(--text-dim); }
.empty-state-line { font-size: var(--text-sm); }
.empty-state-action { display: inline-flex; align-items: center; min-height: var(--tap); }
