/* RPSC — printed matter. Ink on paper, and nothing floats: every surface sits
   flat on the sheet, separated by a hairline or by being inked solid.
   No shadows, no gradients, no webfonts, no images, no network.

   The whole page is built from four decisions:
     · two faces       — one weight of a geometric sans to speak, one of a
                         monospace to record, and no bold anywhere
     · three sizes     — 30 / 15 / 13, and nothing between them or below
     · four inks       — ink, muted, blue, red, over two paper tones
     · one radius      — 3px, on every corner that has one
   Anything that looks like a fifth of something is a mistake.

   The two editions below are the only place a colour is named. Everything
   after them reads these eight values, so the night edition and the paper one
   are the same page with the light turned round. */

:root {
  color-scheme: dark;

  --paper:  #191712;   /* the page */
  --sheet:  #221f19;   /* anything laid on it */
  --ink:    #ece4d5;   /* what does the printing */
  --muted:  #9b9280;
  --blue:   #7fa0dc;
  --red:    #dd8065;

  /* One line and one fill, both cut from the ink. Every rule, border, divider
     and tint on the page is one of these two. */
  --line: rgba(236,228,213,.22);
  --wash: rgba(236,228,213,.07);

  /* Grain multiplies into paper and screens onto night: either way it is the
     press showing through, never a wash over the top. */
  --grain-blend: screen;
  --grain-opacity: .14;

  --font-display: "Futura", "Futura PT", "Avenir Next", "Century Gothic",
                  "URW Gothic", "Trebuchet MS", sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, "DejaVu Sans Mono", Consolas, monospace;

  /* Three sizes. Large is the masthead alone; medium is everything a player
     reads while playing; small is every label, in caps, and nothing else. */
  --fs-lg: 30px;
  --fs-md: 15px;
  --fs-sm: 13px;

  /* A 4px step. Every margin, padding and gap on the page is one of these. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 20px;
  --s6: 28px;

  --r: 3px;
  --hair: 1px solid var(--line);
  --edge: 1px solid var(--ink);

  /* The board is a square that fills its column and stops at this cap, which
     is also the moves card's natural height — so the two panels start and
     finish on the same lines. Change what the card holds and remeasure. */
  --board-max: 595px;
  --board-pad: 32px;

  /* How far the last move's outline stands off a piece. It is the scissors
     that sets it: a clip-path cannot be stroked, so that outline is the same
     polygon on a larger box, and a 3px one needs the box grown by
     1.5 / 0.212932 = 7.0445px a side (the derivation is with the rule). The
     disc and the square would be happy with 3px; they use this too, as the
     floor under every piece's inset, so no piece ever sits closer to its
     square's edge than the widest outline it may have to wear. */
  --outline-off: 7.0445px;

  /* The page is exactly as wide as what it holds: board, gutter, card, and a
     24px margin each side. The masthead measures itself the same way, so the
     title starts on the board's left edge. */
  --card-w: 336px;
  --col-gap: 32px;
  --page: calc(var(--board-max) + var(--col-gap) + var(--card-w) + 48px);
}

/* The paper edition, one attribute away: <html data-theme="light">. */
[data-theme="light"] {
  color-scheme: light;

  --paper:  #ece4d5;
  --sheet:  #faf7ef;
  --ink:    #23201c;
  --muted:  #6c6357;
  --blue:   #2f4f8e;
  --red:    #b4503a;

  --line: rgba(35,32,28,.20);
  --wash: rgba(35,32,28,.07);

  --grain-blend: multiply;
  --grain-opacity: .30;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--fs-md);
  line-height: 1.45;
}

/* Press grain, laid over everything and never in the way. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 99;
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.22'/%3E%3C/svg%3E");
}

/* Keyboard focus is the one ring that overrides everything else. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* ------------------------------------------------------------- masthead */

.masthead {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s6) 24px var(--s4);
}

.masthead h1 {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: 400;
  letter-spacing: .08em;
  line-height: 1;
}

