/* Dark for everyone to begin with, whatever their device is set to, at
   Tahseen's word (2026-11-12). Light only when the visitor picks it with the
   toggle (theme.js puts data-theme on <html> and remembers it). */
:root {
  color-scheme: dark;
  --background: #131312;
  --text: #ecebe6;
  --muted: #8f8e88;
  --surface: #1a1a19;
  --field: #1a1a19;
  --line: #2a2a28;
  --line-strong: #3a3a36;
  --quiet: #858480;
  --live: #5bd18b;
  --denied: #e8a598;
  --sun: block;
  --moon: none;
  --button: #2a2a28;
  --button-hover: #323230;
  --button-line: #3a3a36;
  --button-text: #ecebe6;
}

:root[data-theme="light"] {
  color-scheme: light;
  --background: #f5f4ef;
  --text: #161615;
  --muted: #66655f;
  --surface: #ffffff;
  --field: #f5f4ef;
  --line: #e2e0d9;
  --line-strong: #cfcdc5;
  --quiet: #72706a;
  --live: #1f9d55;
  --denied: #b3412e;
  --sun: none;
  --moon: block;
  --button: #f3f2ed;
  --button-hover: #ebeae4;
  --button-line: #cfcdc5;
  --button-text: #161615;
}

* {
  box-sizing: border-box;
}

html {
  background: var(--background);
  color: var(--text);
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent; /* a phone's grey flash on a tap; hover and focus styles do that job here */
}

/* Selected text in the page's own ink, not the system's blue. */
::selection {
  background: color-mix(in srgb, var(--text) 18%, transparent);
}

/* Headings and short centred lines break evenly; running text avoids a last word alone. */
h1,
.note {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

body {
  margin: 0;
  min-height: 100svh;
  display: grid;
  /* Sit a little above the middle, where the eye expects the centre; the
     footer at the bottom, in the same place on every page. A page taller
     than the window still keeps some room above and below it. */
  grid-template-rows: minmax(clamp(20px, 5vw, 40px), 1fr) auto minmax(clamp(20px, 5vw, 40px), 1.35fr) auto;
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

main {
  grid-row: 2;
  width: min(100% - 48px, 32rem);
  margin-inline: auto;
}

h1 {
  margin: 0 0 16px;
  font-size: 22px;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.012em;
}

a {
  display: inline-flex;
  align-items: center;
  color: var(--muted);
  text-decoration: none;
  transition: color 160ms ease;
}

a:hover,
a:focus-visible {
  color: var(--text);
}

a:focus-visible {
  outline: 1px solid var(--muted);
  outline-offset: 2px;
}

/* Pages of running text, like /privacy. */
.text p {
  margin: 0 0 14px;
  color: var(--muted);
}

/* The /gtm sign-in card. */
.card {
  width: min(100% - 32px, 360px);
  padding: 36px 32px 32px;
  border: 1px solid var(--line);
  background: var(--surface);
  text-align: center;
}

.card h1 {
  margin-bottom: 6px;
}

/* With no note under the heading, the card's first control keeps its distance. */
.card h1 + .button,
.card h1 + .or,
.card h1 + .form {
  margin-top: 24px;
}

.note {
  margin: 0 0 24px;
  color: var(--muted);
  font-size: 14px;
}

.note + .note {
  margin-top: -16px;
}

/* A quiet line at the foot of a card. */
.aside {
  margin: 20px 0 0;
  color: var(--muted);
  font-size: 12px;
}

.aside a {
  display: inline;
  color: var(--text);
}

/* The address a code went to, on a line of its own. */
.note .who {
  display: block;
  color: var(--text);
}

.denied {
  color: var(--denied);
}

/* Under a card: the way back to tahseenomar.com, and the theme toggle. */
/* Every page's footer: a line, then "Sign in" or "← Home" and the theme
   toggle, at the foot of the page, the same width and place on every page,
   so it never moves from one page to the next. */
body > footer {
  grid-row: 4;
  display: flex;
  align-items: center;
  gap: 20px;
  width: min(100% - 48px, 44rem);
  margin-inline: auto;
  padding: 20px 0 28px;
  border-top: 1px solid var(--line);
}

.foot-link,
.signin {
  padding: 10px 0;
  margin: -10px 0;
  font-size: 13px;
}

/* The card's one button, in Google's own sign-in button style for each mode. */
.button {
  justify-content: center;
  gap: 10px;
  width: 100%;
  height: 44px;
  border: 1px solid var(--button-line);
  background: var(--button);
  color: var(--button-text);
  font-size: 14px;
  font-weight: 500;
  transition: background 160ms ease;
}

.button:hover,
.button:focus-visible {
  color: var(--button-text);
  background: var(--button-hover);
}

/* The email sign-in: a divider, then the address or code and its button. */
.or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 20px 0;
  color: var(--quiet);
  font-size: 12px;
}

.or::before,
.or::after {
  content: "";
  flex: 1;
  border-top: 1px solid var(--line);
}

.form {
  display: grid;
  gap: 10px;
  margin: 0;
}

.field {
  width: 100%;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  background: var(--field);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  text-align: center;
  appearance: none;
}

.field::placeholder {
  color: var(--quiet);
}

.field:focus {
  border-color: var(--muted);
  outline: none;
}

.code {
  height: 56px;
  font-size: 26px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.5em;
  text-indent: 0.5em;
}

.code::placeholder {
  font-size: 15px;
  letter-spacing: 0.5em;
}

button.button {
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
}

.switch {
  margin-top: 18px;
  font-size: 13px;
}

/* Under the code: send a new one, or start over with another address. */
.switches {
  display: flex;
  justify-content: center;
  gap: 24px;
  margin: 20px 0 0;
}

.switches .switch {
  margin: 0;
}

.g {
  width: 18px;
  height: 18px;
}

.status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0 0 16px;
  color: var(--muted);
  font-size: 13px;
}

