/* public/stylesheets/layout.css -- page shells and the drawer. */

.page {
  display: grid;
  /* minmax(0, 1fr), not 1fr: a bare 1fr is minmax(auto, 1fr), whose auto floor
     lets an unshrinkable child set the track's width. The header's locale
     switcher did exactly that, forcing 454px of scroll on a 390px viewport. */
  grid-template-columns: minmax(0, 1fr);
  /* svh, NOT vh, and this one line is a fix rather than a preference.
     min-height clamps height, so `.page { min-height: 100vh }` beat
     `.page--focused { height: 100dvh }` on the very device that rule was
     written for: on an iPhone (390x844, ~680px actually visible in Safari)
     100vh is the URL-bar-HIDDEN height, so the play shell computed to 844 and
     hung 164px below the screen -- measured, with the answer bar's submit
     button at y=764..808 against a fold at 680. Every earlier measurement
     missed it because in a headless browser at a fixed window size 100vh and
     100dvh are the same number, and Capybara parses no CSS at all.
     100svh is the SMALL viewport -- browser chrome showing, the smallest the
     visible area ever gets -- which is what "fill the screen when the content
     is short" actually means, and it can never clamp a dvh height upward. */
  min-height: 100svh;
  /* Rows size to content. Without this, align-content defaults to `normal`
     (= stretch for grid), so on any page shorter than the viewport the
     min-height free space above inflates the topbar into a slab -- 386px, half
     the screen, on the landing page at 390px. */
  align-content: start;
}

.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  /* Above #drawer's 30 and .drawer-scrim's 25, so an open drawer slides in
     BELOW the header instead of over it. Before this the drawer (fixed at
     inset: 0 auto 0 0, z-index 30) started at y=0 and buried the header's
     left 320px: the home link, the clock, the theme and language controls on
     a phone -- and the very hamburger that had just been pressed, which is
     the only visible way back out on a touch device. The geometry that keeps
     the drawer clear of this bar is #drawer's top, below. */
  z-index: var(--z-topbar);
  /* Same reason as .page above: without this, the grid item refuses to
     shrink below its content's intrinsic width. */
  min-width: 0;
}

/* Theme toggle + language switcher, as one unit pushed to the end of the
   line. Grouping them is not cosmetic: .topbar wraps, and a loose
   #locale-switcher lands wherever the break happens to fall, which is what
   threw the dropdown off screen (see .locale-menu below). margin-left: auto
   inside a wrapping flex container applies to whichever line the group ends
   up on, so the trigger's right edge is the topbar's right padding edge at
   every width -- there is no longer a viewport at which the anchor moves. */
.topbar-chrome {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
}

/* Phone-width header: three rows down to two.
   The header carries four things -- hamburger 44, home link 140, timestamp
   144, chrome group 200 -- and at 375px the first three come to 352 against
   343 of usable width. Nine pixels short, so the timestamp took a row of its
   own and the bar stood 163px tall, 29% of a 375x553 screen.
   Tightening the gap and the padding buys 16 of those pixels and a smaller
   timestamp buys 18 more, which clears 375 and 360 with room over: measured
   163px -> 121px, and two rows from 360px up.
   320px still wraps to three (151px) and cannot not: the first line alone
   would need the timestamp down to about 96px, which is unreadable. That is
   an iPhone-5-era width and the bar is at least 12px shorter there too.
   The timestamp's smaller size is scoped to this query rather than applied
   everywhere -- at desktop widths the whole header is one 69px row and there
   is nothing to win. */
@media (max-width: 47.99rem) {
  .topbar { gap: var(--space-2); padding: var(--space-3); }
  .topbar-time { font-size: var(--text-sm); }
}

.main {
  padding: var(--space-5) var(--space-4);
  max-width: 72rem;
  width: 100%;
  margin: 0 auto;
  min-width: 0;
  /* break-word, not anywhere, so a table's min-content width and therefore
     desktop column layout are unchanged; it only stops a long unbroken name
     or answer in block text from pushing the page sideways at 390px
     (measured 93-146px on four operator screens). */
  overflow-wrap: break-word;
}

/* Tap-target floor for the primary navigation. These were plain inline text
   links -- 19-20px tall, well under the 44px minimum the spec requires for
   interactive elements. min-height does nothing on an inline box, so each
   also needs display: inline-flex to make it take effect. */
.home-link,
.locale-switcher-link,
#drawer .game-list a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

