/* ---------------------------------------------------------------------------
   The application shell: sidebar, topbar, main, footer.

   Layout copied from the GBFS webapp (../GBFS/gbfs_webapp) — its geometry,
   its collapsible rail, its component shapes. The palette is ours.

   **This file re-grounds the shell, and it can only reach the shell.** /table
   links base.css and style.css and never this one, so the felt keeps the
   palette it was tuned against and everything below is free to move without
   re-checking a single seat. base.css stays the one place a brand value is
   *defined*; what follows re-points those roles for the pages that wear
   chrome, because a solver-style product and a green baize are not the same
   surface and were being asked to share one.

   Sized in px/rem, not the cqw the table uses — the table is one fixed-aspect
   object that scales as a unit, the shell is ordinary chrome that stays put.
   --------------------------------------------------------------------------- */
:root {
  --sidebar-w: 240px;
  --topbar-h: 64px;
  --footer-h: 42px;

  /* --- ground ---------------------------------------------------------------
     Off pure black. #000 with a flat panel on it and a 1px rule around
     everything is what made this read as a spreadsheet: with no depth
     anywhere, every box carries the same weight and the only thing separating
     a headline figure from a table cell is its font size. Three grounds and a
     real shadow do that job instead, so borders can stop doing it. */
  --bg: #0b0f16;               /* the content ground */
  --surface-deep: #070a10;     /* the rail: under the page, not on it */
  --surface: #141924;          /* panels and cards */
  --surface-2: #1c2231;        /* raised again: controls, hover, zebra */
  --edge: #232a39;             /* quieter than base.css's: the shadow carries
                                  the separation now, so the line need only
                                  finish the edge rather than draw it */
  --hairline: rgba(255, 255, 255, .055);
  --muted: #8790a4;

  /* --- brand ----------------------------------------------------------------
     Two hues, not one. base.css's #069494 is a *felt* colour — deliberately
     dark, so chips and card faces sit on top of it — and on chrome it has no
     lift at all. The shell takes a brighter cut of the same hue and pairs it
     with a violet, which is the one thing that turns a monochrome readout into
     something with a front and a back: the teal marks what is yours and what
     is on, the violet is reserved for what is *rated*. Spending it anywhere
     else is what would make this decorative. */
  --primary: #10c9ae;
  --primary-ink: #3fe0c8;
  --on-primary: #04231e;
  --accent: #7b5cfa;
  --accent-ink: #a78dff;
  --accent-wash: rgba(16, 201, 174, .10);
  --hover-wash: rgba(255, 255, 255, .045);
  --danger: #ff5f78;

  /* The action verbs, which were three literals down in the hand view. They
     are the table's own `--fold` / `--call` / `--raise` lifted a little for a
     dark panel rather than felt: a fold has to mean the same colour on
     /hands/<id> as it did when it happened. */
  --act-fold: #f0576b;
  --act-call: #4aa3e8;
  --act-raise: #35b46f;

  /* --- form -----------------------------------------------------------------
     The table is sharp on purpose (`border-radius: 0`, documented there). The
     shell is the opposite case and always was — it already had 10px panels —
     so this only makes one radius scale out of the six that had accumulated. */
  --radius: 12px;
  --radius-sm: 8px;
  --radius-xs: 6px;
  --pill: 999px;

  /* A surface catches a little light at its top edge. One gradient, laid over
     the flat fill, is the whole of what stops a card looking printed. */
  --sheen: linear-gradient(180deg,
             rgba(255, 255, 255, .05), rgba(255, 255, 255, 0) 58%);
  --brand-grad: linear-gradient(135deg, var(--primary), var(--accent));
  --lift-1: 0 1px 0 rgba(255, 255, 255, .04) inset,
            0 10px 22px -16px rgba(0, 0, 0, .9);
  --lift-2: 0 1px 0 rgba(255, 255, 255, .06) inset,
            0 20px 44px -24px rgba(0, 0, 0, 1);
  --ring: 0 0 0 1px color-mix(in srgb, var(--primary) 55%, transparent),
          0 14px 34px -18px color-mix(in srgb, var(--primary) 60%, transparent);
}

/* Light keeps base.css's grounds and takes the shell's own vocabulary in its
   own values: a shadow tuned for a black page is a smudge on a white one, and
   a teal bright enough to glow on #0b0f16 fails as text on #eef1f5. */
html[data-theme="light"] {
  --surface-deep: #ffffff;
  --hairline: rgba(16, 23, 32, .08);
  --primary: #0aa892;
  --primary-ink: #06786a;
  --on-primary: #ffffff;
  --accent: #6344e0;
  --accent-ink: #5334d6;
  --accent-wash: rgba(10, 168, 146, .10);
  --hover-wash: rgba(16, 23, 32, .045);
  --act-fold: #c3283f;
  --act-call: #1668b0;
  --act-raise: #17804d;
  --sheen: linear-gradient(180deg,
             rgba(255, 255, 255, .9), rgba(255, 255, 255, 0) 58%);
  --lift-1: 0 1px 2px rgba(16, 23, 32, .05), 0 8px 20px -16px rgba(16, 23, 32, .3);
  --lift-2: 0 1px 3px rgba(16, 23, 32, .07), 0 18px 38px -24px rgba(16, 23, 32, .4);
  --ring: 0 0 0 1px color-mix(in srgb, var(--primary) 55%, transparent),
          0 14px 30px -20px color-mix(in srgb, var(--primary) 55%, transparent);
}

/* The rail, collapsed. Class lives on <html> so the override cascades into
   the `.app` grid before first paint — see the head script. */
html.sidebar-collapsed { --sidebar-w: 66px; }

/* The sidebar spans all three rows: the logo sits at the top of the rail, not
   in the topbar, and the topbar starts to the right of it. */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  grid-template-rows: var(--topbar-h) 1fr var(--footer-h);
  grid-template-areas:
    "sidebar topbar"
    "sidebar main"
    "sidebar footer";
  /* Pinned to the viewport so the 1fr middle row resolves to a fixed size and
     `.main` scrolls inside it rather than growing the document. */
  height: 100vh;
  transition: grid-template-columns 160ms ease;
}

/* --- sidebar --------------------------------------------------------------- */
/* **The rail sits *under* the page, not on it.** It was `--surface`, the same
   ground as every panel in the content beside it, which is why the whole
   window read as one flat sheet with lines drawn on it. A darker ground is
   what makes the content area look raised without giving anything a border. */
.sidebar {
  grid-area: sidebar;
  background: var(--surface-deep);
  color: var(--fg);
  border-right: 1px solid var(--edge);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

/* Exactly the topbar's height, so the divider under the brand lines up with
   the one under the topbar and reads as a single rule across the page. */
.sidebar-logo {
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 4px 10px;
  border-bottom: 1px solid var(--hairline);
}

/* Safety clamp: nothing in the brand header can overflow the rail, whatever
   the per-image rules resolve to. */
.sidebar-logo img { max-width: 100%; max-height: 52px; }

/* Expanded shows the wordmark; collapsed swaps it for the square mark. Both
   live in the DOM and CSS picks one off the <html> class, so the right one is
   on screen at first paint with no JS and no flash. */
.sidebar-logo .logo-full {
  height: 34px;
  width: auto;
  display: block;
  object-fit: contain;
}

.sidebar-logo .logo-icon {
  width: 34px;
  height: 34px;
  min-width: 34px;
  flex-shrink: 0;
  display: none;
  object-fit: contain;
}

.sidebar-nav {
  flex: 1;
  padding: 8px 12px 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sidebar-nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-decoration: none;
  font-weight: 500;
  font-size: 14px;
  transition: background 120ms ease, color 120ms ease;
}

.sidebar-nav a:hover {
  background: var(--hover-wash);
  color: var(--fg);
}

/* GBFS fills the whole rounded rect for the active item rather than using a
   leading edge; the tint is ours — and now so is the bar, because a tint alone
   on a dark rail is a rectangle you have to look for. The bar is inset into
   the pill rather than sitting outside it, so the row keeps its box and
   nothing shifts by a pixel when the section changes. */
.sidebar-nav a.active {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--primary) 22%, transparent),
    color-mix(in srgb, var(--primary) 7%, transparent) 70%,
    transparent);
  color: var(--primary-ink);
  font-weight: 600;
}

.sidebar-nav a.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 9px;
  bottom: 9px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--primary);
  box-shadow: 0 0 12px color-mix(in srgb, var(--primary) 70%, transparent);
}

/* Collapsed the label is gone, so the bar would be marking an icon it no
   longer sits beside — the pill carries it alone. */
html.sidebar-collapsed .sidebar-nav a.active::before { display: none; }

.sidebar-nav .nav-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  opacity: .85;
}

.sidebar-nav a.active .nav-icon { opacity: 1; }

/* Collapsed rail: labels hide, icons centre, the wordmark swaps out. Native
   `title` tooltips (set in JS) stand in for the hidden labels. */
html.sidebar-collapsed .sidebar-logo { gap: 0; padding-left: 0; padding-right: 0; }
html.sidebar-collapsed .sidebar-logo .logo-full { display: none; }
html.sidebar-collapsed .sidebar-logo .logo-icon { display: block; }
html.sidebar-collapsed .sidebar-nav { padding-left: 8px; padding-right: 8px; }
html.sidebar-collapsed .sidebar-nav a {
  justify-content: center;
  gap: 0;
  padding-left: 0;
  padding-right: 0;
}
html.sidebar-collapsed .sidebar-nav a > span { display: none; }

/* --- topbar ---------------------------------------------------------------- */
/* Translucent over the content it floats above, with the page showing
   through — the one place in the shell that admits there is something behind
   it. `backdrop-filter` is progressive: without it the colour-mix ground is
   already opaque enough to read against. */
.topbar {
  grid-area: topbar;
  height: var(--topbar-h);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  position: sticky;
  top: 0;
  z-index: 10;
}

