/* nard.gg — correspondence board.
   System fonts only: one webfont covering Latin, Greek, Cyrillic, Arabic,
   Hebrew and Japanese is arithmetically impossible, so we never start down
   that road. Costs 0 kB and never flashes. */

:root {
  /* Tells the browser both themes are handled, so native controls, scrollbars
     and the form widgets we do not style follow the page instead of staying
     light on a dark background. */
  color-scheme: light dark;

  --bg: #f0ece4;
  --panel: #fbf9f5;
  --ink: #1b1a17;
  --quiet: #6d6a63;
  --line: #d6d0c4;
  --felt: #2f6b58;
  --pt-a: #cbbfa8;
  --pt-b: #8d7b5e;
  --w: #fafaf8;
  --w-edge: #6b675f;
  --b: #26231e;
  --b-edge: #0d0c0a;
  --accent: #9c2b26;
  --ok: #2f6b58;

  /* The board frame. A felt rectangle with a hairline around it reads as a
     div; a wooden edge reads as a board. One token, both themes. */
  --frame: #6b5c46;

  /* Dice are physical objects, so they keep the same ivory in both themes —
     only the brightness drops in the dark, or they glare. */
  --die-face: #faf7f0;
  --die-edge: #b0a794;
  --die-pip: #221f1a;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14150f;
    --panel: #1d1e18;
    --ink: #ece9e0;
    --quiet: #9a978d;
    --line: #37382f;
    --felt: #1f4a3d;
    --pt-a: #4c4a3f;
    --pt-b: #6f6551;
    --w: #e6e3da;
    --w-edge: #9a978d;
    --b: #17160f;
    --b-edge: #000;
    --accent: #e0736c;
    --ok: #6dbfa4;

    --frame: #2c2619;

    --die-face: #d8d3c5;
    --die-edge: #6e685b;
    --die-pip: #16150f;
  }
}

* { box-sizing: border-box; }

::selection { background: var(--accent); color: #fff; }

html {
  /* Reserve the scrollbar lane so a page that grows past one screen does not
     shift sideways against one that does not. */
  scrollbar-gutter: stable;
  /* Landscape on iOS otherwise inflates body text and breaks the board's
     checker-to-point arithmetic. */
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  /* Android draws a grey box over every tapped element by default; with our own
     :active and :focus-visible states it is duplicate feedback. */
  -webkit-tap-highlight-color: transparent;
  /* No pull-to-refresh in the middle of a game. */
  overscroll-behavior-y: contain;
  /* The Noto entries cost nothing — they only apply where the font is already
     installed — and catch the engines whose OS font linking is weak for Arabic
     and Hebrew. Everything else system-ui resolves already covers Turkish,
     Cyrillic and Greek. */
  font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto,
        "Noto Sans Arabic", "Noto Sans Hebrew", "Helvetica Neue", Arial, sans-serif;
}

main {
  max-width: 46rem;
  margin: 0 auto;
  padding: 1.5rem 1rem 4rem;
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
}
/* Flex items default to a minimum size of their content. On a page whose
   widest children are a board and a list of notation, that default is how a
   phone ends up scrolling sideways. */
main > * { min-inline-size: 0; }

/* --- chrome ------------------------------------------------------------ */

/* Wrapping, not squeezing. The bar carries the brand, a username of unknown
   length, two links and the language picker; on a 320 px phone their combined
   min-content width is wider than the viewport, and a flex row that cannot wrap
   answers that by scrolling the whole document sideways. */
.bar {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: .5rem 1rem; padding: .7rem 1rem;
  border-bottom: 1px solid var(--line); background: var(--panel);
}
.brand { font-weight: 700; letter-spacing: -.01em; text-decoration: none; color: var(--ink); }
.bar nav {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .5rem .9rem; font-size: .9rem;
}
/* A long username breaks the line rather than the layout. */
.who { color: var(--quiet); min-inline-size: 0; overflow-wrap: anywhere; }
a { color: var(--accent); }
button.link {
  background: none; border: 0; font: inherit;
  color: var(--accent); cursor: pointer; text-decoration: underline;
  padding: .2rem 0;
}
/* At .9rem a line of text is about 20px tall — under the 24px WCAG 2.2 asks
   for. The links beside the logout button had the same problem and not the
   same fix, so they get it here together. Padding is vertical only, so the
   underlines still line up. */
.bar nav a, .bar nav button.link {
  display: inline-flex; align-items: center;
  min-height: 1.5rem; padding-block: .2rem;
}

footer {
  max-width: 46rem; margin: 0 auto; padding: 0 1rem 3rem;
  color: var(--quiet); font-size: .82rem;
}

h1 { font-size: 1.6rem; margin: 0; letter-spacing: -.015em; text-wrap: balance; }
h2 { font-size: 1.1rem; margin: 0 0 .6rem; text-wrap: balance; }
p { margin: 0; }
.quiet { color: var(--quiet); font-size: .9rem; }
.center { text-align: center; }

.alert, .notice, .banner {
  padding: .7rem .9rem; border-radius: 3px; border: 1px solid var(--line);
  background: var(--panel);
}
.alert { border-color: var(--accent); color: var(--accent); }
.banner { text-align: center; }

.panel {
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 3px; padding: 1rem 1.1rem;
}

/* --- forms -------------------------------------------------------------- */

.stack { display: flex; flex-direction: column; gap: .9rem; max-width: 24rem; }
.row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: end; }
label { display: flex; flex-direction: column; gap: .25rem; font-size: .9rem; }
label small { color: var(--quiet); font-size: .8rem; }
/* A tick box belongs beside its words, not above them. Without this the
   column direction above applies and the box sits on a line of its own, which
   reads as an unlabelled control. */
