:root {
  --bg-1: #0b1320;
  --bg-2: #121d2c;
  --panel: rgba(18, 29, 44, 0.9);
  --panel-strong: rgba(10, 16, 26, 0.92);
  --board-bg: #1b2c3f;
  --tile-empty: rgba(255, 255, 255, 0.03);
  --tile-border: rgba(255, 255, 255, 0.08);
  --tile-placed: #d9bf70;
  --tile-placed-2: #ebd38a;
  --tile-selected: #7ae0ff;
  --accent: #76f2c3;
  --accent-2: #ffb75d;
  --text: #ebf4ff;
  --muted: #b6c9d9;
  --danger: #ff7e73;
  --shadow: rgba(0, 0, 0, 0.42);
}

* { box-sizing: border-box; }

html, body, #root {
  margin: 0;
  width: 100%;
  height: 100%;
  background:
    radial-gradient(circle at top, rgba(69, 116, 173, 0.22), transparent 30%),
    linear-gradient(180deg, var(--bg-1), var(--bg-2));
  color: var(--text);
  font-family: "Inter", sans-serif;
}

body {
  overflow: hidden;
  /* A phone would otherwise rubber-band the whole page when a gesture on the
     board reaches its edge, which reads as the board coming unstuck. */
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
}

/* Percentage heights resolve against the largest viewport a mobile browser
   might have, so the bottom of the app ends up behind the address bar. dvh
   tracks the chrome as it hides and shows; the percentage above stays as the
   fallback for anything that does not know it. */
@supports (height: 100dvh) {
  html, body, #root { height: 100dvh; }
}

.app-shell {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  /* The row has to be told it may be shorter than its content. Left at the
     default `auto` it grows to fit the sidebar, the sidebar never becomes the
     thing that scrolls, and because body is overflow:hidden the panels at the
     bottom are simply unreachable. */
  grid-template-rows: minmax(0, 1fr);
}

.sidebar {
  background: rgba(6, 11, 18, 0.7);
  border-right: 1px solid rgba(255, 255, 255, 0.06);
  padding: 18px 18px 14px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  overflow-y: auto;
  /* Same reason: a grid item's min-height is auto, which floors it at its
     content height and makes overflow-y a no-op. */
  min-height: 0;
}

.brand {
  display: flex;
  align-items: center;
  justify-content: space-between;

  /* Pinned, with the panels passing underneath.
   *
   * The sidebar is the scroll container, so `top: 0` sticks to its scrollport.
   * The negative margins are the important part: without them the header only
   * covers the content box and panels stay visible sliding through the 18px
   * padding gutters on either side. They have to be undone wherever the
   * sidebar's own padding differs -- see the drawer rules further down. */
  position: sticky;
  top: 0;
  z-index: 5;
  margin: -18px -18px 0;
  padding: 18px 18px 10px;
  background: var(--panel-strong);
  backdrop-filter: blur(8px);
}

/* A soft edge below the header, so a panel scrolling under it dissolves instead
   of being guillotined on a hard line. */
.brand::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 18px;
  background: linear-gradient(180deg, var(--panel-strong), transparent);
  pointer-events: none;
}

.brand h1 {
  margin: 0;
  /* The two rows are sized in container units against this element, and `flex:1`
     is what makes that safe: it gives the h1 the row's leftover width from the
     flex layout rather than from its own contents, so there is no cycle between
     "how wide is the box" and "how big is the text".

     Measuring against the h1 rather than the sidebar is also what keeps the
     phone honest -- there the drawer puts a 32px close button in this same row,
     and a title sized to the full sidebar would run straight into it. */
  flex: 1;
  min-width: 0;
  container-type: inline-size;
  /* Anton is the logo, and it ships in one weight only -- asking for bold here
     would get a synthesised smear on the browsers that oblige. Impact is the
     fallback because it is the one condensed heavy face that is already on
     macOS and Windows, so a failed webfont still looks deliberate rather than
     dropping the title into the body sans. */
  font-family: 'Anton', Impact, 'Haettenschweiler', sans-serif;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  /* Anton's caps carry no descenders, so the two rows can sit tighter than 1
     without colliding. This is what makes it read as one mark. */
  line-height: 0.94;
  display: flex;
  flex-direction: column;
}

/* Each row is scaled so it fills the column on its own: "SPELL IT LIKE" is 13
   characters and "IT'S HOT" is 8, so matching their widths means two different
   sizes. These are measured against Anton itself -- measure them with the
   fallback face loaded instead and you get numbers about a quarter too small,
   because Impact and the system sans are wider than Anton at the same size. */