/* The locale dropdown. Opened by hover on a pointer and by :focus-within
   everywhere else -- that second selector is what makes it work on a phone
   with no JavaScript, because tapping the trigger focuses it, and it covers
   keyboard tabbing into the menu at the same time.

   The menu is hidden by THIS RULE, in an external stylesheet, and that is
   load-bearing rather than incidental. Capybara's rack-test driver parses
   neither stylesheets nor computed style: Capybara::Node::Simple#visible?
   looks at input[type=hidden], <template>, the hidden ATTRIBUTE, an INLINE
   style containing display:none, and script/head/style tags -- nothing else.
   So every link in here stays clickable for
   features/i18n/switch-language.feature:37, whose step is
   within("#locale-switcher") { click_link(label) } and whose file is frozen.

   Therefore, three things not to do later: do not move this hiding into an
   inline style or a hidden attribute, do not rebuild this as <details>
   (a closed one IS special-cased invisible by that same matcher), and do not
   make it a <select> (no JS in this app to navigate on change). */
#locale-switcher { position: relative; }

/* right: 0, and only safe because .topbar-chrome pins the trigger to the end
   of its line. The menu is ~160px wide and opens inward from the trigger's
   right edge; anchoring it to a trigger that can wander is what put it off
   screen. Swept over 12 widths (320..1440) x all 7 locale labels, since the
   label sets the trigger's width and therefore where the header wraps:

     right: 0, loose trigger   fails at 320/360/375/600 -- 0 of 7 on screen at
                               360px, x = -36 at 375px
     left: 0, loose trigger    fixes the left, overflows the RIGHT at 375/600
                               when a short label (Polski) leaves room on the
                               first line and the trigger ends up near the edge
     right: 0, grouped         84/84 on screen

   So do not "simplify" this by taking the controls back out of
   .topbar-chrome, and do not switch the side: neither side is safe on its own
   while .topbar wraps. */
.locale-menu {
  position: absolute;
  top: 100%;
  right: 0;
  /* Inside .topbar's stacking context (z-index 40), so this only orders the
     menu against its siblings in the header. */
  z-index: var(--z-dropdown);
  display: none;
  min-width: 10rem;
  padding: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

#locale-switcher:hover .locale-menu,
#locale-switcher:focus-within .locale-menu { display: block; }

/* flex rather than block so min-height takes effect -- the same reason
   .locale-switcher-link above needs inline-flex. */
.locale-menu a,
.locale-menu .locale-switcher-current,
.locale-menu .locale-switcher-inert {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  white-space: nowrap;
}

.locale-menu .locale-switcher-current { font-weight: 600; }

/* Rendered instead of a link on a page that came back from a POST or PATCH
   (see app/views/layouts/_header.html.erb for why there is nothing safe to
   link to there). Dimmed so it reads as unavailable rather than as a link
   that ignores clicks. */
.locale-menu .locale-switcher-inert { color: var(--text-dim); }

/* The drawer is driven by a hidden checkbox so it opens with CSS alone.
   drawer.js only adds Escape-to-close and focus handling -- without it the
   navigation still works, which matters because this markup is on the login
   page too.

   Visually hidden but still focusable. NOT display:none and NOT
   visibility:hidden -- either removes it from the tab order, and it is the
   only keyboard-operable way to open the drawer. */
#drawer-state {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  opacity: 0;
}

/* The focus ring has to appear on the thing the user can see. */
#drawer-state:focus-visible ~ .page #drawer-toggle {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

#drawer-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap);
  min-height: var(--tap);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-xl);
  cursor: pointer;
}

/* --topbar-h is published by drawer.js from the header's measured height, and
   both the drawer and the scrim start below it so the header stays visible
   AND clickable while the menu is open -- .topbar's z-index 40 is the other
   half of that. It has to be measured rather than written down: the header
   wraps, so it is 69px on a desktop, 125px at 390px wide, and 181px at 375px
   before the .topbar-chrome grouping above pulled it back to two rows.

   The fallback is the one-row height (a --tap-tall control between two
   --space-3 paddings, 68px) rather than 0. If drawer.js does not run, a
   one-row header is still fully clear and a wrapped one loses only its lower
   rows -- strictly better than the 0 that buried all of it. Everything here
   still works with JavaScript off; see the checkbox comment above. */
#drawer {
  position: fixed;
  top: var(--topbar-h, calc(var(--tap) + var(--space-3) * 2));
  bottom: 0;
  left: 0;
  width: min(82vw, 20rem);
  padding: var(--space-4);
  background: var(--surface);
  border-right: 1px solid var(--border);
  transform: translateX(-100%);
  transition: transform 0.18s ease;
  z-index: var(--z-drawer);
  overflow-y: auto;
}