label.check {
  flex-direction: row; align-items: center; gap: .5rem;
  /* The box is the target; make the whole row one. */
  cursor: pointer;
}
label.check input { flex: none; inline-size: 1.15rem; block-size: 1.15rem; }
/* font-size is bound to the root, not inherited. Every field sits inside its
   own label, and `font: inherit` would resolve against that label's .9rem —
   14.4px, under the 16px below which iOS Safari zooms the page in on focus.
   The cure is a big enough field, never maximum-scale=1: switching zoom off
   fails WCAG 1.4.4 for everyone who needs to magnify.
   1rem rather than 16px so a reader who enlarged their default text keeps it. */
input, select, textarea {
  font-family: inherit;
  font-size: 1rem;
  line-height: 1.6;
  padding: .45rem .6rem; border: 1px solid var(--line);
  border-radius: 3px; background: var(--bg); color: var(--ink);
}
/* The padding and the frame above are meant for text fields. On a checkbox
   Firefox takes them as an instruction to stop drawing the native control, and
   what is left is an empty square that never looks checked. */
input[type="checkbox"], input[type="radio"] {
  padding: 0; border: 0; background: none; accent-color: var(--accent);
}
button {
  font: inherit; padding: .45rem .85rem; border: 1px solid var(--line);
  border-radius: 3px; background: var(--bg); color: var(--ink); cursor: pointer;
  /* Kills the 300 ms double-tap-zoom wait that Android and older iOS keep for
     anything they cannot prove is not zoomable. Free 300 ms off every move. */
  touch-action: manipulation;
}
a, label, summary { touch-action: manipulation; }
/* .cta traegt seine Form selbst.
 *
 * Vorher standen hier nur Farben; Polsterung, Rahmen und Rundung kamen aus der
 * button-Regel. Auf sieben Seiten sitzt .cta auf einem <button> und sah damit
 * richtig aus — auf der Startseite sitzt es auf einem <a>, der davon nichts
 * erbt, und der wichtigste Knopf der Seite sah aus wie angestrichener Text.
 * Eine Klasse, die nur auf einem Elementtyp funktioniert, ist eine Falle. */