.topbar-left {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* Borderless ghost button — the icon carries it, and a boxed control next to
   the page title reads heavier than a chrome affordance should. Always
   visible, so a collapsed rail can always be reopened. */
.sidebar-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.sidebar-toggle:hover {
  background: var(--surface-2);
  color: var(--fg);
}

/* No resting outline, so keyboard focus needs its own ring. */
.sidebar-toggle:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

.sidebar-toggle svg { width: 20px; height: 20px; }

/* Which chevron shows. Driven by the same <html> class as the rail, so the
   pre-paint script has it right on the first frame — no flash of the wrong
   direction on a collapsed reload. It shows the *action*, not the state:
   pointing left while expanded, right while collapsed. */
.sidebar-toggle .icon-collapse { display: block; }
.sidebar-toggle .icon-expand { display: none; }
html.sidebar-collapsed .sidebar-toggle .icon-collapse { display: none; }
html.sidebar-collapsed .sidebar-toggle .icon-expand { display: block; }

.topbar-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

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

/* Always-filled pill, so it reads as a card rather than a button. */
.topbar-user {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px;
  border: 1px solid var(--hairline);
  border-radius: var(--pill);
  background: var(--surface);
  color: inherit;
  font: inherit;
}

.topbar-user-avatar {
  display: block;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

/* --- main ------------------------------------------------------------------ */
/* One ambient wash at the top of the content, and nothing else in the shell
   spends a gradient on the background. It is what stops a long page of panels
   reading as a sheet of paper — the page has a light source, so the cards on
   it are lit from somewhere rather than merely outlined. Fixed to the scroll
   container's own top so it stays with the heading rather than sliding away
   under the first table. */
.main {
  grid-area: main;
  position: relative;
  background:
    radial-gradient(120% 55% at 50% -20%,
      color-mix(in srgb, var(--primary) 9%, transparent), transparent 70%),
    radial-gradient(90% 45% at 88% -14%,
      color-mix(in srgb, var(--accent) 8%, transparent), transparent 70%),
    var(--bg);
  background-attachment: local, local, local;
  padding: 24px;
  overflow-y: auto;
  min-width: 0;
}

/* --- footer ---------------------------------------------------------------- */
.footer {
  grid-area: footer;
  height: var(--footer-h);
  border-top: 1px solid var(--hairline);
  background: var(--surface-deep);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 20px;
  color: var(--muted);
  font-size: 12px;
}

/* ---------------------------------------------------------------------------
   Mode cards — the /play lobby. A responsive row that collapses to one column
   on a narrow main, so it survives the rail being expanded on a small window.
   --------------------------------------------------------------------------- */
.mode-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 16px;
  max-width: 760px;
}

/* The front door of the app, and the one place worth spending the most
   surface on. The wash is a radial rather than a flat fill so the card has a
   corner the light comes from, and it deepens on hover instead of the whole
   plate changing colour — a card that swaps its ground under the cursor reads
   as a state change rather than as something you are about to open. */
.mode-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 24px;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background:
    radial-gradient(120% 90% at 0% 0%,
      color-mix(in srgb, var(--primary) 11%, transparent), transparent 62%),
    var(--sheen),
    var(--surface);
  box-shadow: var(--lift-1);
  color: inherit;
  text-decoration: none;
  transition: border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
}

/* Only the playable card reacts — a locked one that lifts under the cursor is
   promising something it can't do. */
.mode-card:not(.is-locked):hover {
  border-color: color-mix(in srgb, var(--primary) 55%, var(--edge));
  transform: translateY(-3px);
  box-shadow: var(--lift-2),
              0 0 0 1px color-mix(in srgb, var(--primary) 25%, transparent);
}

.mode-card:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.mode-card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  margin-bottom: 14px;
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent);
  background: color-mix(in srgb, var(--primary) 15%, transparent);
  color: var(--primary-ink);
  box-shadow: 0 8px 20px -14px color-mix(in srgb, var(--primary) 90%, transparent);
}

.mode-card__icon svg { width: 24px; height: 24px; }

.mode-card__title {
  font-size: 16px;
  font-weight: 600;
  color: var(--fg);
}

.mode-card__desc {
  font-size: 13px;
  color: var(--muted);
}

/* Locked: readable, clearly not yet a destination. */
.mode-card.is-locked {
  opacity: .55;
  cursor: default;
}

.mode-card.is-locked .mode-card__icon {
  background: var(--surface-2);
  color: var(--muted);
}

.mode-card__tag {
  position: absolute;
  top: 16px;
  right: 16px;
  padding: 2px 8px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The one mode that counts. It gets the accent because the difference
   between practice and rated is the only thing on this page worth
   noticing twice. */
.mode-card__tag--rated {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent-ink);
  background: color-mix(in srgb, var(--accent) 15%, transparent);
}

/* ---------------------------------------------------------------------------
   Sign in. No shell — a logged-out visitor has no sidebar to be shown.
   --------------------------------------------------------------------------- */
.signin {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: var(--bg);
}

.signin-card {
  width: min(100%, 22rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 2rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--surface);
}

.signin-logo {
  height: 1.75rem;
  width: auto;
  align-self: center;
}

.signin-title {
  margin: 0 0 .25rem;
  text-align: center;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--fg);
}

.signin-field {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  font-size: .8rem;
  color: var(--muted);
}

.signin-field input {
  padding: .6rem .7rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-xs);
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  font-size: .95rem;
}

/* Neutral throughout: a sign-in form is the one page with nothing to brand,
   so focus and the button speak in the foreground colour, not the palette. */
.signin-field input:focus-visible {
  outline: 2px solid var(--muted);
  outline-offset: 1px;
  border-color: var(--muted);
}

.signin-submit {
  margin-top: .25rem;
  padding: .65rem 1rem;
  border: 0;
  border-radius: var(--radius-xs);
  background: var(--fg);
  color: var(--bg);
  font-family: inherit;
  font-size: .95rem;
  font-weight: 600;
  cursor: pointer;
}

.signin-submit:hover { opacity: .88; }
.signin-submit:focus-visible { outline: 2px solid var(--muted); outline-offset: 2px; }

/* Wrong password: state what happened, no apology, no blame. */
.signin-error {
  margin: 0;
  padding: .6rem .7rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-xs);
  background: var(--bg);
  color: var(--fg);
  font-size: .85rem;
}

/* The topbar chip, once there is a name to put in it. */
.topbar-user-name {
  font-size: .82rem;
  font-weight: 600;
  color: var(--fg);
  padding-right: .25rem;
}

.topbar-signout {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}

.topbar-signout:hover { background: var(--surface-2); color: var(--fg); }
.topbar-signout:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.topbar-signout svg { width: 17px; height: 17px; }

/* ---------------------------------------------------------------------------
   KPI row — headline figures, no plot. A stat tile is the one form that
   carries no hover layer: the number is already the whole story.
   --------------------------------------------------------------------------- */
/* Separate cards, not one box divided N ways — the same form `/hands/<id>`
   uses for its meta row (`.hand-meta`), so a figure looks the same wherever
   the section shows one. The row carries no surface of its own: each tile is
   a card, and the gap between them is space rather than a hairline.
   Full width, like the panels under it and like `.hand-meta`: a header row
   that stops short of the page it heads reads as a card gone missing. */
.kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: .8rem;
  margin: 0 0 1.4rem;
}

/* A scoreboard tile, not a report figure. The hairline of brand across the
   top is the whole of the difference: four identical outlined boxes at the
   head of a page is the shape every analytics dashboard opens with, and one
   lit edge is enough to say these are *your* numbers rather than a summary of
   somebody's data. It is drawn as a pseudo-element inside the radius so the
   card keeps its corners. */
.kpi {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .45rem;
  padding: 1.15rem 1.25rem 1.3rem;
  background: var(--sheen), var(--surface);
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  box-shadow: var(--lift-1);
  overflow: hidden;
}

.kpi::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: var(--brand-grad);
  opacity: .85;
}

/* A tile with nothing in it yet is not one of your numbers. */
.kpi.is-empty::before { opacity: .18; }

.kpi__label {
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  min-height: 1em;                 /* the blank tile keeps the row aligned */
}

/* Proportional figures, deliberately not tabular: `tabular-nums` gives every
   digit the width of a zero, which reads loose at this size. Tabular belongs
   in columns that must line up vertically, which these do not. */
.kpi__value {
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--fg);
  font-variant-numeric: proportional-nums;
}

.kpi__unit {
  font-size: .6em;
  font-weight: 500;
  color: var(--muted);
  margin-left: .1em;
}

/* Reserved slot: present, obviously not yet carrying a number. */
.kpi.is-empty .kpi__value { color: var(--dim, var(--muted)); opacity: .5; }

/* ---------------------------------------------------------------------------
   Section tabs

   A row of links, not a widget: each tab is its own URL and the active one is
   decided server-side from `request.endpoint` (see templates/_tabs.html), so
   there is no JS here and no state to get out of step with the page.

   **A segmented control rather than an underline.** They were underlined tabs
   sitting on a full-width rule, which is the document gesture — a heading with
   sections beneath it. A pill group is the switch, and it says the two views
   are alternatives you flip between rather than chapters you read down. The
   group is inline, so it takes the width of its own labels instead of ruling
   off the page.
   --------------------------------------------------------------------------- */
/* The row the tab pill and the scope switch share: tabs left, scope right.
   The margin that used to sit on `.tabs` lives here, so the two controls are
   one band above the content rather than two things that happen to be near
   each other — and a page that draws no scope switch (a single game, see
   _tabs.html) keeps exactly the spacing it had. `wrap` because the switch is
   four labels wide and the rail is collapsed by hand at any width: this shell
   has no media queries, so the row has to fold on its own. */
.tabbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  margin: 0 0 1.5rem;
}