.brand h1 span:first-child { font-size: 19.8cqw; }
.brand h1 span:last-child  { font-size: 33.3cqw; }

/* The punchline is lit, not painted.
 *
 * Colouring the letters warm was the wrong instinct: orange type just reads as
 * orange type. Fire is light, so the letters stay the same white as the line
 * above and the warmth lives entirely in the glow around them, flickering as
 * though something is burning just out of frame.
 *
 * The keyframes are deliberately uneven -- a smooth pulse reads as a breathing
 * UI element, and it is the irregular timing that makes it read as flame.
 */
.brand h1 span:last-child {
  animation: ember-light 4.4s ease-in-out infinite;
}

@keyframes ember-light {
  0%, 100% { filter: drop-shadow(0 0 10px rgba(255, 150, 60, 0.30))
                     drop-shadow(0 0 26px rgba(255, 110, 40, 0.16)); }
  30%      { filter: drop-shadow(0 0 14px rgba(255, 160, 70, 0.48))
                     drop-shadow(0 0 34px rgba(255, 110, 40, 0.26)); }
  55%      { filter: drop-shadow(0 0  9px rgba(255, 150, 60, 0.26))
                     drop-shadow(0 0 22px rgba(255, 110, 40, 0.13)); }
  78%      { filter: drop-shadow(0 0 13px rgba(255, 165, 75, 0.42))
                     drop-shadow(0 0 30px rgba(255, 110, 40, 0.22)); }
}

@media (prefers-reduced-motion: reduce) {
  /* Hold the glow steady rather than dropping it: the warmth is the design, the
     flicker is the decoration. */
  .brand h1 span:last-child {
    animation: none;
    filter: drop-shadow(0 0 12px rgba(255, 150, 60, 0.34))
            drop-shadow(0 0 28px rgba(255, 110, 40, 0.18));
  }
}

.panel {
  /* The sidebar is a flex column, so panels would otherwise be squeezed to fit
     and the tallest one — the word log — would lose most of its rows. Let them
     keep their full height and let the sidebar scroll instead. */
  flex: none;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 18px;
  padding: 14px 14px 10px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}

.panel h2 {
  margin: 0 0 12px;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.player-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.player-card {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 10px;
  padding: 10px 10px;
  border-radius: 12px;
  background: rgba(255,255,255,0.02);
  border: 1px solid rgba(255,255,255,0.04);
}

.player-badge {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 0.8rem;
  color: #08121d;
}

.player-card strong {
  display: block;
  font-size: 0.9rem;
}

.player-card span {
  display: block;
  font-size: 0.7rem;
  color: var(--muted);
}

.score-pill {
  background: rgba(118, 242, 195, 0.12);
  border: 1px solid rgba(118, 242, 195, 0.4);
  color: var(--accent);
  padding: 4px 8px;
  border-radius: 999px;
  font-weight: 700;
}

.rack-wrap {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.tile {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 10px;
  border: 1px solid rgba(255,255,255,0.12);
  background: linear-gradient(180deg, var(--tile-placed-2), var(--tile-placed));
  color: #1c2a39;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 1.2rem;
  cursor: pointer;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
  box-shadow: 0 6px 10px rgba(0,0,0,0.18);
}

@media (hover: hover) {
  .tile:hover {
    transform: translateY(-1px);
  }
}

.tile.selected {
  outline: 3px solid var(--tile-selected);
  box-shadow: 0 0 0 5px rgba(122,224,255,0.16);
}

.tile.used {
  background: rgba(255,255,255,0.04);
  border-style: dashed;
  color: rgba(255,255,255,0.2);
  cursor: default;
  box-shadow: none;
  opacity: 0.5;
}

.tile-letter { line-height: 1; }

.tile-value {
  position: absolute;
  right: 5px;
  bottom: 3px;
  font-size: 0.55rem;
  font-weight: 800;
  opacity: 0.6;
}

.rack-empty {
  color: var(--muted);
  font-size: 0.8rem;
  padding: 12px 2px;
}

.player-card.active {
  border-color: rgba(118, 242, 195, 0.5);
  background: rgba(118, 242, 195, 0.08);
}

.score-big {
  font-size: 2.4rem;
  font-weight: 800;
  line-height: 1;
  color: var(--accent);
  letter-spacing: -0.02em;
}

.stat-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.72rem;
  color: var(--muted);
}

.top-score {
  color: var(--accent-2);
  font-size: 0.85rem;
}

.reset-line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255,255,255,0.06);
  font-size: 0.72rem;
  color: var(--muted);
}