.dot {
  width: 8px;
  height: 8px;
  background: var(--quiet);
}

.dot.live {
  background: var(--live);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--live) 15%, transparent);
}

/* Shown on the signed-in page until the first map is published. */
.soon {
  padding: 18px 16px;
  border: 1px dashed var(--line-strong);
}

.soon .status {
  margin-bottom: 6px;
  color: var(--text);
  font-size: 14px;
}

.soon .note {
  margin: 0;
}

.signout {
  display: flex;
  justify-content: center;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
  font-size: 13px;
}

/* Light or dark, beside each page's other links; theme.js shows it and
   remembers the choice. A thumb-sized target that takes only its icon's room. */
.theme {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: -14px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 160ms ease;
}

.theme[hidden] {
  display: none;
}

.theme:hover,
.theme:focus-visible {
  color: var(--text);
}

.theme:focus-visible {
  outline: 1px solid var(--muted);
  outline-offset: -6px;
}

.theme svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
}

.sun {
  display: var(--sun);
}

.moon {
  display: var(--moon);
}

nav {
  display: flex;
  align-items: center;
  gap: 20px;
}

/* A bigger tap target without moving anything. */
nav a {
  padding: 10px;
  margin: -10px;
}

.logo {
  fill: currentColor;
}

/* LinkedIn's filled square reads heavier than the open X, so it is drawn smaller. */
.linkedin {
  width: 15px;
  height: 15px;
}

.x {
  width: 16px;
  height: 16px;
}

/* The name slides out of its logo on hover, pushing the next logo along. */
.label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 0;
  margin-left: 0;
  overflow: hidden;
  opacity: 0;
  font-size: 13px;
  white-space: nowrap;
  transition: max-width 240ms ease, margin-left 240ms ease, opacity 200ms ease;
}