.cta {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .45rem .85rem; min-height: 2.75rem;
  border: 1px solid var(--accent); border-radius: 3px;
  background: var(--accent); color: #fff;
  font: inherit; font-weight: 600; text-decoration: none; cursor: pointer;
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Hover only where there is a pointer that can hover. On a touchscreen an
   unguarded :hover sticks after the tap until something else is tapped. */
@media (hover: hover) and (pointer: fine) {
  button:hover { border-color: var(--accent); }
  .cta:hover { background: color-mix(in oklab, var(--accent) 88%, white); }
  .matches li:hover { border-color: var(--accent); }
}

/* Between the click and the new page there is nothing to show on a site
   without JavaScript — except that the button acknowledges the press. This is
   the whole of our loading feedback, so it must be instant. */
button:active { transform: translateY(1px); }

/* --- language picker ---------------------------------------------------- */

/* A label only a screen reader reads. clip-path rather than width:0, so the
   text still has a box and is still announced. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* The sprite itself must not take up a line. Kept in the flow with zero size
   rather than display:none, which stops some engines resolving <use>. */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

/* 4:3, matching the viewBox. A hairline keeps the white stripe of the Bulgarian
   and Russian flags from bleeding into a light background. */
.flag {
  inline-size: 1.4rem; block-size: 1.05rem; flex: none;
  border-radius: 2px; box-shadow: inset 0 0 0 1px rgb(0 0 0 / .18);
}

.langpick { position: relative; }
.langpick > summary {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .25rem .45rem; min-height: 1.75rem;
  border: 1px solid transparent; border-radius: 3px;
  cursor: pointer; color: var(--ink);
  /* Both spellings: the pseudo-element is what current engines use, the
     property is what Safari needed for years. */
  list-style: none;
}
.langpick > summary::-webkit-details-marker { display: none; }
.langpick[open] > summary { border-color: var(--line); background: var(--bg); }
.langpick .chev { inline-size: .8rem; block-size: .8rem; color: var(--quiet); }

/* The panel hangs off the header rather than pushing it apart — a menu that
   resizes the bar it lives in moves everything else on the page. */
.langmenu {
  position: absolute; inset-block-start: calc(100% + .4rem); inset-inline-end: 0;
  z-index: 20; min-inline-size: 11rem; padding: .25rem;
  display: flex; flex-direction: column; gap: .1rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: 4px;
  box-shadow: 0 8px 24px rgb(0 0 0 / .18);
}
.langmenu button {
  display: flex; align-items: center; gap: .55rem; inline-size: 100%;
  padding: .4rem .5rem; border: 0; border-radius: 3px;
  background: none; text-align: start; font-size: .92rem;
}
/* The tick, not just bold: weight alone is not a difference somebody scanning
   a list of six unfamiliar words will notice. */
.langmenu button[aria-current] { font-weight: 600; }
.langmenu button[aria-current]::after {
  content: "✓"; margin-inline-start: auto; color: var(--ok);
}

/* On the settings page there is room to show all six at once. */
.langgrid {
  display: grid; gap: .5rem;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.langgrid button {
  display: flex; align-items: center; gap: .6rem;
  padding: .55rem .7rem; text-align: start;
}
.langgrid button[aria-current] {
  border-color: var(--accent); font-weight: 600;
  background: color-mix(in oklab, var(--accent) 8%, transparent);
}

@media (hover: hover) and (pointer: fine) {
  .langpick > summary:hover { border-color: var(--line); }
  .langmenu button:hover { background: color-mix(in oklab, var(--accent) 12%, transparent); }
}

@media (prefers-reduced-motion: no-preference) {
  /* The panel is created by opening the details, so there is no earlier frame
     to animate from — @starting-style supplies one. */
  .langpick[open] .langmenu {
    opacity: 1; transform: none;
    transition: opacity var(--dur-moderate) var(--ease-standard-enter),
                transform var(--dur-moderate) var(--ease-emphasized-enter);
  }
  @starting-style {
    .langpick[open] .langmenu { opacity: 0; transform: translateY(-.35rem) scale(.97); }
  }
  .langpick .chev { transition: transform var(--dur-fast) var(--ease-standard); }
}
.langpick[open] .chev { transform: rotate(180deg); }

/* --- motion ------------------------------------------------------------- */

/* Motion. Durations are tied to purpose, not to one global constant: a hover
   at 300ms feels sluggish, a page change at 80ms is not readable. Exits are
   shorter than entrances — an entrance directs attention and needs the time,
   an exit is clearing away and must not block. */
:root {
  --dur-instant:  80ms;  /* hover, focus ring */
  --dur-fast:     120ms; /* button press, toggle */
  --dur-normal:   200ms; /* tooltip, small panel */
  --dur-moderate: 280ms; /* dropdown, toast appearing */
  --dur-slow:     400ms; /* page change, board rebuild */
  --dur-fast-exit:     90ms;
  --dur-normal-exit:   150ms;
  --dur-moderate-exit: 200ms;

  /* Entering decelerates, leaving accelerates. Symmetric ease-in-out for both
     is the tell-tale sign of generic animation. */
  --ease-standard:         cubic-bezier(.2, 0, 0, 1);
  --ease-standard-enter:   cubic-bezier(0, 0, 0, 1);
  --ease-standard-exit:    cubic-bezier(.3, 0, 1, 1);
  --ease-emphasized-enter: cubic-bezier(.05, .7, .1, 1);
  --ease-emphasized-exit:  cubic-bezier(.3, 0, .8, .15);
}

/* Opt in rather than opt out: with the base rules unanimated there is no flash
   of movement before a reduce preference takes hold, and no !important war. */
@media (prefers-reduced-motion: no-preference) {
  a, button, input, select, .matches li {
    transition: border-color var(--dur-instant) var(--ease-standard),
                background-color var(--dur-instant) var(--ease-standard),
                color var(--dur-instant) var(--ease-standard);
  }
  /* The press has to read as immediate, so it snaps in and eases out. */
  button { transition: transform var(--dur-fast-exit) var(--ease-standard-exit),
                       border-color var(--dur-instant) var(--ease-standard),
                       background-color var(--dur-instant) var(--ease-standard); }
  button:active { transition-duration: 0s; }

  /* Flash messages arrive with the page, so there is nothing to transition
     from — @starting-style supplies that missing first frame. */
  .alert, .notice {
    opacity: 1; transform: none;
    transition: opacity var(--dur-moderate) var(--ease-standard-enter),
                transform var(--dur-moderate) var(--ease-emphasized-enter);
  }
  @starting-style {
    .alert, .notice { opacity: 0; transform: translateY(-.4rem); }
  }
}

/* Cross-document transitions cost one rule and degrade to a plain navigation
   where they are unsupported. They do not fire after a form POST, so this
   covers link navigation — lobby to match — not the move itself.
   Kept at the top level on purpose: whether @view-transition applies inside a
   conditional group rule is not something to rely on, so the preference is
   honoured by switching the animation off below instead. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
}

/* --- lists -------------------------------------------------------------- */

.matches { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
.matches li {
  display: flex; justify-content: space-between; gap: 1rem; align-items: center;
  padding: .55rem .8rem; border: 1px solid var(--line); border-radius: 3px;
  background: var(--panel);
  /* A row is one thumb target. 44 CSS px is the Apple HIG figure; Material
     asks for 48dp, which lands in the same place once padding is counted. */
  min-height: 2.75rem;
}
.matches li.yours { border-inline-start: 3px solid var(--accent); }
/* A player name can be long, or in a script that does not break where Latin
   does. min-width:0 lets the flex item shrink at all; the wrap rules decide
   where. Without both, a single long name widens the row past the screen. */
.matches a {
  text-decoration: none; color: var(--ink);
  min-width: 0; overflow-wrap: anywhere;
  /* The link, not the row, is what a screen reader and a thumb both aim at,
     so the target has to be the full height of the row. */
  display: flex; align-items: center; align-self: stretch;
}
@media (hover: hover) and (pointer: fine) {
  .matches a:hover { text-decoration: underline; }
}
.meta { color: var(--quiet); font-size: .84rem; white-space: nowrap; }
/* "du bist dran" is the only thing on the lobby page anybody scans for, so it
   stops being coloured text among grey text and becomes a shape. */
.turn {
  color: #fff; background: var(--accent);
  border-radius: 999px; padding: .1rem .5rem;
  font-size: .78rem; font-weight: 700; letter-spacing: .01em;
  white-space: nowrap;
}

.hero { display: flex; flex-direction: column; gap: .8rem; }
.lede { font-size: 1.08rem; max-width: 34rem; }

/* --- board -------------------------------------------------------------- */

.board {
  /* A flat fill reads as a rectangle of paint; one soft light from the top
     edge reads as cloth. Painted once, never animated. */
  background:
    radial-gradient(125% 90% at 50% -12%,
      color-mix(in oklab, var(--felt) 88%, white) 0%,
      var(--felt) 62%);
  border: 3px solid var(--frame);
  border-radius: 6px;
  padding: .4rem;
  /* Grid, not flex: two 1fr rows are forced to the same height, so a point
     carrying a stack of eight cannot leave the opposite half short. */
  display: grid; grid-template-rows: 1fr 1fr; gap: .35rem;
  /* The running direction is a rule of the game, not of the interface. Without
     this, dir="rtl" on <html> flips the grid below and with it the board — the
     same class of bug that Variant.Geometry() exists to prevent. */
  direction: ltr;
  /* Everything inside is measured against the board's own width, not the
     viewport: one set of rules holds from a 320px phone to the 46rem column,
     and the board can never be wider than the space it was given. */
  container-type: inline-size;
  /* Named, so that a container nested inside the board later cannot quietly
     capture the cqi units the points are sized with. */
  container-name: board;
  /* main is a flex column, and a flex item's automatic minimum size is its
     min-content width. Without this the board would push itself wider than the
     phone rather than let its points shrink — and because the checker size is
     read back off the board's width, that mistake feeds itself. */
  min-inline-size: 0;
  /* Only reachable when the clamp floor above outgrows the space, which takes
     an extreme zoom on a narrow screen. Then the board scrolls and the page
     does not — WCAG 1.4.10 allows exactly that for a game board, and it is far
     better than the whole page scrolling sideways. */
  overflow-x: auto;
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .3),
    inset 0 10px 22px -12px rgba(0, 0, 0, .55),
    0 1px 2px rgba(0, 0, 0, .18);
}
.half {
  /* minmax(0, …) and not a bare fr: an fr track refuses to go below its
     min-content size, which on a narrow phone means the twelve points win the
     argument and the board leaves the screen. Here the board wins it. */
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 1fr) minmax(0, 6fr);
  gap: .3rem;
  /* Declared on the half rather than on .board because a container cannot
     query itself — cqw inside .board would resolve against the viewport.
     clamp and not min: a bare min() is a ceiling with no floor, and container
     units behave like viewport units under browser zoom — the CSS-pixel width
     shrinks in step with the magnification, so the checker stays the same
     physical size and the zoom does nothing. On a phone the cqi term always
     wins (the 2.4rem ceiling only binds past ~619px of board), so without a
     rem floor the checkers shrink towards nothing at 400%. That is WCAG 1.4.4,
     and it is a separate question from reflow, which we already pass. */
  --ck-size: clamp(1.15rem, 0.19rem + 4.81cqi, 2.4rem);
}
/* No gap: on a real board the triangles meet at their bases, and the alternating
   colours are what separates them. The single 1fr row makes every point as tall
   as its half, so the triangles all end on the same line. */
.quad {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-template-rows: 1fr; gap: 0;
  min-inline-size: 0;
}

.pt {
  position: relative;
  /* Five checkers plus the point number, so both halves are the same height
     whether a point is loaded or empty. Nothing on the board ever reflows. */
  min-height: calc(5 * var(--ck-size) + 1.6rem);
  padding: .2rem .06rem;
  display: flex; flex-direction: column; align-items: center; gap: .08rem;
  min-inline-size: 0;
}
/* The triangle is a pseudo-element and not the point's own background, because
   the checkers live inside the point and must not be clipped along with it. */
.pt::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  background: linear-gradient(to bottom,
    var(--pt) 0%, color-mix(in oklab, var(--pt) 68%, black) 100%);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}
