/*
 * Application shell.
 *
 * Three breakpoints, three different jobs. On a desktop the board sits between
 * two panels. On a tablet the right panel drops below. On a phone the board
 * takes the screen and the panels become a sheet you pull up — a chess board
 * squeezed into a third of a phone screen is not worth having.
 */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--surface-0);
  color: var(--text-1);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

:where(button, input, select, textarea) {
  font: inherit;
  color: inherit;
}

:where(a) {
  color: var(--accent);
}

/* One consistent focus treatment, visible on every surface. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---------------------------------------------------------------- layout -- */

.app {
  display: grid;
  /*
   * The column is stated explicitly. Left implicit it is `auto`, which sizes to
   * max-content — so the widest thing anywhere inside sets the width of the
   * whole app, and the board overflows the viewport by whatever that comes to.
   */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: var(--header-height) 1fr;
  height: 100dvh;
  overflow: hidden;
}

/*
 * The board sits flush with the bottom of the viewport, so its focus ring — an
 * outset box-shadow — was clipped away on that edge only, leaving a keyboard
 * user a ring on three sides. Inset the ring for this one element instead.
 */
.stage__board:focus-visible {
  box-shadow:
    inset 0 0 0 2px var(--surface-0),
    inset 0 0 0 4px var(--accent);
}

.app__body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--panel-width);
  min-width: 0;
  min-height: 0;
}

.stage {
  position: relative;
  display: grid;
  /*
   * One column, because there is one child. Declared as `auto minmax(0, 1fr)`
   * the frame landed in the auto track and sized to its own content, leaving
   * the 1fr track as a phantom that ate the rest of the row — 59% of it in the
   * 641-900px band, with the board shoved hard left. Above 900px the auto
   * track happened to absorb everything, which is why it never showed.
   */
  grid-template-columns: minmax(0, 1fr);
  min-width: 0;
  min-height: 0;
  background: radial-gradient(120% 80% at 50% 0%, var(--surface-1) 0%, var(--surface-0) 70%);
}

/* Eval bar and board, side by side and the same height. */
.stage__frame {
  display: flex;
  align-items: stretch;
  justify-content: center;
  min-width: 0;
  min-height: 0;
}

/*
 * Explicit flex sizing. With `aspect-ratio` and no flex basis, the board sizes
 * itself from its (empty) content and comes out far narrower than the row it
 * sits in.
 */
.stage__board {
  position: relative;
  /*
   * Never wider than the board can use.
   *
   * A tilted board projects to about 1.6:1, so on a canvas flatter than that
   * the fit is limited by height and the leftover width is dead — the board
   * floated in the middle of it while the eval bar stayed pinned to the far
   * left, 134px away from the thing it measures. `aspect-ratio` here is a cap,
   * not a shape: the height is explicit, so it only ever decides the width, and
   * `max-width` gives the available space back whenever it is the smaller of
   * the two. Below 1.6 the canvas is already narrower than this and nothing
   * changes, which is every desktop and every phone.
   *
   * A constant, not something derived from the board: the tilt reads the
   * device, so trimming the canvas cannot feed back into the shape being
   * trimmed to.
   */
  flex: 0 1 auto;
  height: 100%;
  aspect-ratio: 1.6;
  max-width: 100%;
  min-width: 0;
  min-height: 0;
}

.stage__board canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* ---------------------------------------------------------------- header -- */

.topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-4);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}

/*
 * The wordmark and the star, as one thing.
 *
 * The star used to be the last item in the right-hand cluster, which is where a
 * page puts what it hopes you will not click. Beside the name it is the second
 * thing read, and it says what the name alone cannot: that this is a project
 * you can go and look at.
 */