/* Every label on the page is this: monospace, caps, tracked, muted. */
.tagline,
.card-head h2,
.field legend,
.rules dt {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--muted);
}

.tagline { margin: var(--s2) 0 0; }
.tagline .sep { margin: 0 .2em; color: var(--ink); }

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

main {
  display: grid;
  /* The board column is the board's own width, not the leftover space: a
     wider column would centre the board in it and read as a wider gutter. */
  grid-template-columns: minmax(0, var(--board-max)) var(--card-w);
  gap: var(--col-gap);
  align-items: start;
  justify-content: center;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 24px var(--s6);
}

/* Everything laid on the paper is the same object: a sheet with a hairline. */
.panel {
  border: var(--hair);
  border-radius: var(--r);
  background: var(--sheet);
}

/* The left column is a stack on one rhythm, and it is the same gap as the one
   between the columns — so the board, the banner and the rules read as evenly
   spaced sheets however the page is folded. */
.board-side > * + * { margin-top: var(--col-gap); }

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

.board-frame {
  position: relative;
  max-width: var(--board-max);
  margin-inline: auto;
}

.board-stage { position: relative; }

/* The margin is part of the printed board: wide enough to carry the file and
   rank labels inside the panel on all four sides. */
.board {
  position: relative;
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  aspect-ratio: 1;
  padding: var(--board-pad);
}
/* A hairline around the grid, so the squares read as an inlay on the sheet
   rather than as the sheet itself. */
.board::after {
  content: "";
  position: absolute;
  inset: calc(var(--board-pad) - 1px);
  z-index: 2;
  border: var(--hair);
  pointer-events: none;
}

.cell {
  position: relative;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  outline-offset: -2px;
  opacity: 0;
  animation: press-in .34s cubic-bezier(.2,.7,.3,1) forwards;
}
@keyframes press-in { from { opacity: 0; transform: scale(.88); } to { opacity: 1; transform: none; } }

.cell.dark { background: var(--wash); }
.cell:disabled { cursor: default; }

/* The two corners a game is won in, marked the way a press marks a sheet: a
   register bracket folded round the corner its side is running to, in that
   side's colour. The old mark was a bullseye — a 2px ring and a soft dot, the
   only two weights on the board that belonged to nothing else.

   This one quotes what is already here: the same 3px as the selection ring, the
   last move's outline and the arrow. It is laid two bands out from the piece —
   6px, being the 3px the outline takes and the 3px of its own rule — because a
   piece that lands on its corner has just moved, and so wears that outline. Sat
   one band out, the bracket and the outline would occupy the same ring: paper's
   outline is a full square and swallowed the bracket whole, while the disc and
   the star, touching that ring only in places, left parts of it showing. Two
   bands out, the outline nests inside the bracket and all three read alike.
   The inset follows the piece's own expression, floor and all, so the three
   stay aligned at any board width. The colour is the side's own, at full
   strength: a corner belongs to a side the way a piece does, and a washed-out
   blue would be the only weakened colour on the board. */
.cell.target::before {
  content: "";
  position: absolute;
  inset: calc(max(20%, var(--outline-off)) - 6px);
  border: 3px solid transparent;
}
.cell.t-blue { color: var(--blue); }
.cell.t-red  { color: var(--red); }
/* I9 is the board's top right and A1 its bottom left: each bracket takes the
   two sides that meet at the corner that side is running to. */
.cell.t-blue::before { border-top-color: currentColor; border-right-color: currentColor; }
.cell.t-red::before  { border-bottom-color: currentColor; border-left-color: currentColor; }

.cell.sel  { box-shadow: inset 0 0 0 3px var(--ink); }

/* The last move: one arrow laid over the grid, inset by the board's padding so
   its 9x9 viewBox lines up square for square. It leaves the centre of the
   square the piece came from and points the way it went. Above the squares,
   under the board's hairline, and deaf to clicks. */