.bottom .pt::before {
  background: linear-gradient(to top,
    var(--pt) 0%, color-mix(in oklab, var(--pt) 68%, black) 100%);
  clip-path: polygon(0 100%, 100% 100%, 50% 0);
}
.pt > * { position: relative; z-index: 1; }
.quad .pt { --pt: var(--pt-a); }
.quad .pt:nth-child(even) { --pt: var(--pt-b); }

.bottom .pt { flex-direction: column-reverse; }
.num {
  /* Floor for the same reason as --ck-size: container units do not survive a
     zoom on their own. */
  font-size: clamp(.5rem, 3cqw, .66rem); line-height: 1.3;
  color: var(--ink); opacity: .5; font-variant-numeric: tabular-nums;
}
/* No `order` on the bottom number: column-reverse already puts the first DOM
   child at the bottom, which is where the base of an upward triangle is.
   Re-ordering on top of that floated the number above the stack, so a loaded
   point printed its number halfway up the board and an empty one did not. */

/* Not .stack: that name belongs to the form column above, and the later rule
   would win over it. Same reason .board-bar is not .bar. */
.ck-stack { display: flex; flex-direction: column; align-items: center; gap: .06rem; }
.bottom .ck-stack { flex-direction: column-reverse; }

.ck {
  /* The fallback is for the two checkers in the match heading, which sit
     outside the board and therefore outside the container. */
  --ck-gloss: radial-gradient(circle at 34% 26%, rgba(255,255,255,.5), rgba(255,255,255,0) 58%);
  display: block;
  inline-size: var(--ck-size, 1.15rem);
  block-size: var(--ck-size, 1.15rem);
  border-radius: 50%;
  border: 1px solid;
  background-repeat: no-repeat;
  background-image: var(--ck-gloss);
  box-shadow: 0 1px 1.5px rgba(0, 0, 0, .35);
}
.ck.w { background-color: var(--w); border-color: var(--w-edge); }
.ck.b {
  background-color: var(--b); border-color: var(--b-edge);
  --ck-gloss: radial-gradient(circle at 34% 26%, rgba(255,255,255,.26), rgba(255,255,255,0) 58%);
}
/* A stack of more than five is printed as a number, so the badge has to be the
   size of a checker — otherwise the point it sits on grows and the board
   loses its grid. */