a:hover .label,
a:focus-visible .label,
.labelled .label {
  max-width: 6rem;
  margin-left: 8px;
  opacity: 1;
}

/* Comparison page /2 shows the names all the time. */
.labelled {
  gap: 26px;
}

.arrow {
  width: 8px;
  height: 8px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (max-width: 480px) {
  .linkedin {
    width: 17px;
    height: 17px;
  }

  .x {
    width: 18px;
    height: 18px;
  }
}

@media (prefers-reduced-motion: reduce) {
  a,
  .theme,
  .label {
    transition: none;
  }
}

/* Signed-in pages: your apps, and admin. Wider cards, left-aligned. */
.card.wide {
  width: min(100% - 32px, 640px);
  text-align: left;
}

.card.wide h1 {
  margin-bottom: 4px;
}

.card.wide h2 {
  margin: 32px 0 12px;
  font-size: 13px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}

.tile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  min-height: 112px;
  padding: 18px;
  border: 1px solid var(--line-strong);
  background: var(--field);
  color: var(--text);
  transition: border-color 160ms ease;
}

a.tile:hover,
a.tile:focus-visible {
  border-color: var(--muted);
  color: var(--text);
}

.tile-name {
  font-size: 15px;
  font-weight: 500;
}

.tile-text {
  color: var(--muted);
  font-size: 13px;
  line-height: 1.45;
}

.tile-open {
  margin-top: auto;
  color: var(--muted);
  font-size: 12px;
}

.tile.waiting {
  border-style: dashed;
}

.card-links {
  display: flex;
  gap: 20px;
  margin-top: 28px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: 13px;
}

.card:not(.wide) .card-links {
  justify-content: center;
}