#board > svg {
  position: absolute;
  inset: var(--board-pad);
  z-index: 1;
  pointer-events: none;
}
#board > svg path {
  /* Three strokes, not a filled outline, in the same solid ink as the outline
     round the piece that landed — the two halves of one statement about the
     last move: this square emptied, that way, and the piece is now here. */
  fill: none;
  stroke: var(--ink);
  /* 3px, the outline's width, and the same 3px on every board: non-scaling
     stroke measures it in screen pixels rather than in the viewBox's squares,
     which would thin the arrow as the board narrows while the outline held. */
  stroke-width: 3;
  vector-effect: non-scaling-stroke;
  /* Grow from the arrow's own middle: without these an SVG transform is
     measured from the viewBox origin and the arrow would slide in from the
     board's top-left corner. */
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: arrow-in .34s cubic-bezier(.2,.7,.3,1);
}
@keyframes arrow-in { from { opacity: 0; transform: scale(.86); } }

/* Legal destinations: one dot, the same mark whether the square is empty or
   holds a piece to take — where the piece can go is one question, and it gets
   one answer. On an occupied square the dot lies over the piece rather than
   around it, which is also what makes it a capture: the mark is on the thing
   being taken. A cell makes no stacking context, so the z-index is what lifts
   the dot above the piece regardless of which was appended last. */
.cell .hint-dot {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
  pointer-events: none;
  width: 24%; height: 24%;
  border-radius: 50%;
  background: var(--ink);
  opacity: .34;
}
.cell:hover .hint-dot { opacity: .68; }

/* --------------------------------------------------------------- pieces */

/* The kind is carried by the silhouette alone — disc, square, four-pointed
   star — so nothing is printed on top of them, and nothing is cast under
   them either. */
/* The inset is set by the widest thing ever drawn around a piece, not by the
   piece. A fifth of the square holds on a full-size board — 4.8px clear of the
   outline at the 59px cap — but a percentage cannot keep an absolute promise:
   the narrower the board, the more of the square that 7.04px outline wants,
   and below a 35px square a fifth stops being enough. So the inset takes
   whichever is larger. Phones fall to the floor and their pieces give up the
   room; the board they are on never draws an outline into its neighbours. */
.piece {
  position: absolute;
  inset: max(20%, var(--outline-off));
  animation: land .22s cubic-bezier(.2,.8,.3,1.2);
}
@keyframes land { from { transform: scale(.74); } to { transform: none; } }

.piece .body { position: absolute; inset: 0; }

.p-blue .body { background: var(--blue); }
.p-red  .body { background: var(--red); }

.k-rock  .body { border-radius: 50%; }
.k-paper .body { border-radius: 1px; }
/* The paper square with a triangle carved out of each of its four edges, which
   leaves an X whose arms end in points at the corners. Symmetric about both
   axes, so its corners sit on the square's and all three kinds share a centre.
   The one number that shapes it is the notch depth, 26%: deeper is spikier and
   lighter, shallower reads more like a dented square. */
.k-scissors .body {
  clip-path: polygon(0 0, 50% 26%, 100% 0, 74% 50%, 100% 100%, 50% 74%, 0 100%, 26% 50%);
}

/* The piece that just moved is outlined rather than boxed in: the selection's
   rule, the same ink and the same 3px, taken round the silhouette instead of
   round the square. It is laid down before the body and stands 3px clear of
   it, so each kind repeats its own shape rather than borrowing a rectangle. */
.cell.last .piece::before {
  content: "";
  position: absolute;
  inset: -3px;
  background: var(--ink);
}
.cell.last .k-rock::before  { border-radius: 50%; }
.cell.last .k-paper::before { border-radius: 4px; } /* the body's 1px, plus the 3px it stands off */
/* The star's outline is the one that has to be solved rather than declared. A
   clip-path cannot be stroked, so this is the star's own outline drawn a second
   time, one shape larger: each of the eight edges pushed out 3px, and each of
   the four points cut off square 4.243px out — as far as the outlined square's
   corners reach, and no further.

   The cut is the whole point. A 3px band round a 35-degree tip mitres to a
   9.96px spike (3 / sin 17.53), which grew the star's silhouette by three times
   what the disc's grew by and made an outlined scissors read as a bigger piece
   than an outlined rock or paper. Cutting the tips keeps the band at a full 3px
   on every edge and holds the growth to the square's 4.24px; the cost is a
   3.61px flat across each point instead of a sharp one.

   The twelve numbers below are derived, not chosen: offset the eight edge lines
   by 3px, intersect consecutive pairs, and cut each point at 4.243px along its
   bisector. They depend on the 26% notch depth above and on the 3px band, and
   must be re-derived if either moves. The percentages are of this box, which is
   --outline-off larger than the piece on every side. */