.more {
  font-size: clamp(.55rem, 3cqw, .7rem); font-weight: 700; line-height: 1.25;
  color: var(--ink); background: var(--panel);
  border: 1px solid var(--line); border-radius: 999px;
  padding: 0 .3rem; font-variant-numeric: tabular-nums;
}

.board-bar {
  /* Two shaded edges instead of one flat wash: the bar reads as a recess in
     the frame rather than a grey column. */
  background: linear-gradient(90deg,
    rgba(0,0,0,.36), rgba(0,0,0,.12) 38%, rgba(0,0,0,.12) 62%, rgba(0,0,0,.36));
  border-radius: 2px; border: 0;
  display: flex; flex-direction: column; align-items: center; padding: .2rem;
  min-inline-size: 0;
}
/* Checkers on the bar hang from the frame they were knocked towards. */
.top .board-bar { justify-content: flex-start; }
.bottom .board-bar { justify-content: flex-end; }

/* A pinned checker is drawn at the bottom of the pile and ringed, because the
   one thing a Plakoto player must see at a glance is who is stuck.
   Two things carry it, not one: the ring, and a centre dot so it differs in
   *shape* and not only in colour — on a phone the checker is under 20px and a
   2px ring alone is a thin argument. z-index keeps the ring, which is drawn
   outside the circle, above the checkers stacked on top of it. */
.ck.pinned {
  position: relative;
  z-index: 1;
  background-image:
    radial-gradient(circle closest-side, var(--accent) 100%, #0000),
    var(--ck-gloss);
  background-size: 38% 38%, auto;
  background-position: 50% 50%, 0 0;
  box-shadow: 0 0 0 .13em var(--accent), 0 1px 2px rgba(0, 0, 0, .45);
  margin-block: .1em;
}

/* Whose turn it is, without a second data source: the actions panel is
   rendered only when the viewer may act, so :has() turns that one fact into a
   board that is visibly live. */
main:has(.actions) .board {
  border-color: var(--accent);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .3),
    inset 0 10px 22px -12px rgba(0, 0, 0, .55),
    0 0 0 3px color-mix(in oklab, var(--accent) 28%, transparent);
}