.countdown {
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
  color: var(--accent-2);
  letter-spacing: 0.04em;
}

.compose {
  display: flex;
  align-items: center;
  gap: 8px;
}

.anchor-chip {
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: linear-gradient(180deg, var(--tile-placed-2), var(--tile-placed));
  color: #1c2a39;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  box-shadow: 0 0 0 3px rgba(122,224,255,0.35);
}

.word-input {
  background: rgba(17, 28, 42, 0.92);
  border: 1px solid rgba(122,224,255,0.35);
  border-radius: 10px;
  padding: 9px 12px;
  color: var(--text);
  font: inherit;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  width: 230px;
  outline: none;
}

.word-input:focus {
  border-color: var(--tile-selected);
  box-shadow: 0 0 0 4px rgba(122,224,255,0.14);
}

.secondary-btn.small {
  padding: 8px 10px;
  font-size: 0.7rem;
}

.board-cell.drop-target {
  background: rgba(118, 242, 195, 0.22);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px rgba(118,242,195,0.5);
}

.tile.dragging {
  opacity: 0.4;
  transform: scale(0.94);
}

.tile[draggable="true"] { cursor: grab; }
.board-cell[draggable="true"] { cursor: grab; }

/* The square you clicked: where the word starts. */
.board-cell.cursor {
  box-shadow: 0 0 0 3px var(--tile-selected);
  z-index: 3;
}

/* The squares the word will run through, so the direction is never a guess. */
.board-cell.track {
  background: rgba(122, 224, 255, 0.09);
  border-color: rgba(122, 224, 255, 0.3);
  border-style: dashed;
}

.board-cell.track.placed {
  border-style: solid;
  box-shadow: inset 0 0 0 2px rgba(122,224,255,0.45);
}

.cursor-arrow {
  position: absolute;
  color: var(--tile-selected);
  font-size: 0.85rem;
  line-height: 1;
  pointer-events: none;
  text-shadow: 0 0 6px rgba(8,18,29,0.9);
}

.cursor-arrow.across { right: -1px; top: 50%; transform: translateY(-50%); }
.cursor-arrow.down { bottom: -3px; left: 50%; transform: translateX(-50%); }

.direction-btn {
  width: 38px;
  height: 38px;
  border-radius: 10px;
  background: rgba(122, 224, 255, 0.14);
  border: 1px solid rgba(122, 224, 255, 0.45);
  color: var(--tile-selected);
  font-size: 1.2rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.direction-btn:hover {
  background: var(--tile-selected);
  color: #08121d;
}

.word-log {
  /* No inner scroller: a two-row window inside an already scrolling sidebar is
     more fiddly than one long list. The log is capped at 60 words upstream. */
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.word-row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 0.78rem;
  padding: 4px 6px;
  border-radius: 7px;
  background: rgba(255,255,255,0.02);
}

.word-row strong { color: var(--accent); }

.preview-pill em {
  font-style: normal;
  opacity: 0.6;
  font-weight: 600;
}

.bag-line {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255,255,255,0.06);
  font-size: 0.7rem;
  color: var(--muted);
}

.net-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.7rem;
  color: var(--muted);
}

.net-line strong {
  margin-left: auto;
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.net-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--muted);
  flex: none;
}

.net-online .net-dot {
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(118,242,195,0.18);
}

.net-connecting .net-dot,
.net-retrying .net-dot {
  background: #f0c674;
  animation: pulse 1.4s ease-in-out infinite;
}

.net-offline .net-dot,
.net-rejected .net-dot { background: #e8836f; }

/* The AI count, under the live line. Quieter than it -- a hollow dot and a
   lighter weight -- because it is a footnote to who is here, not a second
   headline. */
.net-bots {
  margin-top: 4px;
  font-size: 0.66rem;
}

.net-bots strong {
  color: var(--muted);
  font-weight: 600;
}

.net-dot.ghost {
  background: transparent;
  border: 1px dashed var(--muted);
  width: 7px;
  height: 7px;
  box-sizing: border-box;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.25; }
}

/* The badge marking who played a word. It sits on the first tile of each play,
   not on every tile: one marker per word reads as attribution, a marker on
   every tile reads as clutter. Small enough that the letter underneath is still
   the thing you see first. */