.tabs {
  display: inline-flex;
  gap: 2px;
  margin: 0;
  padding: 3px;
  border: 1px solid var(--edge);
  border-radius: var(--pill);
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  box-shadow: var(--lift-1);
}

.tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .42rem 1rem;
  border-radius: var(--pill);
  color: var(--muted);
  font-size: .84rem;
  font-weight: 600;
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease;
}

.tabs__tab:hover { color: var(--fg); background: var(--hover-wash); }

/* The glyph recedes until the tab is the one you are on, like the rail's. It
   is decoration beside a label that already says everything, so it never
   carries the meaning on its own. */
.tabs__icon { width: 15px; height: 15px; opacity: .65; }
.tabs__tab.active .tabs__icon { opacity: 1; }

/* Colour alone must not be what says which tab you are on, so the underline
   carries it and `aria-current` says it out loud to a screen reader. */
.tabs__tab.active {
  color: var(--on-primary);
  background: var(--primary);
  box-shadow: 0 6px 16px -8px color-mix(in srgb, var(--primary) 80%, transparent);
}

.tabs__tab.active:hover { color: var(--on-primary); background: var(--primary); }

.tabs__tab:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------------------
   Hand history — a list of hands, and one hand in full.

   Numbers here live in columns that must line up down the page, which is the
   one case `tabular-nums` is for (the KPI tiles above deliberately don't).
   --------------------------------------------------------------------------- */
/* A card that is lit rather than outlined: the sheen over the fill gives it a
   top edge, the shadow gives it a bottom one, and the border is left to finish
   the shape instead of being the only thing describing it. */
.panel {
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--sheen), var(--surface);
  box-shadow: var(--lift-1);
  overflow: hidden;
}

/* The header takes its own ground rather than a rule under it. A hairline
   across a card is the spreadsheet gesture; a darker strip is the one every
   solver interface uses, and it reads as a header at a glance instead of on
   inspection. */
.panel__title {
  margin: 0;
  padding: .85rem 1.1rem;
  background: color-mix(in srgb, var(--surface-2) 55%, transparent);
  border-bottom: 1px solid var(--hairline);
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

.panel__note {
  margin: 0;
  padding: .8rem 1.1rem;
  border-top: 1px solid var(--hairline);
  font-size: .78rem;
  color: var(--muted);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .85rem;
}

.data-table th {
  padding: .7rem 1.1rem;
  border-bottom: 1px solid var(--hairline);
  text-align: left;
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* **Rows are separated by tone, not by rules.** A line under every row is what
   makes a list look like a spreadsheet — and it is doing the same job twice,
   since the alternating ground already says where one row ends. Taller rows
   with no lines through them is the whole difference between reading a table
   and scanning one. */
.data-table td {
  padding: .68rem 1.1rem;
  vertical-align: middle;
}

.data-table tbody tr:nth-child(even) td {
  background: color-mix(in srgb, var(--surface-2) 42%, transparent);
}

.data-table tbody tr:hover td,
.data-table tbody tr:nth-child(even):hover td { background: var(--surface-2); }

/* The row is clickable, so it has to say so. */
.data-table tbody tr[onclick] { cursor: pointer; }

.data-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Two header rows — labels, then the filter controls under them — so the
   rule between the head and the body is drawn once, at the bottom of the
   whole head, rather than through the middle of it. */
.data-table thead tr:not(:last-child) th { border-bottom: 0; }

/* A sortable heading. The anchor takes the cell's padding so the whole box is
   the target rather than the six characters of the word. */
.data-table th.sortable { padding: 0; }
.data-table th.sortable a { display: block; padding: .7rem 1.1rem; }
.data-table th.sortable.num a { text-align: right; }
.data-table th.sortable a:hover { color: var(--fg); }

/* The caret is drawn on both columns and faded on the one not in force: it
   has to say which way the list is sorted *and* that the other column can be,
   and keeping it in the DOM either way is what stops the header widths
   jumping as the sort moves. */
.sort-caret { margin-left: .35rem; opacity: .3; }
.data-table th[aria-sort] { color: var(--fg); }
.data-table th[aria-sort] .sort-caret { opacity: 1; color: var(--primary-ink); }

/* The filter row. Its cells carry no heading of their own, so the uppercase
   tracking the labels above them wear would only make the controls harder to
   read. */
.data-table tr.filters th {
  padding-top: .1rem;
  padding-bottom: .7rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
}

.filter-select {
  width: 100%;
  padding: .3rem .45rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-xs);
  background: var(--surface-2);
  color: var(--muted);
  font: inherit;
  font-size: .74rem;
}
.filter-select:focus-visible { outline: none; border-color: var(--primary); }

/* A filter that is doing something says so. Without it a list showing a
   fraction of itself looks like a list with almost nothing in it. */
.filter-select.is-set { border-color: var(--primary); color: var(--primary-ink); }

/* Submitted by hand, so the button belongs on the same row as the controls it
   applies — at the end of it, where the eye already is after the last one. */
.filter-apply {
  padding: .3rem .7rem;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--fg);
  font: inherit;
  font-size: .74rem;
  cursor: pointer;
}
.filter-apply:hover { border-color: var(--primary); color: var(--primary-ink); }
.filter-apply:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Filtered down to nothing: a row of its own rather than the page's empty
   note, because the controls that got you here have to stay on screen. */
.data-table tr.is-empty td { padding: 1.6rem 1.1rem; color: var(--muted); }
.data-table tr.is-empty a { color: var(--primary-ink); text-decoration: underline; }

.data-table .strong { color: var(--fg); font-weight: 600; }
.data-table .muted  { color: var(--muted); }
.data-table a { color: inherit; text-decoration: none; }
.data-table a:hover { color: var(--primary-ink); }

/* Your own row, marked without shouting — but it has to beat the zebra above
   it, or every other one of your rows loses the mark. */
.data-table tr.is-you td,
.data-table tr.is-you:nth-child(even) td { background: var(--accent-wash); }
.data-table tr.is-you .strong { color: var(--primary-ink); }

.net--up   { color: var(--primary-ink); font-weight: 600; }
.net--down { color: var(--danger); font-weight: 600; }

.tag {
  margin-left: .45rem;
  padding: .12rem .45rem;
  border: 1px solid var(--edge);
  border-radius: var(--pill);
  font-size: .62rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- cards in the shell ----------------------------------------------------
   None here are drawn any more: everywhere a card is *data in a row* it is
   written instead (`.pip`, from templates/_cards.html). The face images stay
   for the table, which is the one place a card is meant to be looked at. */

/* --- one hand -------------------------------------------------------------- */
/* Five cards, not one box divided five ways. Full width, like the panels under
   it — a header row that stops short of the page it heads reads as a stray
   card — with `auto-fit` spreading them across whatever width there is. */
.hand-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: .8rem;
  /* Its own space beneath it. This used to come from the board strip that sat
     between this row and the Action panel; with that gone the cards butted
     straight up against the panel. */
  margin: 0 0 1.4rem;
}

/* One figure, one card. The label sits above the value with real space between
   them, so the two never read as a single run-on string. */
.meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: .4rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--edge);
  border-radius: 10px;
  background: var(--surface);
}

.meta__label {
  font-size: .66rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.meta__value {
  display: block;
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.15;
  color: var(--fg);
}

/* Seats and action side by side while there's room, stacked when there isn't.
   `auto-fit` rather than a media query — the shell has none, and the rail can
   be collapsed at any width. */
.hand-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  gap: 1.1rem;
  align-items: start;
}

/* --- paging ---------------------------------------------------------------- */
.pager {
  display: flex;
  flex-wrap: wrap;
  gap: .8rem 1.2rem;
  align-items: center;
  justify-content: space-between;
  margin-top: 1rem;
}

.pager__note { margin: 0; font-size: .78rem; color: var(--muted); }
.pager__note b { color: var(--fg); font-weight: 600; }

.pager__links { display: flex; align-items: center; gap: .9rem; font-size: .82rem; }
.pager__links a { color: var(--fg); text-decoration: none; }
.pager__links a:hover { color: var(--primary-ink); }
.pager__links .is-disabled { color: var(--muted); opacity: .45; }
.pager__count { color: var(--muted); font-variant-numeric: tabular-nums; }

.ghost-link { color: var(--muted); text-decoration: none; font-size: .82rem; }
.ghost-link:hover { color: var(--fg); }

.empty-note { color: var(--muted); font-size: .9rem; }
.empty-note a { color: var(--primary-ink); }

/* ---------------------------------------------------------------------------
   Theme switch. Same shape as the sidebar toggle, and driven by the same
   pre-paint attribute: CSS picks the glyph, JS only flips the attribute.
   The icon shows what you'd get, not where you are.
   --------------------------------------------------------------------------- */
.theme-toggle {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.theme-toggle:hover { background: var(--surface-2); color: var(--fg); }
.theme-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.theme-toggle svg { width: 18px; height: 18px; }

.theme-toggle .icon-to-light { display: block; }
.theme-toggle .icon-to-dark  { display: none; }
html[data-theme="light"] .theme-toggle .icon-to-light { display: none; }
html[data-theme="light"] .theme-toggle .icon-to-dark  { display: block; }

/* ---------------------------------------------------------------------------
   Configuring a game — the form behind Play → Training.
   --------------------------------------------------------------------------- */
.config { max-width: 60rem; }

/* Full width. The structure cards here are rows rather than columns, and a
   row cut off at two thirds of the page is a row with wasted space in it. */
.config--wide { max-width: none; }

.config__group {
  margin: 0 0 1.4rem;
  padding: 1.1rem 1.2rem 1.3rem;
  border: 1px solid var(--edge);
  border-radius: 10px;
  background: var(--surface);
}

.config__legend {
  padding: 0 .5rem;
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.config__note {
  margin: .9rem 0 0;
  font-size: .78rem;
  color: var(--muted);
}

.structure-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: .7rem;
}

/* The radio itself is hidden; the whole card is the control, and `:has` is
   what keeps the checked state in CSS rather than needing a script. */
.structure {
  display: block;
  padding: .9rem 1rem 1rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--sheen), var(--surface-2);
  cursor: pointer;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}