.matchhead {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  font-size: 1.05rem; font-weight: 600;
}
.matchhead .side { display: flex; align-items: center; gap: .4rem; min-width: 0; overflow-wrap: anywhere; }
.score {
  font-size: 1.35rem; font-variant-numeric: tabular-nums;
  white-space: nowrap; letter-spacing: .02em;
}

.status {
  display: flex; flex-wrap: wrap; gap: .4rem .6rem; align-items: center;
  color: var(--quiet); font-size: .85rem; font-variant-numeric: tabular-nums;
}
.status > span {
  border: 1px solid var(--line); border-radius: 999px;
  padding: .1rem .6rem; background: var(--panel);
}

/* --- dice ---------------------------------------------------------------- */

/* Eyes, not digits. Each pip is one radial-gradient layer placed on the 3x3
   grid every physical die uses. Percentage background-position is measured
   against the *free* space, so with a pip 20% wide the column centres at
   25/50/75% are reached at 18.75/50/81.25%.
   Needs <i class="die" data-d="N"> in the markup; see the note at the end of
   this file for the two template lines that produce it. */
/* Not .roll — that class already names the plain number chips on the dice-proof
   page, and the two would fight over display and padding. */
.dice-roll {
  display: inline-flex; align-items: center; gap: .3rem;
  vertical-align: -.4em;
}
.die {
  --pip: radial-gradient(circle closest-side, currentColor 96%, #0000);
  --c1: 18.75%; --c2: 50%; --c3: 81.25%;
  inline-size: var(--die-size, 1.75rem);
  block-size: var(--die-size, 1.75rem);
  flex: none;
  border: 1px solid var(--die-edge);
  border-radius: 22%;
  background-color: var(--die-face);
  color: var(--die-pip);
  background-repeat: no-repeat;
  background-size: 20% 20%;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .3), inset 0 0 0 1px rgba(255, 255, 255, .45);
}
.die[data-d="1"] {
  background-image: var(--pip);
  background-position: var(--c2) var(--c2);
}
.die[data-d="2"] {
  background-image: var(--pip), var(--pip);
  background-position: var(--c1) var(--c1), var(--c3) var(--c3);
}
.die[data-d="3"] {
  background-image: var(--pip), var(--pip), var(--pip);
  background-position: var(--c1) var(--c1), var(--c2) var(--c2), var(--c3) var(--c3);
}
.die[data-d="4"] {
  background-image: var(--pip), var(--pip), var(--pip), var(--pip);
  background-position: var(--c1) var(--c1), var(--c3) var(--c1),
                       var(--c1) var(--c3), var(--c3) var(--c3);
}
.die[data-d="5"] {
  background-image: var(--pip), var(--pip), var(--pip), var(--pip), var(--pip);
  background-position: var(--c1) var(--c1), var(--c3) var(--c1), var(--c2) var(--c2),
                       var(--c1) var(--c3), var(--c3) var(--c3);
}
.die[data-d="6"] {
  background-image: var(--pip), var(--pip), var(--pip), var(--pip), var(--pip), var(--pip);
  background-position: var(--c1) var(--c1), var(--c3) var(--c1),
                       var(--c1) var(--c2), var(--c3) var(--c2),
                       var(--c1) var(--c3), var(--c3) var(--c3);
}

/* A double is four half-moves, so it is four dice, framed and named. The frame
   is what makes it different at a glance; the word is what makes it different
   for somebody who cannot see the frame (WCAG 1.4.1). */
.dice-roll.double {
  padding: .25rem .4rem;
  border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
  border-radius: 7px;
  background: color-mix(in oklab, var(--accent) 10%, transparent);
}
.dice-roll .tag {
  font-size: .74rem; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--accent);
}

/* --- actions ------------------------------------------------------------- */

.actions {
  display: flex; flex-direction: column; gap: .9rem;
  border-inline-start: 3px solid var(--accent);
  /* The move redirect lands on #actions. The margin keeps the heading clear of
     the top edge on arrival, and matters more the day the bar turns sticky. */
  scroll-margin-top: 1rem;
}
/* The panel is only focusable so the jump carries a screen reader with it. It
   is not a control, so it must not draw a focus ring like one. */
.actions:focus { outline: none; }
.actions h2 {
  display: flex; align-items: center; gap: .5rem;
  margin: 0; font-size: 1.15rem;
}
.actions h2::before {
  content: ""; flex: none;
  inline-size: .6em; block-size: .6em;
  border-radius: 50%; background: var(--accent);
}

/* The move list is the whole interaction on this page. It is a grid, not a
   wrapped row, so every button is the same width and the thumb does not have
   to aim at a ragged edge. */