#drawer-state:checked ~ .page #drawer { transform: translateX(0); }

.drawer-scrim {
  position: fixed;
  top: var(--topbar-h, calc(var(--tap) + var(--space-3) * 2));
  right: 0;
  bottom: 0;
  left: 0;
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.18s ease;
  z-index: var(--z-scrim);
}

#drawer-state:checked ~ .page .drawer-scrim { opacity: 1; pointer-events: auto; }

/* From tablet up the menu is simply always there and the toggle disappears.

   48rem (768px), not the 60rem this used to be. 60rem meant a laptop browser
   window narrower than 960 CSS px -- routine once Windows display scaling is
   on -- got the phone overlay drawer on a screen with room to spare, which is
   the state the header-overlap bug was reported from. At 768px the permanent
   sidebar takes 256px and leaves 512px of content, and the admin tables that
   are the widest thing here already scroll inside .table-wrap.

   Deliberately NOT changed to match: the 60rem query in screens.css, which
   decides when .dash-grid goes two-up. That is about how much room the
   CONTENT has, and between 768 and 960 the content column is 512px -- still a
   single column's worth. */
@media (min-width: 48rem) {
  .page { grid-template-columns: 16rem minmax(0, 1fr); grid-template-areas: "top top" "nav main"; }
  .topbar { grid-area: top; }
  #drawer-toggle, .drawer-scrim { display: none; }
  #drawer {
    grid-area: nav;
    position: static;
    transform: none;
    width: auto;
    border-right: 1px solid var(--border);
  }
  .main { grid-area: main; }
}

/* The play shell: no menu, no max-width, room for a pinned bar.
   Both properties must be overridden together. grid-template-areas from the
   48rem block above still declares two columns, and the explicit grid takes
   the MAX of the two definitions -- so overriding only the columns leaves a
   phantom second track and pushes .main into it. */
.page--focused {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "top" "main";
  /* This shell used to be exactly one viewport tall -- `height: 100dvh`, rows
     `auto minmax(0, 1fr)`, `align-content: stretch` -- so the answer bar could
     sit at the bottom while the task area scrolled between it and the header.
     Every one of those is gone, and it grows with its content like every other
     page in the app.
     The premise was that each region could be squeezed to a floor and scroll
     inside itself, which is true of text and false of a photograph. With
     attachments on a task and on a hint, measured at 390x680: the task area
     held 479px of content in 170px, the option list 228px in 76px, and none of
     the four answers could be tapped without scrolling a third nested
     scrollport. See the .playbar comment in screens.css for the full history
     -- it matters, because two earlier designs were reactions to a bar that
     was OUT of flow, and reintroducing either one would be a regression. */
}

/* The play screen used to hide .topbar-chrome -- both the theme toggle and the
   language switcher -- on the grounds that they were "a race, not a browsing
   session". That rule is gone as of 2026-08-17, and it is worth knowing why so
   it does not come back by reflex.

   Its real justification was height, and the height was real: with four flat
   locale links the header wrapped onto three rows at 390px, 181px of a 680px
   screen, more than the question itself. Then ed8ecc2 made the switcher a
   dropdown, one 44px trigger that does not grow with the locale count, and the
   number stopped being true -- the comment here was updated to admit as much
   and the rule was kept anyway, on taste alone.

   What that cost: the controls were `display: none` while still in the DOM, so
   every request spec asserting id="theme-toggle"/id="locale-switcher" stayed
   green (spec/requests/ui_shell_spec.rb, ui_contract_spec.rb) and nothing in
   either suite could see the difference. .page--focused is set by
   layouts/in_game.html.erb, which GamePassingsController renders for EVERY
   play screen, so this hit real scheduled runs exactly as hard as test runs --
   an author testing their own game could not check it in a second theme or
   language without leaving the run, and a player mid-race had no way at all.
   Reported from a live test run against a quiz.

   What it costs, measured rather than assumed -- the first draft of this
   comment guessed "unchanged" and was wrong by a whole row. The header goes
   69px -> 121px at 390x680 and at 375x553; desktop (1280x800) stays 69px,
   where the row has width to spare. So the chrome does take a second row on a
   phone: 52px, 7.6% of a 390x680 viewport and 9.4% of an iPhone SE's 553px.
   That is the price of the fix and it is a real one -- but it is a third of
   the 181px the original rule was written against, and the full harness stays
   green at every size: the submit button is still hit-testable at both ends of
   the scroll, nothing scrolls inside anything else, horizontal overflow is 0,
   and the answer bar still rests flush on the bottom edge.

   If that row ever needs to come back, shrink the controls on .page--focused
   (an icon-only toggle, a two-letter locale trigger) rather than hiding them
   again. spec/layout/play_screen_layout_spec.rb hit-tests both controls here
   now, which is the check that would have caught the original regression. */