.player-mark {
  position: absolute;
  top: -5px;
  left: -5px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  line-height: 1;
  z-index: 2;
  cursor: pointer;
  border: none;
  transition: transform 0.12s ease;
}

.player-mark:hover {
  transform: scale(1.25);
  z-index: 4;
}

/* Some of the accounts on this board are played by a machine. Saying so is the
   honest thing, but saying it loudly would put a label on half the tiles -- so
   it is a dashed edge instead of a solid one, plus the word "bot" in the title
   and anywhere there is room for a whole line. Close up you can tell; from
   across the board it is still just a badge. */
.player-mark.bot,
.builder-mark.bot {
  border: 1.5px dashed rgba(0, 0, 0, 0.45);
}

.bot-chip {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 999px;
  border: 1px dashed var(--muted);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  vertical-align: 1px;
}

/* Your own tiles, kept very quiet -- enough to pick your words out of a busy
   board at a glance, not enough to compete with the letters. */
.board-cell.mine.placed {
  box-shadow: inset 0 0 0 2px rgba(118, 242, 195, 0.55);
}

.history-card {
  width: min(340px, 90vw);
  text-align: left;
}

/* The words at this square, across and down. The badge on the board can only
   name one of them; this names both. */
.history-words {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 18px;
  padding-bottom: 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}

.history-word {
  display: flex;
  align-items: center;
  gap: 10px;
}

.history-word strong {
  font-size: 0.95rem;
  letter-spacing: 0.08em;
  color: var(--text);
}

/* Only the axis label, never the badge -- a bare `span` here would also
   match the builder mark and shove it out of the left column. */
.history-word .history-axis {
  margin-left: auto;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.builder-mark.opening {
  background: rgba(255,255,255,0.08);
  color: var(--muted);
}

.history-heading {
  margin: 0 0 10px;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--muted);
  font-weight: 700;
}

.history-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 16px;
}

.history-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.history-what {
  min-width: 0;
  flex: 1;
}

/* The name can be long enough to need an ellipsis; the bot chip beside it must
   not be squeezed or stretched by that, so the two share a flex row rather than
   sitting inside the same block. */