.cell.last .k-scissors::before {
  inset: calc(-1 * var(--outline-off));
  clip-path: polygon(
    2.767px 5.322px,
    5.322px 2.767px,
    50% 26%,
    calc(100% - 5.322px) 2.767px,
    calc(100% - 2.767px) 5.322px,
    74% 50%,
    calc(100% - 2.767px) calc(100% - 5.322px),
    calc(100% - 5.322px) calc(100% - 2.767px),
    50% 74%,
    5.322px calc(100% - 2.767px),
    2.767px calc(100% - 5.322px),
    26% 50%
  );
}

/* ---------------------------------------------------------------- rails */

.rail {
  position: absolute;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.rail-files {
  left: var(--board-pad); right: var(--board-pad);
  height: var(--board-pad);
  grid-template-columns: repeat(9, 1fr);
}
.rail-files.is-top { top: 0; }
.rail-files.is-bottom { bottom: 0; }

.rail-ranks {
  top: var(--board-pad); bottom: var(--board-pad);
  width: var(--board-pad);
  grid-template-rows: repeat(9, 1fr);
}
.rail-ranks.is-left { left: 0; }
.rail-ranks.is-right { right: 0; }

/* --------------------------------------------------------------- banner */

.banner {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
}
.banner-dot {
  width: 12px; height: 12px;
  flex: none;
  background: var(--muted);
}
.banner[data-tone="blue"]  .banner-dot { background: var(--blue); border-radius: 50%; }
.banner[data-tone="red"]   .banner-dot { background: var(--red); border-radius: 50%; }
.banner[data-tone="over"]  .banner-dot { background: var(--ink); }
.banner[data-tone="think"] .banner-dot { background: var(--ink); animation: throb 1s steps(2, end) infinite; }
.banner[data-tone="bad"] { color: var(--red); }
.banner[data-tone="bad"] .banner-dot { background: var(--red); }
@keyframes throb { 0%, 100% { opacity: 1; } 50% { opacity: .2; } }

/* ------------------------------------------------------------ the cards */

.card { padding: var(--s5); }

.card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s2);
}
.card-head h2 { margin: 0; }
/* The plies-since-a-capture counter reads as data, not as a label: the same
   size and ink as the heading beside it, but untracked and in mixed case. */