.structure input { position: absolute; opacity: 0; pointer-events: none; }
.structure:hover { border-color: var(--muted); }

.structure:has(input:checked) {
  border-color: var(--primary);
  background: var(--sheen), var(--accent-wash), var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--primary),
              0 10px 26px -16px color-mix(in srgb, var(--primary) 80%, transparent);
}

.structure:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }

.structure__name { display: block; font-size: 1.05rem; font-weight: 600; color: var(--fg); }
.structure__meta { display: block; margin-top: .1rem; font-size: .76rem; color: var(--muted); }

.structure__facts {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .2rem .7rem;
  margin: .75rem 0 0;
  font-size: .78rem;
}

.structure__facts dt { color: var(--muted); }
.structure__facts dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }

.config__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: 1rem;
}

.config__field { display: flex; flex-direction: column; gap: .35rem; font-size: .82rem; }
.config__field > span { color: var(--fg); font-weight: 600; }
.config__field small { color: var(--muted); font-size: .74rem; }

.config__field input {
  padding: .6rem .75rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--fg);
  font-family: inherit;
  font-size: .95rem;
  font-variant-numeric: tabular-nums;
}

.config__field input:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
}

.config__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; }

/* The one control on a page that starts something. It carries the brand
   gradient and a shadow in its own colour, which is the difference between a
   coloured rectangle and a button you can see is the way forward. */
.btn-primary {
  display: inline-block;
  text-decoration: none;            /* it is a button even when it is an <a> */
  padding: .68rem 1.6rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--brand-grad);
  color: #fff;
  font-family: inherit;
  font-size: .95rem;
  font-weight: 700;
  letter-spacing: .01em;
  cursor: pointer;
  box-shadow: 0 10px 24px -12px color-mix(in srgb, var(--primary) 85%, transparent);
  transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
}

.btn-primary:hover {
  filter: brightness(1.08);
  transform: translateY(-1px);
  box-shadow: 0 14px 30px -12px color-mix(in srgb, var(--primary) 95%, transparent);
}

.btn-primary:active { transform: none; }

/* The same size and weight as .btn-primary, and only the fill is different.
   Leaving a table is not a lesser action than starting one — it is the other
   half of the same decision — so it gets the same target and the same
   presence, without competing for the eye. */
.btn-secondary {
  padding: .68rem 1.6rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--sheen), var(--surface-2);
  color: var(--fg);
  font-family: inherit;
  font-size: .95rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}

.btn-secondary:hover { border-color: var(--muted); }
.btn-secondary:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.btn-primary:focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.btn-primary[disabled]:hover { transform: none; filter: none; }

.notice {
  display: flex;
  flex-wrap: wrap;
  gap: .9rem 1.2rem;
  align-items: center;
  justify-content: space-between;
  max-width: 60rem;
  margin: 0 0 1.4rem;
  padding: .9rem 1.1rem;
  border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--edge));
  border-radius: var(--radius);
  background: var(--accent-wash);
  font-size: .85rem;
}

.notice b { color: var(--primary-ink); }

.form-error {
  max-width: 60rem;
  margin: 0 0 1.2rem;
  padding: .75rem 1rem;
  border: 1px solid var(--danger);
  border-radius: 8px;
  color: var(--danger);
  font-size: .85rem;
}

/* --- how a game ended ------------------------------------------------------ */
.place { font-weight: 600; color: var(--fg); }
.place--won {
  padding: .12rem .5rem;
  border-radius: var(--pill);
  background: var(--brand-grad);
  color: #fff;
  font-size: .72rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.place--live { color: var(--primary-ink); }

.after-panel { margin: 1.1rem 0 0; }

/* Both brand files are pure-white cut-outs (`logo.png`, `logo-icon.png`), so
   they need a dark ground. The sidebar is a dark surface in the dark theme and
   a light one in the light theme — without this the wordmark and the square
   mark are simply invisible in light mode. Inverting a pure-white alpha mask
   gives a pure-black one; it is the same mark, not an approximation. Ship a
   real dark-ink asset and this rule goes away. */
html[data-theme="light"] .sidebar-logo img,
html[data-theme="light"] .signin-logo {
  filter: invert(1);
}

/* ---------------------------------------------------------------------------
   Starting-hand matrix — 13x13, pairs on the diagonal, suited above it,
   offsuit below. The layout every poker player already knows; don't invent
   another one.

   Win/lose is **polarity**, so the palette is diverging: two hues that read as
   opposite with nothing at the midpoint. Not a single-hue ramp — that says
   "magnitude", and it would make -30 BB and +30 BB look like the same cell.
   The scale is symmetric about zero and the fill curve is a square root, both
   computed in the route, or the colour misreports which cells are winners.

   It is a card, sized to about a third of the content width. At that size a
   cell is ~30px and holds its label alone; the figure behind it is in the
   hover card below, which is also the only key the grid has — there is no
   legend strip, so a colour is read by pointing at it.
   --------------------------------------------------------------------------- */
/* The Overview tab: the curve takes the room, the heatmap keeps its size.
   They wrap rather than shrink — a 13-column grid squeezed to fit beside
   something else stops being readable long before it stops fitting. */
.analyze-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.4rem;
  margin: 0 0 1.4rem;
}

.matrix-card {
  flex: 0 1 28rem;
  /* .panel clips, and the hover card has to leave the cell it belongs to. */
  overflow: visible;
}

/* Tight to the card. `.panel__title` is shared vocabulary sized for pages of
   prose, so it is overridden here rather than edited — and both values have to
   move together, or the title stops lining up with the grid's left edge, which
   is the one alignment a reader of this card actually sees. */
/* The title and the view switch share the card's top edge. `.panel__title`
   keeps its own padding, so the row is aligned by the header rather than by
   nudging either half. */
.matrix-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  padding-right: .6rem;
}

.matrix-head {
  background: color-mix(in srgb, var(--surface-2) 55%, transparent);
  border-bottom: 1px solid var(--hairline);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* The card is the one panel that cannot clip (the hover card has to leave it),
   so the header ground is on the row and the title is text again — otherwise
   the fill would run past the rounded corners it sits in. */
.matrix-card .panel__title {
  padding: .7rem .6rem;
  background: none;
  border-bottom: 0;
}

/* A segmented control made of links, because that is what it is: two URLs. It
   is deliberately quiet — the board underneath is the thing being looked at,
   and a switch that shouted would compete with 169 cells for the same eye. */
.seg {
  display: inline-flex;
  flex: none;
  padding: 2px;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: var(--surface-2);
}

.seg__item {
  padding: .18rem .6rem;
  border-radius: 999px;
  color: var(--muted);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .01em;
  text-decoration: none;
  white-space: nowrap;
}

.seg__item:hover { color: var(--fg); }

/* The chosen half carries a filled pill: the state has to survive a page load
   with no script, and colour alone would leave a keyboard reader guessing —
   which is what `aria-current` in the markup is for. */
.seg__item.is-on {
  background: var(--primary);
  color: var(--on-primary);
  box-shadow: 0 5px 14px -8px color-mix(in srgb, var(--primary) 85%, transparent);
}
.matrix { padding: .6rem; }

.matrix__grid {
  display: grid;
  grid-template-columns: repeat(13, 1fr);
  gap: 2px;                            /* a surface gap, never a border */
}

.mcell {
  position: relative;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  background: var(--surface-2);
  font-size: clamp(.52rem, .68vw, .66rem);
  line-height: 1;
  cursor: default;
}

/* `--mix` is how far this cell is along its half of the scale. Mixing against
   the surface rather than fading with opacity keeps the label legible at every
   step — and `MATRIX_MAX_MIX` is where it is because past it the label falls
   under 4.5:1 against --fg in the dark theme. */
.mcell--win  { background: color-mix(in oklab, var(--primary) var(--mix), var(--surface-2)); }
.mcell--lose { background: color-mix(in oklab, var(--danger) var(--mix), var(--surface-2)); }

/* The Played board. One hue, because a frequency has no polarity to encode —
   and its own class rather than borrowing `--win`, which on this board means
   "won money" and would be a lie about what the colour is measuring. Same
   ramp, so the two views read as the same chart asked a different question. */
.mcell--freq { background: color-mix(in oklab, var(--primary) var(--mix), var(--surface-2)); }

.mcell__label { color: var(--fg); font-weight: 600; }

/* Too few hands to mean anything. Faded rather than hidden — an empty cell and
   one you have played twice are different facts.

   **The fade must not be `opacity` on the cell.** Opacity applies to the whole
   subtree, and the hover card is a child, so a thin cell handed its card the
   same 50% and the card came out see-through against the board behind it. So
   the label is faded and the fill is mixed half as far toward the surface,
   which reaches the same place by two routes that stop at the cell.

   `.is-empty` may keep its opacity: an empty cell has no card to fade. */
.mcell.is-thin > .mcell__label { opacity: .5; }
.mcell.is-thin.mcell--win  { background: color-mix(in oklab, var(--primary) calc(var(--mix) / 2), var(--surface-2)); }
.mcell.is-thin.mcell--lose { background: color-mix(in oklab, var(--danger)  calc(var(--mix) / 2), var(--surface-2)); }
.mcell.is-thin.mcell--freq { background: color-mix(in oklab, var(--primary) calc(var(--mix) / 2), var(--surface-2)); }
.mcell.is-empty { background: var(--surface); opacity: .45; }

/* The cell carries no figure, so hovering is how you read one — which makes
   this card the chart's only key, not a nicety. It is a real element rather
   than a `::after` on the cell: three lines with their own weights and
   colours, and `--surface-2` with a shadow so it reads as something lifted off
   the board instead of another cell. Not `title` either — a native tooltip
   waits a second, cannot be styled, and would look like a different product
   next to this one. */
.mcell:hover { outline: 2px solid var(--fg); outline-offset: -2px; z-index: 20; }

.mcell__card {
  display: none;
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  min-width: 8.5rem;
  padding: .5rem .65rem;
  border: 1px solid var(--edge);
  border-radius: 8px;
  /* An opaque ground of its own: the card sits over the board, and a colour
     that let any of it through would be unreadable at this size. */
  background: var(--surface-2);
  box-shadow: 0 10px 26px rgb(0 0 0 / .45);
  opacity: 1;
  text-align: left;
  line-height: 1.35;
  white-space: nowrap;
  pointer-events: none;       /* or the card steals its own trigger */
}

.mcell:hover .mcell__card { display: block; }

/* Slice colours: one hue ramped across the seats **in order of action**, which
   preflop is exactly `engine.POSITIONS` — so pale is early and full is late,
   and the colour carries something rather than being nine arbitrary paints.
   Position is ordered data, not nominal, which is why this is a ramp and not a
   categorical palette.

   The pale end stops at 45% rather than running to nothing: mixed any further
   into the card's own ground the first slice stops being visible against it,
   and an invisible slice is a lie about the shape of the circle. Declared on
   the card so both the pie and its legend swatches read the same nine. */
.mcell__card {
  --pos-utg:      color-mix(in oklab, var(--primary) 45%, var(--surface-2));
  --pos-utgplus1: color-mix(in oklab, var(--primary) 52%, var(--surface-2));
  --pos-mp:       color-mix(in oklab, var(--primary) 59%, var(--surface-2));
  --pos-lj:       color-mix(in oklab, var(--primary) 66%, var(--surface-2));
  --pos-hj:       color-mix(in oklab, var(--primary) 73%, var(--surface-2));
  --pos-co:       color-mix(in oklab, var(--primary) 80%, var(--surface-2));
  --pos-btn:      color-mix(in oklab, var(--primary) 87%, var(--surface-2));
  --pos-sb:       color-mix(in oklab, var(--primary) 93%, var(--surface-2));
  --pos-bb:       var(--primary);
}

.mcell__where {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .45rem;
  padding-top: .45rem;
  border-top: 1px solid var(--hairline);
}

/* The gradient itself is inline — it is data, not style. The ring is here so
   the circle has an edge of its own against the card. */
.mcell__pie {
  flex: none;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--edge);
}