.history-who {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.history-who .bot-chip {
  margin-left: 0;
  flex: none;
}

.history-what strong {
  display: block;
  font-size: 0.82rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.history-what span {
  display: block;
  font-size: 0.72rem;
  color: var(--muted);
}

.history-score {
  margin-left: auto;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.history-empty {
  margin: 0 0 16px;
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--muted);
}

/* Legend for the badges on the board: initials alone identify a player only
   once you know whose they are. */
.builders {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.builder {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 0.78rem;
  color: var(--muted);
}

.builder-mark {
  width: 20px;
  height: 20px;
  flex: none;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.56rem;
  font-weight: 800;
  line-height: 1;
}

.builder-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.builder strong {
  margin-left: auto;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.builder.is-me .builder-name { color: var(--text); font-weight: 600; }

/* ---------------------------------------------------------------- account */

.account-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 0.72rem;
  color: var(--muted);
}

.account-line .who {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.link-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.account-line .link-btn { margin-left: auto; }
.link-btn:hover { text-decoration: underline; }

/* Shown where the rack would be, for someone watching without an account. */
.join-cta {
  padding: 4px 0 2px;
}

.join-cta p {
  margin: 0 0 12px;
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--muted);
}

.wide { width: 100%; }

/* ---------------------------------------------------------------- sign in */

.signin-backdrop {
  backdrop-filter: blur(6px);
  background: rgba(4, 8, 14, 0.76);
  padding: 20px;
}

.signin-card {
  position: relative;
  width: min(384px, 100%);
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  padding: 30px 30px 26px;
  border-radius: 20px;
  border: 1px solid rgba(255,255,255,0.1);
  background:
    radial-gradient(120% 90% at 50% -20%, rgba(118,242,195,0.13), transparent 60%),
    linear-gradient(180deg, rgba(21,33,50,0.98), rgba(9,15,25,0.99));
  box-shadow: 0 30px 80px rgba(0,0,0,0.6);
  animation: signin-in 0.22s ease-out;
}

@keyframes signin-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

.signin-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 30px;
  height: 30px;
  padding: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: 1.2rem;
  font-weight: 400;
  line-height: 1;
}

.signin-close:hover { background: rgba(255,255,255,0.08); color: var(--text); }

.signin-head { margin-bottom: 22px; }

.signin-head h2 {
  margin: 0 0 8px;
  font-size: 1.32rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
}

.signin-head p {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.55;
  color: var(--muted);
}

.google-btn,
.guest-btn,
.signin-submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  height: 44px;
  padding: 0 14px;
  border-radius: 11px;
  font-size: 0.88rem;
  font-weight: 600;
}

.google-btn {
  background: #fff;
  color: #1f2430;
  border: 1px solid rgba(255,255,255,0.9);
}

.google-btn:hover:not(:disabled) { background: #eef1f6; }

.google-mark { width: 17px; height: 17px; flex: none; }

.guest-btn {
  margin-top: 9px;
  background: rgba(255,255,255,0.06);
  color: var(--text);
  border: 1px solid rgba(255,255,255,0.1);
}

.guest-btn:hover:not(:disabled) { background: rgba(255,255,255,0.1); }

.divider {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 18px 0;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(182,201,217,0.65);
}

.divider::before, .divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: rgba(255,255,255,0.1);
}

.signin-card form {
  display: flex;
  flex-direction: column;
  gap: 13px;
}

.field { display: block; }

.field > span {
  display: block;
  margin-bottom: 6px;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

.field input {
  width: 100%;
  height: 42px;
  padding: 0 13px;
  border-radius: 11px;
  border: 1px solid rgba(255,255,255,0.12);
  background: rgba(255,255,255,0.045);
  color: var(--text);
  font: inherit;
  font-size: 0.88rem;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.field input::placeholder { color: rgba(182,201,217,0.45); }

.field input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(118,242,195,0.16);
}

.signin-submit {
  margin-top: 3px;
  background: linear-gradient(180deg, #7ce8bc, #42c79e);
  color: #071820;
  font-weight: 700;
}

.signin-submit:hover:not(:disabled) { filter: brightness(1.06); }

.signin-card button:disabled { opacity: 0.5; cursor: default; }

.signin-error {
  display: flex;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 10px;
  background: rgba(255,126,115,0.12);
  border: 1px solid rgba(255,126,115,0.38);
  color: #ffb3ab;
  font-size: 0.78rem;
  line-height: 1.45;
}

.signin-foot {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 20px;
  padding-top: 17px;
  border-top: 1px solid rgba(255,255,255,0.07);
  font-size: 0.8rem;
  color: var(--muted);
}

.preview-pill {
  background: rgba(118, 242, 195, 0.14);
  border: 1px solid rgba(118, 242, 195, 0.45);
  color: var(--accent);
  border-radius: 999px;
  padding: 8px 14px;
  font-weight: 700;
  font-size: 0.75rem;
  max-width: 420px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.preview-pill.invalid {
  background: rgba(255, 126, 115, 0.12);
  border-color: rgba(255, 126, 115, 0.45);
  color: var(--danger);
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(4, 8, 14, 0.68);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal {
  background: var(--panel-strong);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 18px;
  padding: 20px;
  box-shadow: 0 30px 70px var(--shadow);
  max-width: 340px;
}

.modal h3 {
  margin: 0 0 14px;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.letter-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
}

.letter-btn {
  padding: 8px 0;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  color: var(--text);
  border-radius: 8px;
  font-size: 0.85rem;
}

.letter-btn:hover {
  background: var(--tile-selected);
  color: #08121d;
}

button:disabled {
  opacity: 0.45;
  cursor: default;
}

.action-row {
  display: flex;
  gap: 10px;
  margin-top: 12px;
}

button {
  appearance: none;
  border: none;
  border-radius: 12px;
  padding: 10px 12px;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.primary-btn {
  background: linear-gradient(180deg, #7ce8bc, #42c79e);
  color: #071820;
}

.secondary-btn {
  background: rgba(255,255,255,0.06);
  color: var(--text);
  border: 1px solid rgba(255,255,255,0.08);
}

.main-stage {
  position: relative;
  background: linear-gradient(180deg, rgba(19,20,31,0.7), rgba(8,10,18,0.82));
  overflow: hidden;
}

.topbar {
  position: absolute;
  top: 18px;
  left: 18px;
  right: 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  z-index: 30;
  pointer-events: none;
}

.topbar > * {
  pointer-events: auto;
}

.title-pill {
  background: rgba(17, 28, 42, 0.82);
  border: 1px solid rgba(255,255,255,0.08);
  box-shadow: 0 10px 24px rgba(0,0,0,0.18);
  border-radius: 999px;
  padding: 8px 14px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 0.7rem;
}

.controls {
  display: flex;
  gap: 8px;
}

.board-viewport {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  /* The board handles its own dragging and pinching. Without this the browser
     claims both gestures first -- scrolling the page and zooming the document
     -- and cancels the pointer stream halfway through, which is what made
     moving the board on a phone feel like it barely worked. */
  touch-action: none;
  /* No blue flash and no text selection when a drag starts on a square. */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

.board-viewport.dragging {
  cursor: grabbing;
}

/* The cursor answers two different questions depending on what is happening.
 *
 * At rest it answers "what is under me": a letter or a badge is a thing you can
 * click, so pointer; empty space is somewhere to drag from, so grab. You can
 * drag from anywhere, but the cursor over a tile should still say the tile is
 * interactive, because that is the less obvious of the two.
 *
 * Once you are actually dragging it answers "what am I doing" instead, and the
 * answer is the same everywhere -- grabbing, whatever the pointer happens to be
 * over. One gesture, one cursor.
 */
/* Covers the viewport exactly so its centre and the viewport centre coincide --
   the cursor-anchored zoom maths in app.js depends on that. */
.board-surface {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform-origin: center center;
  will-change: transform;
}

.board-grid {
  display: grid;
  padding: 24px;
  background: rgba(255,255,255,0.03);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 22px;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02), 0 35px 80px rgba(0,0,0,0.4);
}

.board-cell {
  position: relative;
  width: 42px;
  height: 42px;
  padding: 0;
  border-radius: 10px;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--tile-border);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: rgba(255,255,255,0.85);
  user-select: none;
  transition: background-color 0.12s ease, border-color 0.12s ease, filter 0.12s ease;
}

/* Hover: just enough to confirm which square you are about to act on. A board
   with a few hundred tiles on it should not twitch as the cursor crosses it,
   so this stays under the threshold of "effect" and only reads as focus.
 *
 * Suppressed while dragging, where the cursor is sweeping across squares rather
 * than choosing one -- lighting each up in turn would be noise.
 *
 * A touch screen has no hover to speak of: it reports the last square tapped as
 * hovered and leaves it that way, so a phone would carry a lit square around
 * for the rest of the session. Hence the whole block is asked for. */
@media (hover: hover) {
  .board-viewport:not(.dragging) .board-cell:not(.placed):hover {
    border-color: rgba(255,255,255,0.2);
  }

  /* A tint laid over the square rather than a replacement background, so a
     premium square keeps its colour instead of flashing grey on the way past.
     Box-shadow would have done the same job, but the cursor ring and the track
     highlight are already using it. */
  .board-viewport:not(.dragging) .board-cell:not(.placed):hover::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: rgba(255,255,255,0.06);
    pointer-events: none;
  }

  .board-viewport:not(.dragging) .board-cell.placed:hover {
    filter: brightness(1.08);
  }

  .board-cell:hover {
    background: rgba(122,224,255,0.08);
  }
}

@media (prefers-reduced-motion: reduce) {
  .board-cell { transition: none; }
}

.board-cell {
  cursor: inherit; /* empty squares follow the viewport: grab */
}

.board-cell.placed,
.player-mark {
  cursor: pointer;
}

.cell-letter,
.cell-value,
.cell-premium {
  cursor: inherit;
}

.board-viewport.dragging .board-cell,
.board-viewport.dragging .board-cell.placed,
.board-viewport.dragging .player-mark {
  cursor: grabbing;
}

/* Premium squares are generated from the coordinates, so they continue
   forever in every direction. */
.premium-dl { background: rgba(122, 224, 255, 0.14); border-color: rgba(122,224,255,0.28); }
.premium-tl { background: rgba(122, 224, 255, 0.26); border-color: rgba(122,224,255,0.45); }
.premium-dw { background: rgba(255, 141, 177, 0.16); border-color: rgba(255,141,177,0.32); }
.premium-tw { background: rgba(255, 141, 177, 0.3); border-color: rgba(255,141,177,0.52); }

.cell-premium {
  font-size: 0.6rem;
  letter-spacing: 0.04em;
  color: rgba(255,255,255,0.6);
  font-weight: 800;
}

.board-cell.placed {
  background: linear-gradient(180deg, var(--tile-placed-2), var(--tile-placed));
  color: #111b29;
  border-color: rgba(0,0,0,0.16);
}

.board-cell.staged {
  background: linear-gradient(180deg, #9df3d4, var(--accent));
  border-color: rgba(255,255,255,0.5);
  box-shadow: 0 0 0 2px rgba(118,242,195,0.35);
}

/* A tile someone else just played. The board can change while you are looking
   away from that corner of it, so new letters announce themselves rather than
   simply being there the next time you glance over. */
.board-cell.fresh {
  animation: land 2.2s ease-out;
  z-index: 1;
}

@keyframes land {
  0%   { box-shadow: 0 0 0 0 rgba(118,242,195,0.9); transform: scale(1.18); }
  35%  { box-shadow: 0 0 0 6px rgba(118,242,195,0.28); transform: scale(1); }
  100% { box-shadow: 0 0 0 0 rgba(118,242,195,0); transform: scale(1); }
}

/* Ours, on the board but not yet acknowledged. Deliberately subtle -- the play
   almost always lands, so this should read as "in flight", not as a warning. */
.board-cell.unconfirmed {
  opacity: 0.72;
}

@media (prefers-reduced-motion: reduce) {
  .board-cell.fresh { animation: none; box-shadow: 0 0 0 3px rgba(118,242,195,0.5); }
}

.cell-letter {
  font-size: 1.1rem;
  line-height: 1;
}

.cell-value {
  position: absolute;
  right: 4px;
  bottom: 2px;
  font-size: 0.52rem;
  font-weight: 800;
  opacity: 0.65;
}

.word-banner {
  position: absolute;
  bottom: 22px;
  right: 22px;
  background: rgba(12, 20, 29, 0.8);
  border-radius: 14px;
  border: 1px solid rgba(118,242,195,0.35);
  box-shadow: 0 10px 22px rgba(0,0,0,0.22);
  padding: 12px 14px;
  max-width: 330px;
  z-index: 20;
}

.word-banner strong {
  color: var(--accent);
  display: block;
  margin-bottom: 4px;
  font-size: 0.76rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.word-banner span {
  color: var(--text);
  font-weight: 600;
}

/* ----------------------------------------------------------------- mobile */

/* Everything below only exists on a phone. Off that, the drawer chrome and the
   heads-up strip are not rendered at all. */
.drawer-close,
.mobile-hud,
.scrim {
  display: none;
}

@media (max-width: 980px) {
  /* The stage is a column of fixed-height chrome with the board taking
     whatever is left, rather than absolutely positioned panels stacked on top
     of each other. On a screen this narrow the overlapping version put the
     title pill, the reset button and the sidebar in the same 200 pixels. */
  .app-shell {
    grid-template-columns: minmax(0, 1fr);
  }

  .main-stage {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .topbar,
  .word-banner,
  .mobile-hud {
    position: static;
    flex: none;
  }

  .board-viewport {
    position: relative;
    inset: auto;
    flex: 1 1 auto;
    /* A flex item will not shrink below its content without this, and the
       board is as tall as the day's tiles make it. */
    min-height: 0;
  }

  /* ---- the sidebar, as a drawer ---- */

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: min(330px, 86vw);
    z-index: 50;
    border-right: 1px solid rgba(255,255,255,0.08);
    box-shadow: 24px 0 60px rgba(0,0,0,0.5);
    background: rgba(9, 14, 22, 0.98);
    padding: 16px 14px calc(18px + env(safe-area-inset-bottom));
    transform: translateX(-100%);
    /* Closed, it must not be in the way of a tap on the board behind it --
       but the hiding waits for the slide to finish, or closing it would look
       like the drawer being deleted rather than put away. */
    visibility: hidden;
    transition:
      transform 0.24s cubic-bezier(0.32, 0.72, 0, 1),
      visibility 0s linear 0.24s;
  }

  .sidebar.open {
    transform: none;
    visibility: visible;
    transition:
      transform 0.24s cubic-bezier(0.32, 0.72, 0, 1),
      visibility 0s;
  }

  .scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 45;
    background: rgba(4, 8, 14, 0.6);
    backdrop-filter: blur(2px);
    animation: scrim-in 0.2s ease-out;
  }

  @keyframes scrim-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  .brand h1 { margin-right: auto; }

  /* The drawer pads 16/14 rather than 18, so the sticky header's bleed has to
     match or panels show through the gutters as they scroll under it. */
  .brand {
    margin: -16px -14px 0;
    padding: 16px 14px 10px;
  }

  .drawer-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    margin-left: 4px;
    border-radius: 50%;
    background: rgba(255,255,255,0.06);
    color: var(--muted);
    font-size: 0.85rem;
    line-height: 1;
  }

  /* ---- the strip above the board ---- */

  /* What you are holding and what you have scored, always visible. The drawer
     has the rest; these two are the ones you check between every word, and
     opening a drawer to see your own tiles is not a game. */
  .mobile-hud {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: calc(8px + env(safe-area-inset-top)) 12px 8px;
    background: rgba(8, 13, 21, 0.9);
    border-bottom: 1px solid rgba(255,255,255,0.07);
  }

  .hud-nav {
    width: 38px;
    height: 38px;
    flex: none;
    padding: 0;
    border-radius: 11px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.1);
    color: var(--text);
    font-size: 1.05rem;
    line-height: 1;
  }

  .hud-score {
    display: flex;
    align-items: baseline;
    gap: 7px;
    min-width: 0;
  }

  .hud-score strong {
    font-size: 1.4rem;
    font-weight: 800;
    line-height: 1;
    color: var(--accent);
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
  }

  .hud-score span {
    font-size: 0.68rem;
    color: var(--muted);
    white-space: nowrap;
  }

  /* The connection light, and nothing else from that line -- the words beside
     it live in the drawer. Green here means your plays are landing. */
  .hud-net {
    flex: none;
    display: flex;
    align-items: center;
  }

  .hud-reset {
    width: 36px;
    height: 36px;
    flex: none;
    padding: 0;
    border-radius: 11px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.1);
    color: var(--muted);
    font-size: 0.95rem;
    line-height: 1;
  }

  .hud-join { flex: 1 1 100%; }

  /* Wide enough to be worth a row of its own, and allowed to take one: held
     upright there is no room beside the score and the buttons, so it wraps,
     while a phone on its side has width to spare and it stays inline. */
  .hud-rack {
    flex: 1 1 260px;
    display: flex;
    gap: 6px;
    overflow-x: auto;
    padding-bottom: 2px;
    /* Sideways within the rack only -- this must not become another way to
       scroll the page. */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .hud-rack::-webkit-scrollbar { display: none; }

  .hud-rack .tile {
    width: 40px;
    height: 40px;
    flex: none;
    font-size: 1.1rem;
  }

  .hud-actions {
    display: flex;
    gap: 6px;
    flex: none;
    margin-left: auto;
  }

  .hud-actions button {
    padding: 9px 13px;
    font-size: 0.78rem;
  }

  /* ---- the row that composes a word ---- */

  /* Only while a word is being built. Idle it held nothing but a button, and
     a permanent empty row is exactly the kind of thing that leaves a phone
     showing three squares of a board. */
  .topbar:not(.busy) { display: none; }

  .topbar {
    flex-wrap: wrap;
    gap: 8px;
    padding: 8px 12px;
    border-bottom: 1px solid rgba(255,255,255,0.06);
  }

  /* The prompt to click a square is a desktop nicety; on a phone it is a line
     of instructions permanently occupying the space the board wants. */
  .title-pill { display: none; }

  /* Nothing being composed means nothing to show -- an empty flex row would
     still cost the gap around it. */
  .compose:not(.composing) { display: none; }
  .compose.composing { flex: 1 1 100%; }
  .compose .word-input { flex: 1; width: auto; min-width: 0; }

  .controls {
    flex: 1 1 100%;
    align-items: center;
  }

  .preview-pill {
    flex: 1;
    min-width: 0;
    max-width: none;
    padding: 7px 12px;
  }

  /* The strip above the board carries this one on a phone, where it has to
     stay reachable even when nothing is being composed. */
  .reset-view { display: none; }

  /* ---- the live feed ---- */

  /* Full width along the bottom instead of a floating card: a card in the
     corner of a phone covers the board, and this is a running commentary
     rather than something you act on. */
  .word-banner {
    max-width: none;
    border-radius: 0;
    border: none;
    border-top: 1px solid rgba(118,242,195,0.28);
    box-shadow: none;
    padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
    display: flex;
    align-items: baseline;
    gap: 8px;
  }

  .word-banner strong {
    margin-bottom: 0;
    flex: none;
    font-size: 0.6rem;
  }

  .word-banner span {
    font-size: 0.76rem;
    line-height: 1.35;
    /* Two lines at most. The messages are one sentence, and a long one must
       not be allowed to eat the board. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* A modal on a phone is the whole screen's business. */
  .modal { width: min(340px, calc(100vw - 32px)); }
  .history-card { width: min(340px, calc(100vw - 32px)); }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
  .scrim { animation: none; }
}