.turns {
  list-style: none; margin: 0; padding: 0;
  /* The track minimum is fluid rather than switched at a breakpoint: it slides
     from 7.5rem on a phone to 9.5rem on the full column, so the buttons never
     jump a size at one particular width. The rem floor also keeps the track
     from collapsing under zoom, where 22vw goes to almost nothing. */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(clamp(7.5rem, 22vw, 9.5rem), 1fr));
  gap: .4rem;
}
.turns li {
  display: flex;
  /* Gul Bara's doubles can produce a very long list; skipping layout for the
     part below the fold is the one measurable performance win on this page.
     The intrinsic size keeps the scrollbar honest while it is skipped. */
  content-visibility: auto;
  contain-intrinsic-size: auto 2.75rem;
}
.turns form { display: flex; inline-size: 100%; }
.turns button {
  inline-size: 100%;
  min-height: 2.75rem;
  padding: .5rem .7rem;
  background: var(--panel);
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: .95rem;
  font-variant-numeric: tabular-nums;
  /* Notation is digits and slashes. In an RTL page the bidi algorithm would
     reorder it; the board's geometry does not flip, and neither does its
     notation. */
  direction: ltr;
}

/* Roll, double, take, resign — the coarse-pointer minimum applies here too,
   and the primary action gets the width it deserves on a phone. */
.actions .row { gap: .5rem; align-items: stretch; }
.actions .row form { flex: 1 1 10rem; display: flex; }
.actions .row button { inline-size: 100%; min-height: 2.75rem; }

/* Waiting is not an error and not an activity. Nothing moves, and the dashed
   edge says "nothing to do here" without a word. */
.banner.quiet { border-style: dashed; }

.posid { color: var(--quiet); font-size: .78rem; }
code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .85em; }
.invite { font-size: 1rem; padding: .3rem .5rem; background: var(--bg);
  border: 1px solid var(--line); border-radius: 3px; display: inline-block; }

@media (max-width: 34rem) {
  /* The board is the widest thing on the page. On a phone it takes the whole
     viewport instead of being squeezed into main's gutter — the checkers grow
     with it, and nothing ever scrolls sideways because the width is a
     percentage of the parent, never a vw. */
  .board { margin-inline: -1rem; border-radius: 0; }
  .matchhead { flex-direction: column; gap: .3rem; }
  main { padding-bottom: 3rem; }
}

/* Landscape on a phone, gated on height rather than width. A phone turned
   sideways is often wider than 34rem, so a width query would miss most of
   them; what is actually scarce is height. A tablet in landscape has plenty of
   height and drops out by itself — no device sniffing needed. lichess gates
   its own two-column phone layout the same way. */
@media (orientation: landscape) and (max-height: 32rem) {
  main:has(.actions) {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(13rem, 19rem);
    align-items: start;
    gap: .75rem 1rem;
  }
  /* Anything that speaks to the whole page keeps the full width; without this
     a flash message would be auto-placed into whichever cell happened to be
     free and end up beside the board. */
  main:has(.actions) .matchhead,
  main:has(.actions) .alert,
  main:has(.actions) .notice,
  main:has(.actions) > p.quiet { grid-column: 1 / -1; }
  main:has(.actions) .board { grid-column: 1; margin-inline: 0; }
  main:has(.actions) .status,
  main:has(.actions) .actions,
  main:has(.actions) .posid { grid-column: 2; }
  /* The right column is the narrow one; one move per row reads better there
     than two half-width ones. */
  main:has(.actions) .turns { grid-template-columns: minmax(0, 1fr); }
}

/* --- motion: the dice ---------------------------------------------------- */

/* The die never shows a value it does not have. It tumbles as a whole and
   lands on the one face the server already committed to — a face cycling
   through 3-1-5-2 is a slot machine reel, and a lie about where the number
   comes from. */