/* No bottom reserve for the pinned answer bar: it is position: sticky, so it
   holds its own space in flow at whatever height its contents come to. The
   12rem/17rem constants that used to live here were measured against a bar
   holding a code field and were 118px short of a quiz bar, which is how the
   captain's exit button ended up unreachable underneath it. See the .playbar
   comment in screens.css. */
.page--focused .main {
  max-width: 44rem;
  /* A column still, so the bar is reliably the last box and .play-body
     reliably the one above it -- but no longer a column that rations a fixed
     height. `min-height: 0` and `overflow-y: auto` are gone with the shell
     that needed them: the second was documented as a last resort that "should
     never fire on a real phone" and fired on every iPhone SE (553px of visible
     viewport against the ~595px the note assumed). */
  display: flex;
  flex-direction: column;
}

/* --- The play shell where there is room for two columns -----------------
 * Below this width the play screen is one column that scrolls, with the
 * answer bar stuck to the bottom of the viewport.
 *
 * Letting the page scroll was measured and REJECTED once, when the shell was
 * a viewport-height column: content ~890px tall against a 780px window
 * scrolled the page 110px and put «Отправить!» below the fold, at 1920x780,
 * 1440x800, 1024x600 and 845x600. That finding was about a bar that scrolled
 * away with everything else. A sticky bar cannot leave the bottom edge, so
 * the failure it describes is not reachable from the current design -- which
 * is why the same approach is now the right one, and why this note stays
 * rather than being deleted as obsolete.
 *
 * The two columns are still worth having here: at 1920x780 the single stack
 * left roughly 1200px of horizontal space idle either side of a 44rem column
 * while the answer bar and the question competed for vertical room. Task
 * text and the captain's controls take the left column, the answer bar the
 * right. Measured with the worst content state (quiz options and a
 * wrong-answer flash), page scroll and every clip are 0 and the submit button
 * is hit-testable at 1920x780, 1440x800, 1280x845, 845x600 and 800x900;
 * 1024x600 leaves 2px of page scroll.
 *
 * 52rem (832px), one threshold and not two: it is where two readable columns
 * first fit (24rem of task + 20rem of bar + the gap), and it catches a
 * scaled-down laptop viewport at 845px, which a 48rem or 60rem line would
 * have left on one or the other side of the fix.
 *
 * A width query rather than a height one: a height query would flip the
 * whole layout when someone opened dev tools.
 *
 * The companion half of this block -- the bar's full-bleed margins and its
 * stickiness -- is in screens.css, next to the rules it undoes. */
@media (min-width: 52rem) {

  .page--focused .main {
    /* 44rem is a single column of prose. Two columns need the room the rest
       of the app already takes (.main's own 72rem). */
    max-width: 72rem;
    /* Replaces the flex column below 52rem. Auto-placement rather than named
       areas: the full-width items are the title and any number of flashes
       (the layout renders one per Rails flash entry, and the view adds answer
       feedback of its own), and two items sharing one named area would stack
       on top of each other instead of taking a row each. Spanning both
       columns and letting the grid place them keeps that correct for any
       count. The two single-column items follow, so they land side by side in
       the next row. */
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 24rem);
    /* Each item at its natural height; without this the two columns stretch
       to match each other. */
    align-items: start;
    /* Column gap only -- the row rhythm is each block's own margin
       (screens.css), and a row gap here would double it. */
    gap: 0 var(--space-5);
  }

  .page--focused .main > h2,
  .page--focused .main > .flash { grid-column: 1 / -1; }
}

@media (prefers-reduced-motion: reduce) {
  #drawer, .drawer-scrim { transition: none; }
}

/* Marks the viewer's own row in a completed-game standings table
   (games/_pass_standings.html.erb, rendered with an optional `highlight`
   local). game_passings/gated_finish.html.erb passes its own attempt so a
   team that just finished a paid game can find themselves in the table
   without reading every row; games/show.html.erb renders the same partial
   with no highlight and is unaffected.

   Background and font-weight only -- .table--cards is already responsive
   (collapses to cards below 48rem), and anything that changes the row's box
   needs bin/measure-play-screen re-run to catch a new overflow or scroll. */
.table--cards tr.is-you { background: var(--surface-2); font-weight: 600; }