.mcell__legend { display: flex; flex-direction: column; gap: .1rem; }

/* One row per seat played from. The count is the point — a share of two hands
   is a share of two hands, and the circle alone would not say so. */
.mcell__key {
  display: flex;
  align-items: center;
  gap: .3rem;
  font-size: .64rem;
  line-height: 1.5;
  color: var(--muted);
}

.mcell__key i {
  width: .5rem;
  height: .5rem;
  flex: none;
  border-radius: 2px;
}

.mcell__key b {
  min-width: 2.4rem;
  color: var(--fg);
  font-weight: 600;
  letter-spacing: .02em;
}

.mcell__key span { font-variant-numeric: tabular-nums; }

.mcell__card-hand {
  display: block;
  font-size: .95rem;
  font-weight: 700;
  color: var(--fg);
}

.mcell__card-bb {
  display: block;
  font-size: .88rem;
  font-variant-numeric: tabular-nums;
}

.mcell__card-n {
  display: block;
  font-size: .72rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The top rows have nothing above them inside the card, so their card drops
   below the cell instead of over the panel's own title. **Two rows, not one**:
   the Played board's card carries a pie and a legend under the figures, which
   is roughly three cells tall, so a second-row card opening upward clears the
   grid and lands on the heading. Dropping below costs the other two boards
   nothing — the card is simply under the cell instead of over it. */
.matrix__grid > .mcell:nth-child(-n+26) .mcell__card {
  bottom: auto;
  top: calc(100% + 8px);
}

/* Same for the columns at either edge: a centred card would hang off the side
   of the panel, so the outer two columns anchor to their own edge instead. */
.matrix__grid > .mcell:nth-child(13n+1) .mcell__card,
.matrix__grid > .mcell:nth-child(13n+2) .mcell__card {
  left: 0;
  transform: none;
}

.matrix__grid > .mcell:nth-child(13n) .mcell__card,
.matrix__grid > .mcell:nth-child(13n+12) .mcell__card {
  left: auto;
  right: 0;
  transform: none;
}



/* Cards as text — the four-colour deck. At list sizes a 24px picture of a card
   is unreadable and a two-character label is not, so anywhere a card is *data
   in a row* it is written rather than drawn. */
.pips { display: inline-flex; gap: .28rem; }

.pip {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  white-space: nowrap;
}

.pip--s { color: var(--suit-s); }
.pip--h { color: var(--suit-h); }
.pip--d { color: var(--suit-d); }
.pip--c { color: var(--suit-c); }
.pip--back { color: var(--muted); opacity: .6; }

/* The board on a hand's own page: the same text deck, sized to lead the page.
   It replaced 56px face images, which the seats table below could not match at
   24px — a card you have to squint at is not showing you anything. */
.pips--board { gap: .5rem; font-size: 1.5rem; letter-spacing: .01em; }
.pips--board .pip--back { font-size: .8em; }

/* --- cards drawn as cards --------------------------------------------------
   Portrait, rank stacked over suit, dark ink on a light face — the shape and
   the colours a playing card actually has. The earlier version was a square
   text badge, which read as a label rather than a card.

   Sized in `em` throughout, so one font-size on the container scales the whole
   card. Face and ink are theme-independent on purpose: see base.css.
   --------------------------------------------------------------------------- */
.pcards { display: inline-flex; gap: .22em; vertical-align: middle; }

.pcard {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .05em;
  width: 2.1em;
  aspect-ratio: 5 / 7;                 /* the proportions of a real card */
  border: 1px solid var(--card-edge);
  border-radius: .22em;
  background: var(--card-face);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .28);
  line-height: 1;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.pcard__rank { font-size: .92em; }
.pcard__suit { font-size: .86em; }

.pcard--s { color: var(--card-s); }
.pcard--h { color: var(--card-h); }
.pcard--d { color: var(--card-d); }
.pcard--c { color: var(--card-c); }

/* Face down: a back, not an empty slot. The redaction is `for_viewer()`'s; this
   is only what it looks like. */
.pcard--down {
  border-color: #2c3a52;
  background:
    repeating-linear-gradient(45deg,
      rgba(255, 255, 255, .10) 0 2px, transparent 2px 5px),
    #1d2942;
  box-shadow: none;
}

/* The board leads its page, so it gets the size real cards would have. */
.pcards--board { font-size: 1.5rem; gap: .28em; }

/* ---------------------------------------------------------------------------
   Action board — one column per street, and inside each a conversation: who
   acted, what they did, and (for your own) how it rated.

   Four columns rather than one long list, because the streets are the thing a
   reader navigates by: "what happened on the turn" is a glance rather than a
   scan. Full width for the same reason — squeezed beside another panel this
   becomes four columns of one word each.
   --------------------------------------------------------------------------- */
.board-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1px;                            /* the rule between columns is the gap */
  background: var(--edge);
  border-top: 1px solid var(--edge);
}

.scol { display: flex; flex-direction: column; background: var(--surface); }

.scol__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .7rem .9rem;
  border-bottom: 1px solid var(--hairline);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.scol__n {
  min-width: 1.4em;
  padding: .05rem .3rem;
  border-radius: 999px;
  background: var(--surface-2);
  text-align: center;
  font-size: .95em;
  font-variant-numeric: tabular-nums;
}

.scol__body { display: flex; flex-direction: column; gap: .35rem; padding: .7rem; flex: 1; }

/* A street the hand never got to. Present, and obviously not part of it. */
.scol.is-unreached { background: var(--bg); }
.scol.is-unreached .scol__head { opacity: .5; }

.scol__empty { margin: 0; padding: .3rem .2rem; font-size: .76rem; color: var(--muted); opacity: .7; }

/* --- one action, laid out like a message ----------------------------------- */
.act {
  display: grid;
  /* face | who and what | the hand, when there is one to show */
  grid-template-columns: auto 1fr auto;
  gap: .5rem;
  align-items: start;
  padding: .45rem .55rem;
  border: 1px solid var(--edge);
  border-radius: 8px;
  background: var(--surface-2);
}

/* Yours, marked the way the table's chat marks yours. */
.act.is-you { border-color: var(--primary); background: var(--accent-wash); }

.act__face { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; }

.act__body { min-width: 0; display: flex; flex-direction: column; gap: .12rem; }

.act__who {
  display: flex;
  align-items: center;
  gap: .3rem;
  font-size: .78rem;
  font-weight: 600;
  color: var(--fg);
}

/* A row, so the badge sits beside the action rather than stretching under it:
   as a block child of the flex *column* above, it was a full-width bar. */
.act__what {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .35rem;
  font-size: .82rem;
  color: var(--muted);
}
.act__what b { color: var(--fg); font-variant-numeric: tabular-nums; font-weight: 600; }

/* The three decisions keep the colours they have on the table itself. */
/* Tokens now, not literals — the rule the stylesheets already follow, and the
   one place in the shell that speaks the table's own colour language: a fold
   has to be the same colour on a hand's page as it was when it happened. */
.act__verb--fold  { color: var(--act-fold); }
.act__verb--check,
.act__verb--call  { color: var(--act-call); }
.act__verb--bet,
.act__verb--raise { color: var(--act-raise); }


/* SB / BB / D, the markers the table puts on a seat. */
.seatmark {
  padding: .02rem .28rem;
  border: 1px solid var(--edge);
  border-radius: 3px;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--muted);
}