.topbar__identity {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

.topbar__brand {
  margin: 0;
  font-size: var(--text-md);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
}

.topbar__spacer {
  flex: 1;
}

.topbar__nav {
  display: flex;
  flex: none;
}

.topbar__actions {
  display: flex;
  gap: var(--space-1);
  /* `flex: none` is load-bearing. Without it the row was allowed to overflow
     the space flex gave it and ran underneath the utilities cluster — measured
     137px of overlap at 901px, with the icons drawn on top of "New game". */
  flex: none;
  min-width: 0;
}

/* The primary action gets air, so the group before it reads as its own. */
.topbar__actions .button--primary {
  margin-left: var(--space-2);
}

/* In the top layer, so neither the app's `overflow: hidden` nor the action
   row's own scroller can clip it. */
.topbar__menu {
  position: fixed;
  inset: auto;
  margin: 0;
  padding: var(--space-1);
  min-width: 12rem;
  /* Anchored by its right edge, so this is what keeps it on screen at any
     width — it can never be wider than the window it is measured against. */
  max-width: calc(100vw - 1rem);
  display: none;
  flex-direction: column;
  gap: 2px;
  background: var(--surface-2);
  color: var(--text-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

.topbar__menu:popover-open {
  display: flex;
}

.topbar__menu .button {
  justify-content: flex-start;
  width: 100%;
}

/*
 * Chrome, after the game actions and behind a rule. Two controls: the settings
 * you change and the menu that holds everything you almost never do.
 */
.topbar__utilities {
  display: flex;
  flex: none;
  gap: var(--space-1);
  padding-left: var(--space-2);
  margin-left: var(--space-2);
  border-left: 1px solid var(--border);
}

/* Drawn icons, one 24-grid and one stroke weight, sized by the button. */
.icon {
  flex: none;
  display: block;
}

/* --------------------------------------------------------------- controls -- */

.button {
  /*
   * Positioned so the visually-hidden label the narrow layout uses stays inside
   * it. Absolutely positioned with no positioned ancestor, that label resolved
   * against the viewport and escaped every overflow clip on the way out — one
   * 1px span, sitting past the right edge, scrolling the whole page sideways.
   */
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  /* The 44px touch floor. Controls measured 36px (and 32px wide on a phone),
     which is 67% of the minimum target area on the surface where pointing is
     least precise. */
  min-height: 2.75rem;
  background: var(--surface-3);
  color: var(--text-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition:
    background var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
  white-space: nowrap;
}

.button:hover:not(:disabled) {
  background: var(--surface-4);
  border-color: var(--border-strong);
}

.button:active:not(:disabled) {
  transform: translateY(1px);
}

/*
 * Opacity alone was the whole disabled signal, and it dropped the glyph to
 * 2.67:1 against its own background — unreadable rather than obviously off,
 * and invisible on touch where there is no cursor to change.
 */
.button:disabled {
  opacity: 1;
  color: var(--text-disabled);
  background: transparent;
  border-color: transparent;
  cursor: not-allowed;
}

.button--primary {
  background: var(--accent);
  border-color: transparent;
  color: var(--accent-text);
}

.button--primary:hover:not(:disabled) {
  background: var(--accent-hover);
}

.button--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}

.button--quiet:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--text-1);
}

/*
 * Resign at rest used to fall through to the full filled `.button` treatment
 * while every neighbour was quiet — making the one irreversible control the
 * loudest thing in the row. It now reads as danger only once you are on it.
 */
.button--danger {
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}

.button--danger:hover:not(:disabled),
.button--danger:focus-visible {
  background: color-mix(in oklab, var(--negative) 18%, var(--surface-3));
  border-color: var(--negative);
  color: var(--negative);
}

/*
 * The star link, beside the wordmark.
 *
 * A ghost button carrying the only warm colour in the bar: loud enough to find
 * without being a second call to action next to "New game". It was declared
 * above `.button` before, at equal specificity, so its transparent background
 * and border lost the cascade outright and it rendered as the only filled chip
 * in the row — the exact opposite of what it needed to be.
 */
.starlink {
  text-decoration: none;
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}

.starlink:hover {
  background: var(--surface-3);
  border-color: transparent;
  color: var(--text-1);
}

.starlink .icon {
  color: var(--warning);
}

.button--icon {
  padding: 0;
  width: 2.75rem;
  min-height: 2.75rem;
}

.button-row {
  display: flex;
  gap: var(--space-2);
}

.button-row .button {
  flex: 1;
}

.field {
  display: grid;
  gap: var(--space-1);
}

.field > label,
.field__label {
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

.select {
  appearance: none;
  width: 100%;
  padding: var(--space-2) var(--space-5) var(--space-2) var(--space-3);
  min-height: 2.75rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  cursor: pointer;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    right 1rem top 55%,
    right 0.7rem top 55%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}

.select:hover {
  border-color: var(--border-strong);
}

.switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-2) 0;
  font-size: var(--text-sm);
  cursor: pointer;
}