.note a {
  color: var(--muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Admin: one row per person; "Manage" opens their settings beneath it. Every
   panel shares one grid, a label column and a control column, so the labels
   line up with the names above them and the controls with each other. */
.admin .note {
  margin-bottom: 24px;
}

.notice {
  margin: 0 0 16px;
  padding: 2px 0 2px 12px;
  border-left: 2px solid var(--muted);
  font-size: 13px;
}

.notice.denied {
  border-left-color: var(--denied);
  color: var(--denied);
}

.panel .notice {
  margin: 0;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 36px;
  padding: 0 14px;
  border: 1px solid var(--line-strong);
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease;
}

.btn:hover {
  border-color: var(--muted);
}

.btn.primary {
  border-color: var(--button-line);
  background: var(--button);
  color: var(--button-text);
  font-weight: 500;
}

.btn.primary:hover {
  background: var(--button-hover);
}

.btn.danger {
  color: var(--denied);
}

.btn.quiet-danger {
  padding: 0 0 0 8px;
  border-color: transparent;
  color: var(--muted);
}

.btn.quiet-danger:hover {
  border-color: transparent;
  color: var(--denied);
}

/* A keyboard user always sees where they are. Text fields show it with their border. */
:where(button, summary, select, input[type="checkbox"]):focus-visible {
  outline: 1px solid var(--text);
  outline-offset: 2px;
}

details > summary {
  list-style: none;
  cursor: pointer;
}

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

details:not([open]) > summary .open-label,
details[open] > summary .closed-label {
  display: none;
}

.add {
  margin-bottom: 24px;
}

.add > summary {
  width: max-content;
}

/* Open, the button reads "Cancel" and steps back, leaving "Add person" the one loud button. */
.add[open] > summary {
  border-color: var(--line-strong);
  background: transparent;
  color: var(--muted);
  font-weight: 400;
}

.people {
  border-top: 1px solid var(--line);
}

.row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px 128px 56px;
  align-items: center;
  gap: 16px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}

.row.head {
  padding: 10px 0;
  color: var(--muted);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.person[open] > summary.row {
  border-bottom-color: transparent;
}

.who {
  display: grid;
  gap: 3px;
  min-width: 0;
}

.who-name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 500;
}

.who-mail {
  overflow: hidden;
  color: var(--muted);
  font-size: 12px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row-apps {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.row-apps .quiet {
  font-size: 12px;
}

.row-seen {
  color: var(--muted);
  font-size: 12px;
}

.row-manage {
  justify-self: end;
  color: var(--muted);
  font-size: 12px;
}

.person > summary.row:hover .row-manage,
.person[open] > summary.row .row-manage {
  color: var(--text);
}

.person.off .who,
.person.off .row-apps {
  opacity: 0.55;
}

.pill {
  display: inline-flex;
  align-items: center;
  height: 18px;
  padding: 0 6px;
  border: 1px solid var(--line-strong);
  color: var(--muted);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.pill.off {
  border-color: var(--denied);
  color: var(--denied);
}

.panel {
  margin: 0;
  padding: 4px 0 24px;
  border-bottom: 1px solid var(--line);
}

.add > .panel {
  padding: 12px 0 8px;
  border-bottom: 0;
}

.setting {
  display: grid;
  grid-template-columns: 104px minmax(0, 448px);
  align-items: start;
  gap: 0 24px;
  padding: 8px 0;
}

.setting-label {
  padding-top: 10px;
  color: var(--muted);
  font-size: 11px;
  font-weight: 500;
  line-height: 16px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.setting-control {
  display: grid;
  gap: 8px;
  min-width: 0;
}

.setting-control > .btn {
  justify-self: start;
}

.setting.access {
  margin-top: 12px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

.inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.inline form {
  margin: 0;
}

/* Admin fields sit on the card, not sunk into it. */
.field.slim,
.select {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  text-align: left;
}

.select {
  padding: 0 6px;
}

.addresses {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.address {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 128px 72px;
  align-items: center;
  gap: 8px;
  font-size: 13px;
}

.address-email {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.address form {
  justify-self: end;
  margin: 0;
}

.choices {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 20px;
  min-height: 36px;
}

.choice {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
}

/* "Remove person": a quiet link-like button that opens its own confirmation. */
.confirm > summary.btn {
  width: max-content;
  padding: 0;
}

.confirm[open] > summary {
  color: var(--text);
}

.confirm-body {
  margin-top: 8px;
  padding: 12px 0 0 12px;
  border-left: 2px solid var(--denied);
  font-size: 13px;
}

.confirm-body p {
  margin: 0 0 10px;
  color: var(--muted);
}

.activity {
  margin-top: 24px;
}

.activity > summary {
  width: max-content;
  padding: 10px 0;
  margin: -10px 0;
  color: var(--muted);
  font-size: 12px;
}

.activity > summary::before {
  content: "+ ";
}

.activity[open] > summary::before {
  content: "− ";
}

.log {
  display: grid;
  gap: 8px;
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
  font-size: 12px;
  color: var(--muted);
}

.log li {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  gap: 12px;
}

.quiet {
  color: var(--quiet);
}

/* The tables of /admin/visits: plain rows on the card's own lines, numbers to the right. */
.stats-wrap {
  overflow-x: auto;
}

.stats {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.stats th,
.stats td {
  padding: 9px 0 9px 16px;
  border-bottom: 1px solid var(--line);
  text-align: right;
  font-weight: 400;
  white-space: nowrap;
}

.stats th:first-child {
  padding-left: 0;
  text-align: left;
}

.stats thead th {
  color: var(--muted);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.stats td {
  color: var(--text);
}

@media (max-width: 560px) {
  .row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 6px 12px;
  }

  .row.head {
    display: none;
  }

  .row-apps,
  .row-seen {
    grid-column: 1;
  }

  .row-manage {
    grid-column: 2;
    grid-row: 1;
  }

  /* The column heads are hidden here, so the date says what it is. */
  .row-seen::before {
    content: "Last signed in · ";
  }

  .setting {
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
  }

  .setting-label {
    padding-top: 0;
  }

  .setting-label:empty {
    display: none;
  }

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

  /* With no Remove beside it, the choice runs the full width, like the fields. */
  .address:has(> span:empty) .select {
    grid-column: 1 / -1;
  }

  .address > span:empty {
    display: none;
  }

  .address-email {
    grid-column: 1 / -1;
  }

  .btn,
  .field.slim,
  .select {
    height: 40px;
  }

  .log li {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  /* Small text links get a thumb-sized target without moving. */
  .card-links a,
  .card-links button.link,
  .switches .switch,
  .activity > summary {
    padding: 12px 0;
    margin: -12px 0;
  }
}

/* A form button that reads as a link, for sign-out among links. */
button.link.plain {
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

button.link.plain:hover,
button.link.plain:focus-visible {
  color: var(--text);
}

/* The homepage: the name large on one left edge with its two links beneath,
   and the way in and the theme toggle in a quiet footer at the bottom. */
body.home {
  grid-template-rows: minmax(clamp(20px, 5vw, 40px), 1fr) auto minmax(clamp(20px, 5vw, 40px), 1fr) auto;
}

.home main {
  width: min(100% - 48px, 44rem);
}

.home h1 {
  margin: 0 0 24px;
  font-size: clamp(34px, 9vw, 44px);
  line-height: 1.1;
  letter-spacing: -0.03em;
}

.home .links {
  gap: 28px;
}

.home .links a {
  gap: 5px;
  padding: 10px 0;
  margin: -10px 0;
  color: var(--text);
  font-size: 15px;
}

.home .links a:hover,
.home .links a:focus-visible {
  color: var(--muted);
}

.home .links .arrow {
  width: 9px;
  height: 9px;
}

/* The homepage's game (game.js): drawn across the whole page, behind the
   name and links, in the page's own background and ink. A click anywhere
   on the page starts it; while it runs the page's text fades away. It needs a
   keyboard and a mouse, so it is left out on touch screens and small
   windows; game.js also leaves it out where WebGL is missing. */
.home .game {
  position: fixed;
  inset: 0;
  z-index: 0;
  user-select: none;
  -webkit-user-select: none;
}

.home main,
.home footer {
  position: relative;
  z-index: 1;
  transition: opacity 0.3s ease;
}

.home.in-game main,
.home.in-game footer {
  opacity: 0;
  pointer-events: none;
}

.game canvas {
  display: block;
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
  pointer-events: none;
}

.in-game .game canvas {
  pointer-events: auto;
  cursor: none;
}

.game-hud {
  position: absolute;
  inset: 24px;
  display: none;
  color: var(--muted);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

.in-game .game-hud {
  display: block;
}

.game-score,
.game-wave {
  position: absolute;
  top: 0;
}

.game-score {
  left: 0;
}

.game-wave {
  right: 0;
}

/* Under the keys, where a bar once was: thirty small hearts, side by side
   the whole width of the keys above them. A whole heart is ink, an empty
   one a faint outline of itself. */
.game-hearts {
  display: flex;
  justify-content: space-between;
}

.heart {
  width: 17.5px;
  height: 15px;
}

.heart .empty {
  fill: var(--line-strong);
}

.heart .half,
.heart .whole {
  fill: var(--text);
  display: none;
}

.heart.half .half,
.heart.whole .whole {
  display: block;
}

.game-hearts.hit .heart {
  animation: game-hit 0.3s ease;
}

.game-hearts.low .heart {
  animation: game-low 0.5s ease-in-out infinite alternate;
}

.game-hearts.low .heart:nth-child(even) {
  animation-delay: 0.25s;
}

/* Recovering: the heart that is coming back breathes. */
.game-hearts .heart.mending {
  animation: game-heal 1.1s ease-in-out infinite;
}

.heart.mending .empty {
  fill: var(--muted);
}

@keyframes game-hit {
  30% {
    transform: translateY(-3px);
  }
}

@keyframes game-low {
  to {
    transform: translateY(-1.5px);
  }
}

@keyframes game-heal {
  50% {
    opacity: 0.4;
  }
}

/* Your air, only while you are under water: a row of bubbles above the keys' right end. */
.game-air {
  position: absolute;
  right: 0;
  bottom: calc(100% + 12px);
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.3s ease;
}

.game-air.on {
  opacity: 1;
}

.bubble {
  width: 11px;
  height: 11px;
  fill: none;
  stroke: var(--text);
  stroke-width: 1.2;
}

.bubble.gone {
  stroke: var(--line-strong);
}

.game-cross {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 13px;
  height: 13px;
  margin: -6px 0 0 -6px;
  mix-blend-mode: difference;
}

.game-cross::before,
.game-cross::after {
  content: "";
  position: absolute;
  background: #fff;
}

.game-cross::before {
  top: 0;
  left: 6px;
  width: 1px;
  height: 13px;
}

.game-cross::after {
  top: 6px;
  left: 0;
  width: 13px;
  height: 1px;
}

.game-banner {
  position: absolute;
  top: 18%;
  right: 0;
  left: 0;
  color: var(--text);
  font-size: 15px;
  text-align: center;
  opacity: 0;
  transition: opacity 0.3s;
}

.game.banner .game-banner {
  opacity: 1;
}

/* Dying: "You died!" over the fading world, with the score; the controls step away. */
.game-dead {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-shadow: 0 0 4px var(--background), 0 0 10px var(--background);
  opacity: 0;
  transition: opacity 0.6s ease;
  pointer-events: none;
}

.game-dead strong {
  color: var(--text);
  font-size: 44px;
  font-weight: 500;
  letter-spacing: -0.03em;
}

.game-dead span {
  color: var(--muted);
  font-size: 14px;
}

.game.dead .game-dead {
  opacity: 1;
  transition-delay: 0.35s;
}

.game-help,
.game-corner,
.game-cross,
.game-score,
.game-wave {
  transition: opacity 0.4s ease;
}

.game.dead .game-help,
.game.dead .game-corner,
.game.dead .game-cross,
.game.dead .game-score,
.game.dead .game-wave {
  opacity: 0;
}

.game-sound .sound-off,
.game-sound.muted .sound-on {
  display: none;
}

.game-sound.muted .sound-off {
  display: block;
}

/* While playing, in the corner opposite the hearts: the theme (T) and
   the sound (M), each with its icon. */
.game-corner {
  position: absolute;
  right: 0;
  bottom: -6px;
  display: flex;
  align-items: center;
  gap: 18px;
  font-size: 12px;
}

.game-theme,
.game-sound {
  display: flex;
  align-items: center;
  gap: 6px;
}

.game-theme svg,
.game-sound svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linejoin: round;
}

/* While a game can start, the page is a crosshair, with "Click to play"
   beside it wherever a click would start one. */
.playable {
  cursor: crosshair;
}

/* Over the world, the page's small words are drawn in full ink with a halo
   of the page's own colour, so they read against whatever is behind them. */
.playable .signin,
.playable .theme {
  color: var(--text);
}

.playable .signin,
.playable .links a {
  text-shadow: 0 0 3px var(--background), 0 0 8px var(--background), 0 0 14px var(--background);
}

.playable .theme svg {
  filter: drop-shadow(0 0 3px var(--background)) drop-shadow(0 0 7px var(--background));
}

/* The footer's line, likewise: a good share of the ink, edged above and
   below with the page's colour, so it shows over light and dark parts of
   the world alike. */
.playable > footer {
  border-top-color: color-mix(in srgb, var(--text) 42%, transparent);
  box-shadow: 0 -1px 0 color-mix(in srgb, var(--background) 55%, transparent), inset 0 1px 0 color-mix(in srgb, var(--background) 55%, transparent);
}

.playable .signin:hover,
.playable .signin:focus-visible,
.playable .theme:hover,
.playable .theme:focus-visible {
  color: var(--muted);
}

.playable a,
.playable button {
  cursor: pointer;
}

.game-hint {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 2;
  color: var(--text);
  font-size: 13px;
  white-space: nowrap;
  text-shadow: 0 0 3px var(--background), 0 0 6px var(--background);
  pointer-events: none;
  opacity: 0;
  transition: opacity 160ms ease;
}

.game-hint.on {
  opacity: 1;
}

/* The controls while playing: keycaps drawn like the page, bottom left,
   just above the hearts. */
.game-help {
  position: absolute;
  bottom: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  font-size: 12px;
  text-shadow: 0 0 3px var(--background), 0 0 6px var(--background);
}

.help-keys {
  display: flex;
  align-items: flex-end;
  gap: 22px;
}

.keys-move {
  display: grid;
  grid-template-columns: repeat(3, 28px);
  gap: 4px;
}

.keys-move .key:nth-child(1) {
  grid-area: 1 / 2;
}

.keys-move .key:nth-child(2) {
  grid-area: 2 / 1;
}

.keys-move .key:nth-child(3) {
  grid-area: 2 / 2;
}

.keys-move .key:nth-child(4) {
  grid-area: 2 / 3;
}

.help-rest {
  display: flex;
  align-items: center;
  gap: 22px;
  height: 28px;
}

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

.key {
  display: grid;
  place-items: center;
  min-width: 28px;
  height: 28px;
  padding: 0 7px;
  box-sizing: border-box;
  border: 1px solid var(--line-strong);
  background: color-mix(in srgb, var(--background) 80%, transparent);
  color: var(--text);
  font: inherit;
  font-size: 11px;
  line-height: 1;
}

.keys-move .key {
  grid-template-rows: auto auto;
  gap: 1px;
  padding: 0;
}

.key i {
  color: var(--muted);
  font-size: 9px;
  font-style: normal;
}

.key.space {
  width: 120px;
}

/* Held down: the key lights up in the page's ink. */
.key {
  transition: background 80ms ease, color 80ms ease, border-color 80ms ease;
}

.key.down {
  border-color: var(--text);
  background: var(--text);
  color: var(--background);
  text-shadow: none;
}

.key.down i {
  color: var(--background);
}

.mouse.down {
  filter: drop-shadow(0 0 4px var(--text));
}

.mouse {
  width: 14px;
  height: 21px;
  fill: none;
  stroke: var(--text);
  stroke-width: 1.2;
}

.mouse .click {
  fill: var(--text);
  stroke: none;
}

.game-hint small {
  display: block;
  color: var(--muted);
  font-size: 12px;
}

/* The homepage's noughts and crosses (noughts.js), shown only where the world is not. */
.home .ttt {
  display: none;
  margin-top: 52px;
}

.home .ttt-stage {
  margin: 0 0 14px;
}

/* On the zombie game's own page (the games' host), a word for where the game cannot run. */
.home .needs {
  display: none;
  margin: 28px 0 0;
  color: var(--muted);
  font-size: 14px;
}

@media (pointer: coarse), (hover: none), (max-width: 600px), (max-height: 560px) {
  .home .game {
    display: none;
  }

  .home .needs {
    display: block;
  }

  /* In its place, under the name: noughts and crosses. */
  .home .ttt {
    display: flex;
  }
}

/* The sign-in page: the card and, to its right on desktops, noughts and
   crosses for those with no account (noughts.js), the two centred as one.
   The game is as tall as the card: its opening line level with the card's
   top, its last with the card's foot, the board between. The board is 192px
   of squares; its lines run 26px past them on each side (40 of every 300
   units), like a drawn #, and its marks are drawn at nearly the lines'
   weight, in the page's ink. */
.pair {
  grid-row: 2;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 128px; /* card, gap and game add up to the footer's 44rem, so their outer edges meet the ends of its line */
  width: min(100% - 32px, 56rem);
  margin-inline: auto;
}

.pair .card {
  flex: none;
  width: min(100%, 360px);
  margin: 0;
}

.ttt {
  flex: none;
  display: flex;
  flex-direction: column;
  width: 216px; /* the board's own width, so the words above and below share its edges */
  font-size: 13px;
}

.ttt-intro {
  margin: 0;
  color: var(--muted);
}

/* Two lines are kept for the last words, so the board never moves when they change. */
/* The same quiet grey as the words above the board: the marks are the only bright thing here. */
.ttt-note {
  min-height: 40px;
  margin: 0;
  color: var(--muted);
}

/* The board sits midway between the words above and below it. */
.ttt-stage {
  width: 100%;
  margin: auto 0;
}

/* Back into place, unhurried. */
.ttt-board {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  aspect-ratio: 1;
  transition: transform 0.32s cubic-bezier(0.3, 0, 0.2, 1);
}

/* The cheat: the board darts under the cursor, quick but smooth. */
.ttt-board.shifting {
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* By touch it goes twice as fast, to be there before the finger lifts. */
.ttt-board.shifting.touched {
  transition: transform 0.07s cubic-bezier(0.2, 0, 0, 1);
}

/* The four lines draw themselves in, one after another, when the page opens.
   Each is exactly three squares long and they cross at thirds, so all nine
   squares are the same size and a mark sits in the middle of its own. */
.ttt-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1.4; /* 1px at this size */
  pointer-events: none;
}

.ttt-lines path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: ttt-draw 0.45s ease-out forwards;
}

.ttt-lines path:nth-child(2) {
  animation-delay: 0.12s;
}

.ttt-lines path:nth-child(3) {
  animation-delay: 0.24s;
}

.ttt-lines path:nth-child(4) {
  animation-delay: 0.36s;
}

.ttt-cell {
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  touch-action: manipulation; /* a tap is a tap at once, never the start of a zoom */
}

.ttt-cell:disabled {
  cursor: default;
}

.ttt-cell:focus-visible {
  outline: 1px solid var(--muted);
  outline-offset: -10px;
}

.ttt-mark,
.ttt-strike {
  position: absolute;
  pointer-events: none;
  fill: none;
  stroke: var(--text);
  stroke-linecap: square;
}

.ttt-mark {
  width: calc(100% / 3);
  height: calc(100% / 3);
  stroke-width: 1.5; /* 1.3px: a touch heavier than the grid, like ink over a ruled line */
}

.ttt-mark path,
.ttt-strike path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: ttt-draw 0.22s ease-out forwards;
}

.ttt-mark.x path:nth-child(2) {
  animation-delay: 0.12s;
}

.ttt-strike {
  inset: 0;
  width: 100%;
  height: 100%;
  stroke-width: 1.5;
}

.ttt-strike path {
  animation-duration: 0.35s;
}

@keyframes ttt-draw {
  to {
    stroke-dashoffset: 0;
  }
}

/* Kept in the layout while hidden, so the page does not shift when it appears. */
.ttt-again,
.ttt-again[hidden] {
  display: block;
}

.ttt-again[hidden] {
  visibility: hidden;
}

.ttt-again {
  align-self: flex-start;
  padding: 10px 0;
  margin: -50px 0 -10px;
  min-height: 60px;
  border: 0;
  background: none;
  color: var(--muted);
  text-align: left;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.ttt-again:hover,
.ttt-again:focus-visible {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* The game needs a mouse and room beside the card. */
@media (pointer: coarse), (hover: none), (max-width: 860px), (max-height: 520px) {
  .ttt {
    display: none;
  }
}

input[type="checkbox"] {
  accent-color: var(--text);
}

.row-apps .quiet {
  font-size: 12px;
}