/* --- how the hand ended ---------------------------------------------------
   Below the action on the page, for the reason the template gives. The table
   itself is the shell's `.data-table`; only what that does not cover is here.
   --------------------------------------------------------------------------- */

/* It sits at the foot of the column the hand ended on, so it is bounded by
   that column's width — around 13rem. Everything here is sized for that: one
   figure per row, no second unit beside it, and the sentence above wrapping
   rather than truncating. */
.sresult {
  border-top: 1px solid var(--hairline);
  background: var(--surface-2);
  padding: .6rem .7rem .7rem;
}

.sresult__lede {
  margin: 0 0 .5rem;
  color: var(--muted);
  font-size: .76rem;
  line-height: 1.45;
}

.sresult__lede b { color: var(--fg); font-weight: 600; }

.sresult__list { margin: 0; padding: 0; list-style: none; }

.sresult__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem;
  padding: .16rem 0;
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
}

.sresult__who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
}

/* The two rows worth finding at a glance: the account reading the page, and
   whoever took the pot. Weight rather than a second colour — the figures on
   these rows already carry one. */
.sresult__row.is-you .sresult__who { color: var(--primary-ink); font-weight: 600; }
.sresult__row.is-winner .sresult__who { color: var(--fg); font-weight: 600; }

/* The cards that arrived on this street, above the actions taken on it — what
   the players were looking at when they acted. */
.scol__cards {
  padding: .6rem .7rem .1rem;
  font-size: .82rem;                   /* .pcard is sized in em, so this scales it */
}

/* Panels stacked down a page need air between them — without it the rules of
   two adjacent cards read as one divided box. */
.panel + .panel { margin-top: 1.6rem; }
.panel + .after-panel { margin-top: 1.6rem; }

/* The acting player's hand, on preflop rows — at the right-hand end, where a
   chat layout puts an attachment. What arrives before the flop *is* the hole
   cards, so the preflop column shows them where the others show the board.
   Only rendered for hands the page is allowed to show, so there are no card
   backs here. */
.act__hand { align-self: center; font-size: .6rem; }

/* ---------------------------------------------------------------------------
   Cumulative big blinds — one series, so one shape and no legend; the card's
   title names it. The ink is split at the zero line by a two-stop gradient
   (see the template): `--primary` above, `--danger` below, which is the only
   thing on this chart that has to be true at a glance.

   The plot keeps its aspect ratio, so the stroke and the end marker scale with
   the card rather than being smeared sideways by it.
   --------------------------------------------------------------------------- */
.curve-card {
  flex: 1 1 100%;            /* full width; the heatmap wraps beneath it */
  min-width: 0;              /* or a long path refuses to let the card shrink */
}

.curve-card .panel__title { padding: .8rem 1rem; }
.curve { padding: 1.1rem 1rem 1.2rem; }

.curve__plot { display: block; width: 100%; height: auto; overflow: visible; }

/* The scale, and it stays out of the way: hairlines thinner than the series
   they sit behind, and type at caption size. The labels live in the viewBox,
   so they scale with the card — which is what keeps them in register with the
   rules they belong to, and why the size is in user units and not a rem. */
.curve__grid { stroke: var(--hairline); stroke-width: .75; }

.curve__ylabel,
.curve__xlabel {
  fill: var(--muted);
  font-family: var(--font);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
}

.curve__ylabel { text-anchor: end; dominant-baseline: middle; }
.curve__xlabel { text-anchor: middle; }

.curve__ink-up { stop-color: var(--primary); }
.curve__ink-down { stop-color: var(--danger); }

/* Densest at the far edge of each half, gone at the zero line. The fill then
   encodes distance from level, which is the thing the chart is about; a flat
   wash encodes only "there is area here", and ends in a hard band of colour
   along the baseline that reads as a second, meaningless line. */
.curve__fill-up { stop-color: var(--primary); stop-opacity: .32; }
.curve__fill-off { stop-color: var(--primary); stop-opacity: 0; }
.curve__fill-off-down { stop-color: var(--danger); stop-opacity: 0; }
.curve__fill-down { stop-color: var(--danger); stop-opacity: .32; }