@keyframes die-toss {
  0%   { transform: translateY(-42%) rotate(-24deg) scale(.85); opacity: 0; }
  30%  { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
@keyframes die-settle {
  from { opacity: 0; transform: scale(.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes turn-ping {
  0%, 100% { transform: scale(1); opacity: 1; }
  45%      { transform: scale(1.4); opacity: .6; }
}

@media (prefers-reduced-motion: no-preference) {
  .die {
    /* Hand-tuned spring, roughly stiffness 260 / damping 22 / mass 1. If it
       needs changing, regenerate the stops from those parameters rather than
       nudging numbers — that is how these curves turn to mush. Overshoot is
       what cubic-bezier structurally cannot do. */
    animation: die-toss 520ms linear(
      0, .06 8%, .28 20%, .72 42%, 1.06 60%, .97 72%, 1.02 84%, 1
    ) both;
  }
  /* Four dice landing together is one event; four landing 70 ms apart is a
     throw. That stagger is the entire celebration a double gets. */
  .die:nth-child(2) { animation-delay: 70ms; }
  .die:nth-child(3) { animation-delay: 140ms; }
  .die:nth-child(4) { animation-delay: 210ms; }

  @supports not (animation-timing-function: linear(0, 1)) {
    .die { animation-timing-function: cubic-bezier(.34, 1.4, .64, 1); }
  }

  /* Two beats, then still. A looping pulse would be an attention tax on a game
     played over days, and past five seconds WCAG 2.2.2 would want a stop
     control for it. */
  .actions h2::before { animation: turn-ping 1.1s var(--ease-standard) 2 both; }
}

@media (prefers-reduced-motion: reduce) {
  /* Replaced, not removed: the arrival of a new roll is information. */
  .die { animation: die-settle 120ms var(--ease-standard-enter) both; }
}

/* Correspondence backgammon on an e-reader is not far-fetched — correspondence
   chess is played there. Every animation on that hardware is ghosting. */
@media (update: slow) {
  .die, .actions h2::before, .alert, .notice, button {
    animation: none; transition: none;
  }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none; }
}

/* --- the dice proof page ------------------------------------------------- */

.proof h2 { margin-top: 0; }

.verdict {
  margin: 0 0 .75rem;
  padding: .5rem .75rem;
  border-inline-start: 3px solid var(--line);
  font-weight: 600;
}
.verdict.good    { border-inline-start-color: var(--ok); }
.verdict.bad     { border-inline-start-color: var(--accent); }
.verdict.pending { border-inline-start-color: var(--line); font-weight: 400; }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; margin: 0; }
.kv dt { color: var(--quiet); }
.kv dd { margin: 0; min-width: 0; }

/* A 64-character hash has to wrap on a phone rather than widen the page. */
.hash { word-break: break-all; font-size: .85em; }

.rolls { display: flex; flex-wrap: wrap; gap: .3rem; }
.roll {
  font-variant-numeric: tabular-nums;
  padding: .05rem .35rem;
  border: 1px solid var(--line);
  border-radius: 3px;
}
.back { color: var(--quiet); }

/* --- forced colours ------------------------------------------------------ */

/* Windows high contrast collapses every background-color onto a system colour
   and drops box-shadow entirely. Without this block the two checker colours
   become one, and the pinned ring — the one thing a Plakoto player must see —
   disappears with the shadow. Background *images* survive, so the gloss and
   the pin dot have to be switched off by hand or they paint over the
   system colours. */
@media (forced-colors: active) {
  .ck        { background-image: none; box-shadow: none; }
  .ck.w      { background: Canvas;     border: 2px solid CanvasText; }
  .ck.b      { background: CanvasText; border: 2px solid Canvas; }
  .ck.pinned { box-shadow: none; outline: 2px solid LinkText; outline-offset: 1px; }
  .pt        { border: 1px solid CanvasText; }
  .pt::before { display: none; }
  .die       { background-color: Canvas; color: CanvasText; box-shadow: none; }
  .board     { background: Canvas; }
  main:has(.actions) .board { outline: 3px solid Highlight; }
}

/* --- print ---------------------------------------------------------------- */

/* A correspondence game is worth keeping. Printing one should give the board
   and the score, not the navigation and the buttons that no longer do
   anything on paper. */
@media print {
  .bar, footer, .actions, .row, .turns, .posid { display: none; }
  body { background: #fff; color: #000; }
  main { max-width: none; padding: 0; gap: 1rem; }
  /* Browsers drop backgrounds when printing unless told otherwise, and without
     them the two checker colours print as the same empty circle — the same
     failure as in forced colours. The felt goes white to save the ink that
     buys. */
  .board {
    break-inside: avoid; border-color: #000; background: #fff;
    box-shadow: none;
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .panel, .matches li { border-color: #999; }
  .ck.w { background: #fff; border-color: #000; box-shadow: none; }
  .ck.b { background: #000; border-color: #000; box-shadow: none; }
  /* Triangles stay, in grey — they are what makes the printout a board — but
     they do not get the felt's ink budget. */
  .pt::before { background: #eee; }
  .quad .pt:nth-child(even)::before { background: #c9c9c9; }
  .board-bar { background: #eee; }
  .die { box-shadow: none; border-color: #000; background-color: #fff; color: #000; }
}

/* --- reduced contrast preferences ---------------------------------------- */

/* --line sits close to --bg by design; when the reader asks for more contrast
   it has to step away from it, or the panels lose their edges. */
@media (prefers-contrast: more) {
  :root { --line: #a89f8c; --pt-b: #7a6644; --die-edge: #6f6a5d; }
  @media (prefers-color-scheme: dark) {
    :root { --line: #6b6c5e; --pt-a: #3a3830; --pt-b: #857a60; }
  }
  button, input, select, .panel, .matches li { border-width: 2px; }
  .ck { border-width: 2px; }
  .num { opacity: .8; }
}

/* --- pending confirmation ------------------------------------------------ */

/* An address that has been entered but not confirmed yet. Not an error — the
   visitor did nothing wrong — but not settled either, so it gets the dashed
   edge the board uses for "nothing to do here but wait". */
.pending {
  margin-block: .75rem;
  padding: .7rem .9rem; border-radius: 3px;
  border: 1px dashed var(--line); background: var(--panel);
  font-size: .9rem;
}