.switch input {
  appearance: none;
  position: relative;
  width: 2.25rem;
  height: 1.25rem;
  flex: none;
  background: var(--surface-4);
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease-out);
}

.switch input::after {
  content: '';
  position: absolute;
  inset: 2px auto 2px 2px;
  width: 1rem;
  background: var(--text-1);
  border-radius: 50%;
  transition: translate var(--duration-fast) var(--ease-out);
}

.switch input:checked {
  background: var(--accent);
}

.switch input:checked::after {
  translate: 1rem 0;
  background: var(--accent-text);
}

.range {
  width: 100%;
  /* The bare input renders a 16px-tall target; the floor is 44. */
  min-height: 2.75rem;
  accent-color: var(--accent);
}

/* ----------------------------------------------------------------- panel -- */

.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface-1);
  border-left: 1px solid var(--border);
}

/*
 * Pinned. The clocks, status and engine readout used to share one scroller with
 * the move list, and `flex: 1` on the moves section was inert against a block
 * parent — so a long game simply pushed the clocks off the top of the screen.
 */
.panel__fixed {
  flex: none;
}

/*
 * The match band: both players and the state of the game, in one block.
 *
 * The padding is asymmetric on purpose. The rows carry their own inset, so the
 * band only needs to hold them off the panel edges; the generous space is
 * below, separating the band from the move list, which is the one division in
 * the column that means anything.
 */
.panel__match {
  padding: var(--space-2) var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
}

/* The only thing in the panel that scrolls, and it owns its own scroller so
   bringing the current move into view no longer drags the clocks away. */
.panel__moves {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  padding: var(--space-3) var(--space-4) 0;
}

.panel__moves .movelist {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  /* Room for the focus ring on the last row, which the scrollport used to cut. */
  padding-bottom: var(--space-1);
}

/*
 * The engine readout, below the list rather than above it.
 *
 * It is ambient — you glance at it between moves — and at the top it was
 * holding 94px of the fixed budget, above the move list, with a heading
 * shouting ENGINE at something that already says its own name.
 */