.curve__area {
  fill: url(#curve-fill);
  stroke: none;
  /* The path is closed along the baseline, so a curve that crosses zero encloses
     loops of opposite winding. `evenodd` keeps those from cancelling. */
  fill-rule: evenodd;
}

/* Thin. The line is the subject, and weight is not how it says so — the two
   hues and the empty space around it already do. */
.curve__line {
  fill: none;
  stroke: url(#curve-ink);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Where level is. A shade stronger than the grid so it reads as the reference
   rather than as another tick, and solid, never dashed — dashed reads as a
   projection or a threshold. */
.curve__zero { stroke: var(--edge); stroke-width: 1; }

/* The one point worth marking, ringed in the surface so it stays legible
   wherever the line leaves it. */
.curve__end { stroke: var(--surface); stroke-width: 1.5; }
.curve__end--up { fill: var(--primary); }
.curve__end--down { fill: var(--danger); }

/* ---------------------------------------------------------------------------
   Big blinds won by position — columns either side of a zero line.

   Length carries magnitude and hue carries only the sign, so both halves of
   every column are always in the DOM at the same height: the zero line then
   sits at one y across the card and the eye can run along it. Bars grow *away*
   from the line, which is why the upper half aligns to its end.
   --------------------------------------------------------------------------- */
/* Takes every bit of width the heatmap beside it doesn't want. It used to be
   capped at 30rem, which on a wide window left a strip of empty page to its
   right and read as a third card that had failed to load. Nothing here needs
   the cap: the columns are a `1fr` grid, and `.poscol__bar` is capped at
   2.2rem, so extra width spreads the bars apart rather than fattening them. */
.position-card {
  flex: 1 1 22rem;
  align-self: stretch;
  display: flex;
  flex-direction: column;
}

.position-card .panel__title { padding: .7rem .6rem; }

.poscols {
  flex: 1;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: .4rem;
  min-height: 13rem;
  padding: .9rem .7rem .7rem;
}

.poscol {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.poscol__value {
  text-align: center;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  margin-bottom: .3rem;
}

.poscol__half { flex: 1; display: flex; justify-content: center; }

/* The zero line lives on the upper half's edge, so it cannot drift from the
   place the bars actually grow from. */
.poscol__half--pos { align-items: flex-end; border-bottom: 1px solid var(--hairline); }
.poscol__half--neg { align-items: flex-start; }

.poscol__bar { width: 58%; max-width: 2.2rem; border-radius: 2px; }
.poscol__bar--win { background: var(--primary); }
.poscol__bar--lose { background: var(--danger); }

.poscol__seat {
  margin-top: .45rem;
  text-align: center;
  color: var(--fg);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .02em;
}

/* How many hands the column rests on. Quiet: it qualifies the bar rather than
   competing with it — +49 over 23 hands is not the claim +192 over 42 is. */
.poscol__hands {
  text-align: center;
  color: var(--muted);
  font-size: .7rem;
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Where the tournaments ended. A distribution over the places in the field, so
   rows in field order and never sorted by size — the axis is the field, and a
   reader looks down it for the shape rather than for the biggest bar.

   Takes whatever the row has left, like the position card it sits beside, and
   wraps to its own line rather than squeezing: nine rows of `1st..9th` stay
   readable at any width, but the note under them does not.
   --------------------------------------------------------------------------- */
.places-card {
  flex: 1 1 18rem;
  min-width: 0;
}

.places-card .panel__title { padding: .7rem 1rem; }

.placerows {
  display: flex;
  flex-direction: column;
  gap: .45rem;
  padding: .9rem 1rem 1rem;
}

/* One grid shape for every row, so the counts line up down the card. */
.placerow {
  display: grid;
  grid-template-columns: 2.2rem 1fr 4.6rem;
  align-items: center;
  gap: .5rem;
}

/* A place never finished in is faded to almost nothing: the row is there to
   hold the axis open, not to say anything. Kept rather than dropped, so the
   places stay at fixed positions down the card. */
.placerow.is-empty { opacity: .38; }

.placerow__place {
  font-size: .78rem;
  font-weight: 600;
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

/* The full 0-100% of tournaments played. Not scaled to the tallest bar: every
   player would then have one full-length row and they would all look alike. */
.placerow__track {
  display: flex;
  height: .85rem;
  border-radius: 3px;
  background: var(--surface-2);
  overflow: hidden;
}

/* Two segments of one hue: the games played out, then the games left. `flex:
   none` because each carries its own width as a share of the *track* — let
   them flex and a row with both would have its two shares renegotiated
   against each other, which is a different quantity entirely. */
.placerow__bar {
  flex: none;
  height: 100%;
  /* A floor, so a single finish out of many games is a sliver rather than
     nothing. Safe because a place never finished in gets no bar at all. */
  min-width: 3px;
  background: var(--primary);
}

/* Left rather than played out. Paler, not a second hue: it is the same
   result, reached a different way — a hue would make it a different series. */
.placerow__bar--left {
  background: color-mix(in srgb, var(--primary) 34%, var(--surface-2));
}

/* Rounded on the outside of the pair only, so two segments read as one bar. */
.placerow__bar:first-child { border-radius: 3px 0 0 3px; }
.placerow__bar:last-child { border-radius: 0 3px 3px 0; }
.placerow__bar:only-child { border-radius: 3px; }

/* The count, and the total it is out of — a share drawn from eleven games is
   not the claim the same share drawn from two hundred would be. */
.placerow__games {
  text-align: right;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}

.placerow__of { color: var(--muted); }

/* ---------------------------------------------------------------------------
   How each position is played. Rows rather than columns, because the label is
   a word and the bar is a rate: a horizontal track is read left to right the
   way a percentage is, and it leaves room for both figures beside it.
   --------------------------------------------------------------------------- */
/* Half the row. `.analyze-grid`'s gap is 1.4rem, so half of it comes off each
   card's basis — a bare 50% would overflow the row by exactly one gap and wrap
   the card onto its own line at half width, which looks the same until the
   window is wide enough for something to sit beside it.

   The rows inside run the card's full width. They carried a `max-width` at
   first, on the theory that a short bar smeared across the page is harder to
   compare against the row under it; the cap left the panel visibly unfilled,
   and the comparison these rows are for is vertical anyway — every bar starts
   at the same left edge on the same 0-100% track, so a longer track separates
   them more, not less. With the card itself at half width there is nothing
   left for a cap to do. */
.play-card {
  flex: 0 1 calc(50% - .7rem);
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.play-card .panel__title { padding: .8rem 1rem; }

.playrows {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: .5rem;
  padding: 1rem 1rem 1.1rem;
}

/* One grid shape for the header and every row, so the figures line up down the
   card without either knowing the other's widths. */
.playrow {
  display: grid;
  grid-template-columns: 2.6rem 1fr 3rem 3rem 3rem;
  align-items: center;
  gap: .5rem;
}

.playrow--head {
  font-size: .68rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* A seat played a handful of times is kept and faded, never dropped — the same
   rule the heatmap applies to a sparse cell. */
.playrow.is-thin { opacity: .5; }

.playrow__seat {
  font-size: .78rem;
  font-weight: 600;
  color: var(--fg);
  letter-spacing: .02em;
}

/* The track is the full 0-100% of hands dealt in that seat. Not scaled to the
   longest bar: a length has to mean the same thing in every row and in every
   session. */
.playrow__track {
  position: relative;
  height: .85rem;
  border-radius: 3px;
  background: var(--surface-2);
  overflow: hidden;
}

/* Both bars start at the track's left edge and are measured against the track,
   so PFR is drawn over VPIP rather than inside it — see the template. PFR is a
   subset of VPIP by construction (`count_hand()` counts a raise as voluntary),
   so the pale remainder is exactly the hands entered without a raise. */
.playrow__vpip,
.playrow__pfr {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  border-radius: 3px 0 0 3px;
}

.playrow__vpip { background: color-mix(in srgb, var(--primary) 34%, var(--surface-2)); }
.playrow__pfr { background: var(--primary); }

.playrow__fig {
  text-align: right;
  font-size: .74rem;
  font-variant-numeric: tabular-nums;
  color: var(--fg);
}

.playrow__fig--pfr { color: var(--primary-ink); font-weight: 600; }

.playrow__hands {
  text-align: right;
  font-size: .7rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* ---------------------------------------------------------------------------
   Ranked — the table list and one table's lobby. Both lean on the shell's own
   vocabulary (.panel, .data-table, .meta) rather than inventing a second one;
   what is here is only what those do not cover.
   --------------------------------------------------------------------------- */
.rank-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 1.2rem;
}

.rank-bar__title {
  margin: 0;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--fg);
}

.rank-bar__create { display: flex; align-items: flex-end; gap: .7rem; }
.rank-bar__create label { display: grid; gap: .3rem; font-size: .76rem; }

.rank-bar__create select {
  padding: .5rem .65rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--fg);
  font: inherit;
}

/* An unfilled seat, drawn rather than implied — waiting for two more is a
   different feeling from waiting for one. */
.data-table tr.is-open td { opacity: .55; }

/* One table, one card, full width. Three parts on a row: who opened it, how
   full it is, and the way in — so the eye lands on the same three places down
   the list without a grid's rules to follow. */
.table-list { display: grid; gap: .8rem; }

.table-card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.2rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius);
  background: var(--sheen), var(--surface);
  box-shadow: var(--lift-1);
}

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

.table-card__who { flex: 1 1 14rem; display: grid; gap: .2rem; min-width: 0; }

.table-card__host {
  color: var(--fg);
  font-size: 1rem;
  font-weight: 600;
}

.table-card__meta { color: var(--muted); font-size: .76rem; }

.table-card__seats {
  color: var(--muted);
  font-size: .82rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.table-card__seats b { color: var(--fg); font-weight: 600; }

.table-card__go { margin-left: auto; }

.rank-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .8rem;
  margin: 1.2rem 0 0;
}

.rank-actions__wait { font-size: .88rem; color: var(--muted); }
.rank-actions__wait b { color: var(--fg); font-weight: 600; }

.ghost-button {
  display: inline-block;
  padding: .38rem .85rem;
  border: 1px solid var(--edge);
  border-radius: var(--pill);
  background: var(--surface-2);
  color: var(--fg);
  font: inherit;
  font-size: .78rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.ghost-button:hover { border-color: var(--primary); color: var(--primary-ink); }
.ghost-button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.btn-primary[disabled] { opacity: .45; cursor: not-allowed; filter: none; }

/* ---------------------------------------------------------------------------
   Ranked table creation — structures stacked full width, seats as pills.
   --------------------------------------------------------------------------- */
.structure-rows { display: grid; gap: .7rem; }

/* The wide variant of a structure card: name and cadence on the left, the
   figures laid out along the right, so a whole row is one game rather than a
   column in a comparison. */
.structure--wide {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .95rem 1.15rem;
}

.structure--wide .structure__head { display: grid; gap: .15rem; }
.structure--wide .structure__name { font-size: 1.05rem; }

.structure--wide .structure__facts {
  display: flex;
  gap: 1.6rem;
  margin: 0;
  font-size: .78rem;
}

.structure--wide .structure__facts dt {
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.structure--wide .structure__facts dd { margin: .1rem 0 0; text-align: left; color: var(--fg); }
.structure--wide .structure__facts > div { display: grid; }

/* Five one-word choices. A dropdown hides four of them behind a click and the
   chosen one behind a glyph; pills show the whole set and which is picked. */
.seat-pills { display: flex; flex-wrap: wrap; gap: .4rem; }

.seat-pill { position: relative; cursor: pointer; }
.seat-pill input { position: absolute; opacity: 0; pointer-events: none; }

.seat-pill span {
  display: block;
  min-width: 2.9rem;
  padding: .5rem .2rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--sheen), var(--surface-2);
  color: var(--fg);
  font-size: .92rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.seat-pill:hover span { border-color: var(--muted); }

.seat-pill input:checked + span {
  border-color: var(--primary);
  background: var(--primary);
  color: var(--on-primary);
}

.seat-pill input:focus-visible + span { outline: 2px solid var(--primary); outline-offset: 2px; }

/* --- who is at the table ---------------------------------------------------
   A face, a name and what they are. Laid out as a grid rather than a list
   because a table is a set of seats, not an order — and an empty seat has to
   look like a seat, or "3 of 6" is the only thing telling you what is missing.
   --------------------------------------------------------------------------- */
.seat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: .8rem;
  padding: 1rem 1.15rem;
}

.seat-card {
  display: grid;
  justify-items: center;
  gap: .35rem;
  padding: 1rem .8rem;
  border: 1px solid var(--edge);
  border-radius: 10px;
  background: var(--surface-2);
  text-align: center;
}

.seat-card__face {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface);
}

/* An empty seat gets the same circle, drawn rather than filled — the shape is
   what says "someone goes here". */
.seat-card__face--empty {
  display: block;
  border: 1px dashed var(--edge);
  background: transparent;
}

.seat-card__name {
  color: var(--fg);
  font-size: .88rem;
  font-weight: 600;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.seat-card__role {
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .09em;
  text-transform: uppercase;
}

.seat-card.is-you { border-color: var(--primary); background: var(--accent-wash); }
.seat-card.is-you .seat-card__name { color: var(--primary-ink); }
.seat-card.is-open { background: var(--surface); }
.seat-card.is-open .seat-card__name { color: var(--muted); font-weight: 500; }

/* A face beside a name on the ladder. Same placeholder as everywhere else —
   one avatar column on `users` and this reads from it. */
.ladder-who { display: inline-flex; align-items: center; gap: .55rem; }

.ladder-face {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--surface-2);
}

/* --- an empty list, with something to do about it --------------------------
   A dashed ground rather than a solid card: this is a space waiting to be
   filled, and the border says so without a word.
   --------------------------------------------------------------------------- */
.empty-state {
  display: grid;
  justify-items: center;
  gap: .7rem;
  padding: 3.2rem 1.5rem;
  border: 1px dashed var(--edge);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 50%, transparent);
  text-align: center;
}

.empty-state__icon { color: var(--muted); opacity: .7; }
.empty-state__icon svg { width: 44px; height: 44px; }

.empty-state__title {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--fg);
}

.empty-state__text {
  margin: 0 0 .6rem;
  max-width: 34rem;
  color: var(--muted);
  font-size: .86rem;
  line-height: 1.55;
}

/* --- "you are in a game" ----------------------------------------------------
   In the topbar, so it is on every page. A live dot rather than a word like
   LIVE: it reads at a glance and does not compete with the section heading.
   --------------------------------------------------------------------------- */
.playing {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  padding: .38rem .8rem;
  border: 1px solid color-mix(in srgb, var(--primary) 50%, transparent);
  border-radius: var(--pill);
  background: var(--accent-wash);
  box-shadow: 0 8px 20px -14px color-mix(in srgb, var(--primary) 90%, transparent);
  color: var(--primary-ink);
  font-size: .76rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.playing:hover { filter: brightness(1.1); }

.playing__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--primary);
  animation: playing-pulse 2s ease-in-out infinite;
}

@keyframes playing-pulse { 50% { opacity: .3; } }

/* The pulse is the decoration; the dot is the message, so it stays. */
@media (prefers-reduced-motion: reduce) {
  .playing__dot { animation: none; }
}

/* --- settings ---------------------------------------------------------------
   Two choices, so two groups and nothing else.
   --------------------------------------------------------------------------- */
.pfp-row { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; }

.pfp-preview {
  width: 88px;
  height: 88px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--edge);
  background: var(--surface-2);
}