.card-tag {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.sheet {
  height: 214px;
  overflow: hidden auto;
  border: var(--hair);
  border-radius: var(--r);
  background: var(--paper);
  font-family: var(--font-mono);
}
.sheet-row {
  display: grid;
  grid-template-columns: 2.6em 1fr 1fr;
  border-bottom: var(--hair);
}
.sheet-row:last-child { border-bottom: 0; }
.sheet-no {
  padding: var(--s1) var(--s2);
  background: var(--wash);
  color: var(--muted);
  text-align: right;
}
.sheet-m {
  padding: var(--s1) var(--s2);
  border: 0;
  border-left: var(--hair);
  background: transparent;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.sheet-m:hover { background: var(--wash); }
.sheet-m.now { background: var(--ink); color: var(--sheet); }
.sheet-empty {
  padding: var(--s2);
  color: var(--muted);
}

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

/* One control surface: an inked hairline on the sheet, filled solid when it is
   on or held. Buttons and segments differ only in how they are joined. */
.btn, .seg-b {
  padding: var(--s2) var(--s3);
  border: var(--edge);
  background: var(--sheet);
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 400;
  line-height: 1.2;
  color: var(--ink);
  cursor: pointer;
  transition: background .12s, color .12s;
}
.btn { flex: 1; border-radius: var(--r); }
.btn:hover:not(:disabled),
.seg-b:hover { background: var(--wash); }
.btn:active:not(:disabled) { background: var(--ink); color: var(--sheet); }
.btn:disabled {
  border-color: var(--line);
  color: var(--muted);
  cursor: default;
}

.row {
  display: flex;
  gap: var(--s2);
  margin-top: var(--s3);
}

.seg { display: flex; }
.seg-b {
  flex: 1;
  padding-inline: var(--s1);
  border-left-width: 0;
  text-align: center;
}
.seg-b:first-child {
  border-left-width: 1px;
  border-radius: var(--r) 0 0 var(--r);
}
.seg-b:last-child { border-radius: 0 var(--r) var(--r) 0; }
.seg-b.is-on { background: var(--ink); color: var(--sheet); }

/* The card's rhythm: a labelled block, then the next, all on the same step.
   The rules are the only dividers, and they get a step of their own. */
.field {
  margin: var(--s5) 0 0;
  padding: 0;
  border: 0;
}
.field legend { padding: 0 0 var(--s2); }

.rule {
  margin: var(--s5) 0;
  border: 0;
  border-top: var(--hair);
  color: inherit; /* the UA paints <hr> grey; nothing on this page is grey */
}

#pasteBox {
  width: 100%;
  margin-top: var(--s2);
  padding: var(--s2);
  border: var(--hair);
  border-radius: var(--r);
  background: var(--paper);
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--ink);
  resize: vertical;
}

/* --------------------------------------------------------------- footer */

/* A dim line under everything, on the page's own margins and in the ink the
   labels use — a colophon, not another panel. */
footer {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 24px var(--s6);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* The one link on the page. The colophon around it is set in muted ink, so the
   link takes full ink and an underline cut from the same stroke — it reads as
   the one live word on the line without a drop of colour. Hover is the wash
   every other control uses; the padding is cancelled by an equal negative
   margin, so the wash appears around the word without moving it. */
footer a {
  color: var(--ink);
  padding: 1px 3px;
  margin: 0 -3px;
  border-radius: var(--r);
  text-decoration: underline;
  text-decoration-color: currentColor;
  text-underline-offset: 2px;
  transition: background .12s;
}

footer a:hover { background: var(--wash); }

/* ---------------------------------------------------------------- rules */

/* The rules as a spec sheet: the term in caps, the sentence under it in ink,
   the four of them set two up and two across. Each term and its sentence are
   one grid item, so a long rule lengthens its own cell and nothing else. */
.rules {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s5) var(--col-gap);
  font-family: var(--font-mono);
}
.rules dd {
  margin: var(--s1) 0 0;
  text-wrap: pretty;
}
.rules .t-blue { color: var(--blue); }
.rules .t-red { color: var(--red); }

.cycle { display: grid; gap: var(--s1); }
.cycle span { display: flex; align-items: center; gap: var(--s3); }
.lg {
  width: 14px; height: 14px;
  flex: none;
  background: var(--ink);
}
.lg.k-rock { border-radius: 50%; }
.lg.k-paper { border-radius: 1px; }
.lg.k-scissors {
  clip-path: polygon(0 0, 50% 26%, 100% 0, 74% 50%, 100% 100%, 50% 74%, 0 100%, 26% 50%);
}

/* --------------------------------------------------------------- narrow */

@media (max-width: 960px) {
  /* One column, board over card, everything held to the board's width — the
     masthead included, so the title keeps the board's left edge. */
  main, .masthead, footer { max-width: calc(var(--board-max) + 48px); }
  main { grid-template-columns: minmax(0, 1fr); }
  main > * + * { margin-top: var(--col-gap); }
}

@media (max-width: 640px) {
  /* Below this the two columns are too narrow to set monospace in. */
  .rules { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .cell, .piece, .banner-dot { animation: none !important; opacity: 1 !important; }
  #board > svg path { animation: none !important; }
}