.panel__engine {
  flex: none;
  padding: var(--space-3) var(--space-4) var(--space-2);
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------ transport --- */

/*
 * Move navigation, attached to the move list it drives. It replaces a 56px left
 * rail that spent 41% of its height empty and sat a screen's width away from
 * the list it controlled.
 */
.transport {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex: none;
  padding: var(--space-2) 0 var(--space-3);
  border-top: 1px solid var(--border);
}

.transport__group {
  display: flex;
  gap: var(--space-1);
}

/* Hint and take back. Labelled, because as glyphs "take back" and "back one
   move" were two similar marks with very different consequences. */
/*
 * No rule above it: the engine strip already opened the footer, and a second
 * hairline 60px later divided nothing. The panel is down to two lines from
 * four, and each of the two now marks a real change of subject — the players,
 * the game, then the machine and what it can do for you.
 */
.assist {
  display: flex;
  gap: var(--space-2);
  flex: none;
  padding: 0 var(--space-4) var(--space-3);
}

.assist .button {
  flex: 1;
  justify-content: center;
}

/*
 * A short window tips the view further down (see `polarForAspect`), and a
 * flatter board projects closer to square — so the cap that suits a tablet is
 * too generous here and the eval bar drifts away again. Same idea, tighter
 * number; the board is height-limited at these shapes either way, so this only
 * trims canvas it was not using.
 */
@media (max-height: 560px) {
  .stage__board {
    aspect-ratio: 1.3;
  }
}

/* ------------------------------------------------------------ responsive -- */

@media (max-width: 1100px) {
  :root {
    --panel-width: 18rem;
  }
}

@media (max-width: 900px) {
  .app__body {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
  }

  /*
   * The panel becomes the scroller in this band. Capped at 45dvh with a fixed
   * header that refuses to shrink, everything below it — the move list, the
   * transport, hint and take back — was squeezed to nothing and unreachable.
   */
  .panel {
    border-left: none;
    border-top: 1px solid var(--border);
    max-height: 45dvh;
    overflow-y: auto;
  }

  .panel__moves {
    flex: none;
  }

  .panel__moves .movelist {
    overflow-y: visible;
  }
}

@media (max-width: 900px) {
  /* Labels go, icons carry. The primary action keeps its label all the way
     down: it is the one control here anybody came for, and a bare plus sign
     does not say "new game". The accessible name is on the button either way. */
  .topbar__nav .button__label,
  .topbar__actions .button:not(.button--primary) .button__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .topbar__nav .button,
  .topbar__actions .button:not(.button--primary) {
    padding: var(--space-2);
    min-width: 2.75rem;
  }
}

@media (max-width: 640px) {
  :root {
    --header-height: 3.25rem;
  }

  /* Draw and Resign have moved into the menu by here — see the header's own
     media query — so the row is the wordmark, the star, the library, one
     primary action and two icons, and it no longer needs to scroll. */
  .topbar {
    padding: 0 var(--space-2);
    gap: var(--space-2);
  }

  .topbar__brand {
    font-size: var(--text-sm);
  }

  .starlink .button__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .starlink {
    padding: var(--space-2);
    min-width: 2.75rem;
  }
}

@media (max-width: 640px) {
  /*
   * Stacked, not a sheet.
   *
   * A 3D board in portrait is framed by its width, so it occupies a square no
   * matter how tall the viewport is. Letting the stage take the full height
   * just leaves a band of empty table above and below it, so the board gets a
   * square of its own and everything else gets the rest.
   */
  .app__body {
    grid-template-rows: auto auto minmax(0, 1fr);
  }

  .stage {
    grid-template-columns: minmax(0, 1fr);
  }

  /*
   * On a phone the page scrolls; it does not compress.
   *
   * Locked to the viewport height, the grid squeezed the stage below what its
   * contents needed and the eval bar spilled over the panel underneath. A
   * board, two clocks, a status line and a move list do not fit in one phone
   * screen, and pretending otherwise just breaks the layout.
   */
  .app {
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }

  .app__body {
    grid-template-rows: auto auto auto;
    min-height: 0;
  }

  /* The page scrolls on a phone, so the move list does not need its own
     scroller — and a nested one would trap the gesture. */
  .panel__moves .movelist {
    overflow: visible;
  }

  /*
   * The box is the shape the board draws, not a square.
   *
   * A square reserved 390px of height on a 390px phone for a board that drew
   * 268 of it — 122px of empty table above and below, a third of the box, which
   * read as "the board is small" rather than as framing. At the tilt this width
   * gets (see polarForAspect) the silhouette is about 1.15:1, and the eval bar
   * takes 42px off the width, so 1.29 is the box that fits it.
   */
  .stage__frame {
    aspect-ratio: 1.29;
    max-height: 55dvh;
    margin-inline: auto;
    width: 100%;
  }

  .panel {
    border-left: none;
    border-top: 1px solid var(--border);
    max-height: none;
  }

  .evalbar {
    margin: var(--space-2) 0 var(--space-2) var(--space-2);
  }
}

/* The last stop before the smallest phones.
   Four 44px targets and a wordmark fit in 320px; the same four with "New game"
   spelled out need 437, which is wider than the phone this is aimed at. The
   library moves into the menu here too — see the header's own breakpoints. */
@media (max-width: 480px) {
  .topbar {
    gap: var(--space-1);
    padding: 0 var(--space-1);
  }

  .topbar__actions .button--primary .button__label {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .topbar__actions .button--primary {
    padding: var(--space-2);
    min-width: 2.75rem;
    margin-left: 0;
  }

  .topbar__utilities {
    padding-left: var(--space-1);
    margin-left: var(--space-1);
  }
}