.config__field input[type="file"] {
  padding: .5rem 0;
  color: var(--muted);
  font: inherit;
  font-size: .82rem;
}

/* The choice is a colour, so it is the colour you click. */
.felt-picker { display: flex; flex-wrap: wrap; gap: .8rem; }

.felt { position: relative; display: grid; justify-items: center; gap: .4rem; cursor: pointer; }
.felt input { position: absolute; opacity: 0; pointer-events: none; }

.felt__swatch {
  display: block;
  width: 68px;
  height: 46px;
  border-radius: 8px;
  border: 2px solid var(--edge);
  /* The same radial the table draws, so a swatch is a small picture of the
     thing rather than a flat approximation of it. */
  background: radial-gradient(ellipse 70% 90% at 50% 45%,
              var(--felt-mid) 0%, var(--felt-deep) 75%, var(--felt-deep) 100%);
}

.felt input:checked + .felt__swatch { border-color: var(--primary); }
.felt input:focus-visible + .felt__swatch { outline: 2px solid var(--primary); outline-offset: 2px; }

.felt__name {
  color: var(--muted);
  font-size: .74rem;
  font-weight: 600;
  text-transform: capitalize;
}

.felt input:checked ~ .felt__name { color: var(--fg); }

/* The four tables. Defined once here and again in style.css for the stage —
   see the note there for why they are not shared. */
.felt__swatch--black { --felt-mid: #10151f; --felt-deep: #0a0e15; }
.felt__swatch--green { --felt-mid: #13402c; --felt-deep: #07200f; }
.felt__swatch--blue  { --felt-mid: #12314f; --felt-deep: #071626; }
.felt__swatch--red   { --felt-mid: #4a1620; --felt-deep: #250910; }
.felt__swatch--purple   { --felt-mid: #3a1c4a; --felt-deep: #261231; }
.felt__swatch--wine     { --felt-mid: #3f1220; --felt-deep: #290c15; }
.felt__swatch--graphite { --felt-mid: #1b1f24; --felt-deep: #121418; }

/* --- picking cards ----------------------------------------------------------
   The choice is a picture of a card, so it is a card you click. Faces and
   backs use the same control because they are the same kind of decision made
   twice.
   --------------------------------------------------------------------------- */
.deck-picker { display: flex; flex-wrap: wrap; gap: 1rem; }

/* Faces and backs are two choices, not one long list of cards. Stacked
   directly inside the fieldset they had nothing between them, so with the
   captions gone the two rows ran together into a single wall of cards. The
   rule is scoped to fields that are direct children of a group — the training
   form puts its fields in a grid, which spaces them itself. */
.config__group > .config__field + .config__field { margin-top: 1.9rem; }

/* And a little more air under the label, so the word belongs to the row of
   cards beneath it rather than floating between the two. */
.config__field > .deck-picker { margin-top: .35rem; }

.deck { position: relative; display: grid; justify-items: center; gap: .4rem; cursor: pointer; }
.deck input { position: absolute; opacity: 0; pointer-events: none; }

.deck__sample {
  display: block;
  width: 58px;
  aspect-ratio: 238.11 / 332.6;      /* the real card ratio, as the table uses */
  object-fit: cover;
  border-radius: 5px;
  border: 2px solid var(--edge);
  background: #fff;
}

/* The border is the only thing saying which one is chosen now that nothing is
   captioned, so it is doubled up with a ring rather than a colour change
   alone. */
.deck input:checked + .deck__sample {
  border-color: var(--primary);
  box-shadow: 0 0 0 2px var(--accent-wash), 0 0 0 3px var(--primary);
}
.deck input:focus-visible + .deck__sample { outline: 2px solid var(--primary); outline-offset: 2px; }



/* ---------------------------------------------------------------------------
   The push/fold drill.

   Two cards, three facts and two buttons: everything a shove-or-fold decision
   rests on, and nothing else. There is deliberately no picture of a table —
   nine drawn seats around three numbers would be decoration, and the seat's
   name is the only part of the geometry the decision uses.
   --------------------------------------------------------------------------- */
.drill-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.4rem;
  margin: 0 0 1.4rem;
}

.drill-title {
  margin: 0 0 .3rem;
  font-size: 1.35rem;
  letter-spacing: -.01em;
}

.drill-sub {
  margin: 0;
  max-width: 46rem;
  color: var(--muted);
  font-size: .84rem;
  line-height: 1.55;
}

.drill-sub code, .empty-note code {
  padding: .05rem .3rem;
  border-radius: 3px;
  background: var(--surface-2);
  font-size: .95em;
}

/* The session, and it says so by being resettable. Figures are tabular here
   because they change under the eye between spots. */
.drill-tally {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding: .6rem .9rem;
  border: 1px solid var(--edge);
  border-radius: 6px;
  background: var(--surface);
}

.drill-tally__item { text-align: center; }

.drill-tally__item span {
  display: block;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
}

.drill-tally__item small {
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.drill {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.4rem;
}

.spot-card {
  flex: 1 1 20rem;
  padding: 1.4rem 1.4rem 1.2rem;
}

.spot__prompt {
  color: var(--fg);
  font-size: 1.02rem;
  min-height: 1.5rem;
}

/* Big, because they are what is being read. The pair is the question. */
.spot__cards {
  display: flex;
  gap: .6rem;
  margin: 1rem 0 1.2rem;
  min-height: 8.4rem;
}

.drillcard {
  width: 6rem;
  border-radius: 6px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, .28);
}

.spot__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 1.6rem;
  margin: 0 0 1.3rem;
}

.spot__facts dt {
  color: var(--muted);
  font-size: .68rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.spot__facts dd {
  margin: .15rem 0 0;
  font-size: 1.05rem;
  font-variant-numeric: tabular-nums;
}

.spot__actions { display: flex; gap: .7rem; }

/* The two actions are the two colours the table already uses for them, so a
   drill and a hand read the same way round. */
.drill-btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .7rem 1.3rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-sm);
  background: var(--sheen), var(--surface-2);
  color: var(--fg);
  font-family: inherit;
  font-size: .95rem;
  cursor: pointer;
}

.drill-btn:disabled { opacity: .4; cursor: default; }
.drill-btn:not(:disabled):hover { background: var(--hover-wash); }

.drill-btn--yes:not(:disabled) {
  border-color: color-mix(in srgb, var(--primary) 55%, var(--edge));
  background: color-mix(in srgb, var(--primary) 16%, var(--surface-2));
}

.drill-btn--next {
  border-color: color-mix(in srgb, var(--primary) 55%, var(--edge));
}

.drill-btn kbd {
  padding: .05rem .35rem;
  border: 1px solid var(--edge);
  border-radius: 3px;
  background: var(--bg);
  color: var(--muted);
  font-family: inherit;
  font-size: .72rem;
}

/* --- the mark, and the range behind it ------------------------------------ */
.verdict-card {
  flex: 1 1 22rem;
  padding: 1.2rem 1.4rem 1.3rem;
}

.verdict__head {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  flex-wrap: wrap;
}

.verdict__grade {
  padding: .18rem .6rem;
  border-radius: var(--pill);
  font-size: .72rem;
  letter-spacing: .05em;
  text-transform: uppercase;
}

/* The grade is a word first and a colour second — the word is what a
   colourblind reader gets and the fill only agrees with it. The polarity is
   the shell's own, not the table's: in here teal is the good direction (see
   `.net--up`) and `--rate-good` is a table token that means something else. */
.verdict__grade.is-correct {
  background: color-mix(in srgb, var(--primary) 22%, var(--surface-2));
  color: var(--primary-ink);
}
.verdict__grade.is-mediocre {
  background: color-mix(in srgb, var(--warn) 26%, var(--surface-2));
}
.verdict__grade.is-error,
.verdict__grade.is-blunder {
  background: color-mix(in srgb, var(--danger) 26%, var(--surface-2));
}

.verdict__line { color: var(--fg); font-size: .92rem; }

.evrow {
  display: flex;
  justify-content: space-between;
  padding: .4rem 0;
  border-bottom: 1px solid var(--hairline);
  font-size: .86rem;
}

.evrow__value { font-variant-numeric: tabular-nums; }
.evrow.is-best .evrow__label,
.evrow.is-best .evrow__value { color: var(--primary-ink); font-weight: 600; }

.verdict__evs { margin: .9rem 0 1.1rem; }

.verdict__note {
  margin: 1rem 0 0;
  color: var(--muted);
  font-size: .8rem;
  line-height: 1.5;
}

/* The 13x13, on the Analyze heatmap's axis so a reader of one can read the
   other. In or out is a fill, and a hand the solution *mixes* is part-filled
   from the bottom rather than given a third colour — the fill is the
   frequency, and a mixed hand is genuinely between the two. */
.rangegrid {
  display: grid;
  grid-template-columns: repeat(13, 1fr);
  gap: 1px;
  margin-top: .2rem;
}

.rcell {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: 2px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: .52rem;
  font-variant-numeric: tabular-nums;
}

.rcell.is-in { background: var(--primary); color: var(--on-primary); }

.rcell.is-mixed::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--fill, 50%);
  background: var(--primary);
}

.rcell__label { position: relative; }

/* Your hand, on top of whatever the cell already says. An outline rather than
   a colour, so it can mark a cell that is in the range and one that is not
   without either reading as a third state. */
.rcell.is-you {
  outline: 2px solid var(--fg);
  outline-offset: -2px;
  color: var(--fg);
  font-weight: 700;
}

.drill-assumptions {
  margin: 1.6rem 0 0;
  color: var(--muted);
  font-size: .82rem;
}

.drill-assumptions summary { cursor: pointer; }
.drill-assumptions ul { margin: .7rem 0 0; padding-left: 1.1rem; line-height: 1.6; }
.drill-assumptions strong { color: var(--fg); }
