/* ============================================================
   MyFitClub — Precision Instrument
   A timing-tower register: crisp neutral surfaces, tabular data,
   one race-orange accent. The collectible cards are the only
   place the design is allowed to go maximalist.
   ============================================================ */

:root {
  color-scheme: light;

  --ground: #f5f6f8;
  --card: #ffffff;
  --sunk: #eef0f4;
  --hair: #e7eaf0;
  --hair-soft: #f0f2f6;

  --ink: #12151a;
  --ink-2: #6b7482;
  --ink-3: #8a93a3;
  --ink-4: #b6bdc9;

  --accent: #e8420c;
  --accent-deep: #c33305;
  --accent-tint: #fdece3;
  --accent-lift: #f28f5d;

  /* the top of the streak ladder, which is also the legendary card's gold */
  --gold: #e8b45a;
  --gold-lift: #f6d9a0;

  --good: #0e7c5a;
  --good-tint: #e3f3ed;
  --warn: #a8620b;
  --warn-tint: #fbeeda;

  /* single-hue sequential ramp for the heatmap */
  --h0: #eceef3;
  --h1: #fbddce;
  --h2: #f8b999;
  --h3: #f28f5d;
  --h4: #e85f22;
  --h5: #c33305;

  --shell: 430px;
  --tab-h: 66px;
  --radius: 10px;
  --lift: 0 1px 2px rgba(18, 21, 26, 0.05), 0 4px 14px rgba(18, 21, 26, 0.05);
  --lift-2: 0 2px 6px rgba(18, 21, 26, 0.08), 0 18px 40px rgba(18, 21, 26, 0.12);
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: #dfe3ea;
  color: var(--ink);
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}

button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
input, select, textarea { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.disp {
  font-family: 'Saira Condensed', 'Arial Narrow', system-ui, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.005em;
  line-height: 1;
}
.mono {
  font-family: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
.num { font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-2); }
.faint { color: var(--ink-3); }

/* ---------- shell ---------- */
.shell {
  max-width: var(--shell);
  margin: 0 auto;
  min-height: 100dvh;
  background: var(--ground);
  position: relative;
  display: flex;
  flex-direction: column;
  box-shadow: 0 0 0 1px var(--hair), 0 24px 60px rgba(18, 21, 26, 0.12);
}
.screen {
  flex: 1;
  /* 26px, not tab-h + 26px. The tab bar is `position: sticky`, which means it
     is IN FLOW and already occupies its own height at the end of the shell —
     reserving it here as well counted it twice and left every scrolling screen
     with 66px of nothing under its last panel, enough to make a page that fits
     scroll anyway. Screens with no tab bar at all (the recap, the rescue, the
     profile) keep the 26px, which is the part that was ever breathing room. */
  padding: 0 18px 26px;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.screen--flush { padding-left: 0; padding-right: 0; }

/* ---------- Today as a fixed frame ----------
   The club's state — the date, the streak, who is in — stays put, and only the
   plan moves under it. On a phone that context was the first thing to scroll
   away, which is backwards: it is what the list is being worked against.
   Everywhere else in the app the page still scrolls as one piece.          */
.shell--frame { height: 100dvh; }
.shell--frame .screen {
  min-height: 0;
  /* Enough that the completed bar is not welded to the tab bar. The frame does
     not scroll, so this is the whole bottom margin the screen gets. */
  padding-bottom: 10px;
  /* auto, not hidden. A tall header — a rescue call and an approval queue and
     three away notes at once — would otherwise be clipped with no way to
     reach it. In the ordinary case nothing here scrolls at all. */
  overflow-y: auto;
}
/* Only the plan takes the leftover height; everything above keeps its own. */
.shell--frame .section--grow { flex: 1; min-height: 0; }
.shell--frame .scroller {
  flex: 1;
  /* Without this a flex child refuses to shrink below its content and the
     whole frame grows instead of the list scrolling inside it. */
  min-height: 7rem;
  overflow-y: auto;
  /* The page behind must not start moving when the list hits its end. */
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* No trailing space under the last tile: the progress bar sits below this,
     outside the scroll, and the two together were leaving a hole. */
  padding-bottom: 0;
  /* The bar itself is noise on a list this short-lived. Firefox and the
     WebKit/Blink pseudo-element disagree about how to say so, so both. */
  scrollbar-width: none;
  /* Both edges fade, and only when there is something past them to fade into —
     a hard cut reads as a broken tile rather than as a list with more in it.
     At 0 the two stops coincide, so the edge is simply opaque. */
  --fade-top: 0px;
  --fade-bottom: 0px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-top),
    #000 calc(100% - var(--fade-bottom)), transparent 100%);
}
.shell--frame .scroller::-webkit-scrollbar { width: 0; height: 0; }
.shell--frame .scroller--cut { --fade-top: 16px; }
.shell--frame .scroller--more { --fade-bottom: 16px; }
.pad { padding: 0 18px; }

/* ---------- masthead: the start line ---------- */
.mast { padding-top: 20px; display: flex; flex-direction: column; gap: 8px; }
.mast .startline { margin-top: 2px; }
.startline { display: flex; align-items: center; gap: 8px; }
.startline::before {
  content: '';
  width: 26px;
  height: 3px;
  background: var(--accent);
  flex: none;
}
.startline::after { content: ''; height: 1px; background: var(--hair); flex: 1; }
.startline > .mono { color: var(--ink-3); flex: none; }

/* Fluid rather than fixed: at 34px "WED 2 SEP" is ~170px of condensed caps and
   a 360px phone has ~116px to give it once the streak and profile badges have
   taken their side. It wrapped to two lines, which since the header became
   fixed costs height that is on screen permanently. */
.mast h1 { margin: 0; font-size: clamp(28px, 9vw, 34px); }
.mast .sub { color: var(--ink-2); font-size: 13px; margin: 0; }

.headrow { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }

/* ---------- sections ---------- */
.section { display: flex; flex-direction: column; gap: 10px; }
.section > header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.section > header .mono { color: var(--ink-3); }

/* ---------- panels ---------- */
.panel {
  background: var(--card);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  box-shadow: var(--lift);
}
.panel--pad { padding: 14px 15px; }
.panel--sunk { background: var(--sunk); box-shadow: none; }

/* stadium display: the ink chip that carries a headline number */
.display {
  background: var(--ink);
  color: #fff;
  border-radius: var(--radius);
  padding: 14px 16px;
  display: flex;
  align-items: center;
  gap: 16px;
}
.display .mono { color: rgba(255, 255, 255, 0.55); }
.display--alert { background: var(--ink); box-shadow: inset 0 0 0 2px var(--accent); }
.display__big { font-size: 44px; line-height: 0.86; }

.display__big small { font-size: 17px; color: rgba(255, 255, 255, 0.5); }

/* ---------- chips ---------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--hair);
  background: var(--card);
  border-radius: 999px;
  padding: 4px 9px;
  color: var(--ink-2);
  white-space: nowrap;
}
.chip--ink { background: var(--ink); border-color: var(--ink); color: #fff; }
.chip--accent { background: var(--accent-tint); border-color: #f6cfbb; color: var(--accent-deep); }
.chip--good { background: var(--good-tint); border-color: #c5e4d8; color: var(--good); }
.chip--warn { background: var(--warn-tint); border-color: #efd9ae; color: var(--warn); }
.chip--ghost { background: transparent; }
/* The half of a chip a narrow screen can do without. The streak keeps its
   flame and its number, which is the whole of the information; the words go,
   and the chip's title attribute still carries them for anyone who needs it. */
@media (max-width: 380px) {
  .chip__wide { display: none; }
}

/* ---------- the streak ladder ----------
   Six rungs at 1, 7, 14, 30, 90 and 365 days, climbing out of the neutral
   register and into the single-hue ramp, then past it into gold. Each rung is
   nothing but a set of custom properties, worn by the masthead chip, the club
   banner and the profile tile alike — so a tier cannot come to mean one colour
   in one place and another somewhere else. Named in src/ui/tier.js.          */
.tier--ember {
  --tier-bg: var(--h1);
  --tier-ink: #8f5210;
  --tier-line: #f4cdb6;
  --tier-soft: rgba(143, 82, 16, 0.66);
  --tier-track: rgba(143, 82, 16, 0.16);
  --tier-fill: var(--accent);
  --tier-glow: none;
}
.tier--kindled {
  --tier-bg: var(--h2);
  --tier-ink: #8a3d06;
  --tier-line: #f0a37c;
  --tier-soft: rgba(138, 61, 6, 0.7);
  --tier-track: rgba(138, 61, 6, 0.18);
  --tier-fill: var(--accent-deep);
  --tier-glow: none;
}
/* The last rung that carries dark ink. White over a mid orange is barely 3:1 and
   these labels are 10px — the ink flips at Blazing instead, which is no loss:
   the flip is itself the moment the ladder turns hot. */
.tier--burning {
  --tier-bg: var(--h3);
  --tier-ink: #6b2404;
  --tier-line: #e8752f;
  --tier-soft: rgba(107, 36, 4, 0.74);
  --tier-track: rgba(107, 36, 4, 0.2);
  --tier-fill: var(--accent-deep);
  --tier-glow: 0 2px 12px rgba(232, 95, 34, 0.26);
}
.tier--blazing {
  --tier-bg: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
  --tier-ink: #fff;
  --tier-line: transparent;
  --tier-soft: rgba(255, 255, 255, 0.76);
  --tier-track: rgba(255, 255, 255, 0.26);
  --tier-fill: #fff;
  --tier-glow: 0 2px 16px rgba(232, 66, 12, 0.38);
}
.tier--wildfire {
  /* A molten band through the middle, dark at both ends. Gold anywhere near a
     corner puts white 10px labels on a light ground — the badge sits top right
     and the milestone count bottom right, and both were unreadable over it. */
  --tier-bg: linear-gradient(118deg, #341606 0%, var(--accent-deep) 22%, var(--accent) 42%, var(--gold) 58%, var(--accent) 74%, #7d2404 100%);
  --tier-ink: #fff;
  --tier-line: transparent;
  --tier-soft: rgba(255, 255, 255, 0.78);
  --tier-track: rgba(255, 255, 255, 0.24);
  --tier-fill: var(--gold-lift);
  --tier-glow: 0 2px 20px rgba(232, 66, 12, 0.44);
}
.tier--eternal {
  --tier-bg: linear-gradient(135deg, #0b0d12 0%, #1d1408 56%, #2f2109 100%);
  --tier-ink: var(--gold-lift);
  --tier-line: rgba(232, 180, 90, 0.55);
  --tier-soft: rgba(246, 217, 160, 0.72);
  --tier-track: rgba(246, 217, 160, 0.2);
  --tier-fill: var(--gold-lift);
  --tier-glow: 0 2px 22px rgba(232, 180, 90, 0.34);
}

/* A year of days earns the collectible cards' foil, on a slow sweep of its own
   rather than the pointer tracking the cards use — this one is never held. */
.tier--eternal { position: relative; overflow: hidden; }
.tier--eternal::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: color-dodge;
  background: linear-gradient(
    115deg,
    transparent 34%,
    rgba(255, 226, 170, 0.4) 46%,
    rgba(255, 247, 224, 0.52) 50%,
    rgba(255, 226, 170, 0.4) 54%,
    transparent 66%
  );
  background-size: 300% 100%;
  animation: foilsweep 5.5s linear infinite;
}
@keyframes foilsweep { from { background-position: 150% 0; } to { background-position: -50% 0; } }

.chip--tier {
  background: var(--tier-bg);
  border-color: var(--tier-line);
  color: var(--tier-ink);
  box-shadow: var(--tier-glow);
}
/* Doubled up on purpose. .tile and .tile .mono are declared further down the
   file, so at equal specificity they would win and leave a tiered tile with the
   card's white background — and Blazing's white ink on it, invisible. */
.tile.tile--tier {
  background: var(--tier-bg);
  border-color: var(--tier-line);
  color: var(--tier-ink);
  box-shadow: var(--tier-glow);
}
.tile.tile--tier .mono { color: var(--tier-soft); }

/* ---------- club streak banner ----------
   The screen's headline. It carries the streak, what today did to it, and a
   rail measuring the distance to the next rung, so the number always has
   somewhere to be going.                                                     */
.banner {
  border-radius: var(--radius);
  padding: 13px 15px 14px;
  display: flex;
  flex-direction: column;
  gap: 11px;
  background: var(--tier-bg, var(--card));
  color: var(--tier-ink, var(--ink));
  border: 1px solid var(--tier-line, var(--hair));
  box-shadow: var(--tier-glow, var(--lift));
}
.banner--alert {
  --tier-bg: var(--ink);
  --tier-ink: #fff;
  --tier-line: transparent;
  --tier-soft: rgba(255, 255, 255, 0.78);
  --tier-track: rgba(255, 255, 255, 0.16);
  --tier-fill: var(--accent);
  --tier-glow: inset 0 0 0 2px var(--accent), var(--lift);
}
.banner__top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.banner__top > .mono { color: var(--tier-soft); }
.banner__badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid currentColor;
  border-radius: 999px;
  padding: 2px 8px;
  opacity: 0.95;
}
.banner__rise {
  width: 0;
  height: 0;
  border: 3.5px solid transparent;
  border-top: 0;
  border-bottom-color: currentColor;
}
.banner__mid { display: flex; align-items: baseline; gap: 14px; }
.banner__n { font-size: 52px; display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.banner__unit { color: var(--tier-soft); font-size: 10px; }
.banner__state { flex: 1; min-width: 0; text-align: right; font-size: 13px; font-weight: 600; line-height: 1.3; }
.banner__sub { color: var(--tier-soft); font-weight: 500; margin-top: 2px; }
/* The open state carries its "who" in the unit line rather than a second row,
   so it needs no layout of its own beyond a slightly smaller numeral. */
.banner--alert .banner__n { font-size: 44px; flex: 1; }

.banner__railrow { display: flex; flex-direction: column; gap: 7px; }
.banner__rail { height: 5px; border-radius: 3px; background: var(--tier-track); overflow: hidden; }
.banner__rail i { display: block; height: 100%; background: var(--tier-fill); border-radius: 3px; }
.banner__foot { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.banner__foot .mono { color: var(--tier-soft); }
.banner--alert .banner__railrow { flex-direction: row; align-items: center; gap: 12px; }
.banner--alert .banner__rail { flex: 1; }
.banner--alert .banner__foot { flex: none; }

/* ---------- buttons ---------- */
.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 48px;
  padding: 0 18px;
  border-radius: var(--radius);
  border: 1px solid var(--hair);
  background: var(--card);
  font-weight: 600;
  font-size: 14px;
  transition: transform 0.08s ease, background 0.15s ease, border-color 0.15s ease;
}
.btn:active { transform: translateY(1px); }
.btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 2px 10px rgba(232, 66, 12, 0.28); }
.btn--primary:disabled { background: var(--ink-4); border-color: var(--ink-4); box-shadow: none; }
.btn--ink { background: var(--ink); border-color: var(--ink); color: #fff; }
.btn--ghost { background: transparent; }
.btn--sm { min-height: 36px; padding: 0 12px; font-size: 13px; }
.btn:disabled { cursor: default; opacity: 0.9; }

/* ---------- tap stepper ----------
   What you actually did, without a keyboard and without a sheet — it sits in
   the exercise tile, so the number is edited where it is read. 34px square to
   the millimetre of the tick beside it, because a row of controls that do not
   line up reads as a form rather than a scoreboard.                          */
.stepper {
  display: inline-flex;
  align-items: stretch;
  height: 28px;
  flex: none;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: var(--card);
  overflow: hidden;
}
.stepper__b {
  width: 28px;
  flex: none;
  display: grid;
  place-items: center;
  color: var(--ink-2);
  background: var(--sunk);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.12s ease, color 0.12s ease;
}
.stepper__b:active { background: var(--accent); color: #fff; }
.stepper__b:disabled { color: var(--ink-4); background: var(--hair-soft); cursor: default; }
.stepper__b svg { stroke: currentColor; }
.stepper__v {
  min-width: 26px;
  display: grid;
  place-items: center;
  padding: 0 4px;
  font-size: 16px;
  user-select: none;
}

/* ---------- exercise rows ---------- */
.rows { display: flex; flex-direction: column; gap: 8px; }

/* THE CONTROLS ALL GET THE SAME WIDTH, WHICH IS THE WIDEST ONE'S.
   "3 × 10" is two steppers and a times sign; "20 min" is one stepper and a
   word. Right-aligning each on its own left every tile's controls starting at a
   different place down the list, which reads as a wobble rather than a column.
   So the whole list is one grid and each tile is a subgrid of it: the browser
   sizes the middle track to the widest set of controls in the plan — no guessed
   pixel width that a four-digit number or a longer unit would overflow — and
   `justify-self: start` leaves the slack on the right, against the tick.
   display:contents on the body puts the name and the controls into that grid
   without changing the markup, so the rescue screen's rows, which share these
   class names and have a different shape, are untouched.
   Guarded: without subgrid the declaration would be dropped and the tile would
   stack into one column, so the flex layout above stays the fallback. */
@supports (grid-template-columns: subgrid) {
  .rows--aligned {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    row-gap: 8px;
    column-gap: 12px;
  }
  .rows--aligned .row {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
  }
  .rows--aligned .row__body { display: contents; }
  /* Stretched to the track rather than shrink-wrapped, so the box really is the
     width of the widest one; the controls sit at its left edge and the slack
     falls on the right, between the last control and the tick. */
  .rows--aligned .row__set { margin-left: 0; justify-content: flex-start; }
}
.row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--card);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  padding: 11px 12px 11px 14px;
  box-shadow: var(--lift);
  position: relative;
  overflow: hidden;
  text-align: left;
  width: 100%;
}
.row::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--hair);
}
.row--done { background: #fcfdfe; }
.row--done::before { background: var(--accent); }
/* Same size and weight as the plan screen's list. One exercise should not look
   like two different things in two places. */
.row__name {
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0;
  text-align: left;
  min-width: 0;
  /* Two lines, then dots. A name long enough to need a third is one nobody is
     reading anyway, and the tile has a fixed height to keep. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}
.row--done .row__name { color: var(--ink-2); }
.row__half { color: var(--ink-4); margin-left: 6px; }
.row__meta { color: var(--ink-3); margin-top: 1px; }
/* Name and controls on one line, the controls pushed right. */
.row__body { flex: 1; min-width: 0; display: flex; align-items: center; gap: 10px; }

/* the load, live in the tile */
.row__set { display: flex; align-items: center; gap: 6px; margin-left: auto; flex: none; }
.row__x { color: var(--ink-4); font-size: 13px; }
.row__unit { color: var(--ink-3); }

/* The scoring tick. Full height of the tile rather than a 34px square: with the
   load now edited on the tile, two steppers were out-weighing the one control
   that actually finishes the exercise. */
.tick {
  width: 44px;
  align-self: stretch;
  flex: none;
  border-radius: 8px;
  border: 1.5px solid var(--hair);
  display: grid;
  place-items: center;
  background: var(--card);
  transition: background 0.16s ease, border-color 0.16s ease, transform 0.16s cubic-bezier(0.3, 1.6, 0.5, 1);
}
.tick--on { background: var(--accent); border-color: var(--accent); }
.tick--on svg { stroke: #fff; }
.tick svg { stroke: var(--ink-4); }
@keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } }
.tick--pop { animation: pop 0.34s cubic-bezier(0.3, 1.4, 0.5, 1); }

/* A line in a list that opens something — the plan a manager can change, an
   exercise in the library. Borderless on purpose: it sits inside a panel with
   its siblings, and five button-shaped rows would read as five buttons rather
   than as the plan. */
.planrow {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 2px 0;
  text-align: left;
  color: var(--ink);
}
.planrow:active { opacity: 0.55; }
.planrow svg { stroke: var(--ink-4); flex: none; }
.planrow .chip { margin-left: 7px; }
/* Added part-way through the week, and therefore not counted against anybody
   for the days before it arrived. */
.planrow__new { color: var(--accent); margin-left: 7px; }

/* ---------- club strip ---------- */
.strip { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
.strip__m { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  border: 1.5px solid var(--hair);
  background: var(--card);
  color: var(--ink-3);
  position: relative;
}
.avatar--done { background: var(--ink); border-color: var(--ink); color: #fff; }
.avatar--part { border-color: var(--accent); color: var(--accent); background: var(--accent-tint); }
.avatar--away {
  border-style: dashed;
  border-color: var(--ink-4);
  color: var(--ink-4);
  background: repeating-linear-gradient(45deg, #fff 0 4px, #f2f4f8 4px 8px);
}
.strip__bar--away { background: repeating-linear-gradient(45deg, #e9ecf2 0 3px, #dfe3ea 3px 6px); }
.avatar--photo {
  background-image: var(--img);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  color: transparent;
}
/* Away, with a picture. The dashed grey ring is the whole signal and it
   disappears against a photograph, so the photo is desaturated and washed back
   until the ring reads again. grayscale() is safe on the border because the
   away border is already achromatic; the wash is a background layer rather than
   opacity, which would fade the ring along with it. */
.avatar--away.avatar--photo {
  background-image: linear-gradient(rgba(246, 248, 251, 0.62), rgba(246, 248, 251, 0.62)), var(--img);
  filter: grayscale(1);
}
.avatar--sm { width: 28px; height: 28px; font-size: 10px; }
.avatar--lg { width: 54px; height: 54px; font-size: 16px; }
.strip__bar { width: 100%; height: 4px; border-radius: 2px; background: var(--h0); overflow: hidden; }
.strip__bar i { display: block; height: 100%; background: var(--accent); }
.strip__name { font-size: 9px; color: var(--ink-3); }

.avatar__dot {
  position: absolute;
  right: -3px;
  bottom: -3px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  display: grid;
  place-items: center;
  border: 2px solid var(--ground);
}

/* ---------- data ---------- */
.grid7 { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
.dow { text-align: center; font-size: 9px; color: var(--ink-3); }
.cell {
  aspect-ratio: 1;
  border-radius: 5px;
  background: var(--h0);
  display: grid;
  place-items: center;
  font-size: 10px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.cell--future { background: transparent; box-shadow: inset 0 0 0 1px var(--hair); color: var(--ink-4); }
.cell--away { background: repeating-linear-gradient(45deg, #eef0f4 0 3px, #e3e7ee 3px 6px); }
.cell--today { box-shadow: 0 0 0 2px var(--accent); }
.cell--dark { color: rgba(255, 255, 255, 0.92); }

.legend { display: flex; align-items: center; gap: 6px; }
.legend i { width: 16px; height: 9px; border-radius: 2px; display: block; }

.bars { display: flex; align-items: flex-end; gap: 8px; height: 92px; }
.bars__col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 5px; justify-content: flex-end; height: 100%; }
.bars__bar { width: 100%; border-radius: 3px 3px 0 0; background: var(--h2); }
.bars__bar--now { background: var(--accent); }

.meter { height: 6px; border-radius: 3px; background: var(--h0); overflow: hidden; flex: 1; }
.meter i { display: block; height: 100%; background: var(--ink); border-radius: 3px; }
.meter--accent i { background: var(--accent); }

.pips { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 3px; }
.pip { height: 12px; border-radius: 3px; background: var(--h0); }
.pip--done { background: var(--accent); }
.pip--short { background: var(--h2); }
.pip--missed { background: transparent; box-shadow: inset 0 0 0 1.5px var(--ink-4); }
.pip--today { background: var(--ink); }
.pip--away { background: repeating-linear-gradient(45deg, #eef0f4 0 3px, #e3e7ee 3px 6px); }

.tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.tile { background: var(--card); border: 1px solid var(--hair); border-radius: var(--radius); padding: 11px 12px; box-shadow: var(--lift); }
.tile__n { font-size: 26px; line-height: 1; }
.tile .mono { color: var(--ink-3); margin-top: 5px; display: block; }

.lb { display: flex; flex-direction: column; gap: 2px; }
.lb__row { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: 8px; }
.lb__row--me { background: var(--ink); color: #fff; }
.lb__row--me .meter { background: rgba(255, 255, 255, 0.18); }
.lb__row--me .meter i { background: var(--accent-lift); }
.lb__row--me .faint, .lb__row--me .muted { color: rgba(255, 255, 255, 0.6); }
.lb__pos { width: 14px; font-size: 11px; color: var(--ink-3); }
.lb__name { width: 84px; font-size: 13px; font-weight: 500; }
.lb__val { width: 40px; text-align: right; font-size: 13px; font-weight: 600; }

/* ---------- tab bar ---------- */
.tabbar {
  position: sticky;
  bottom: 0;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  background: rgba(255, 255, 255, 0.94);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--hair);
  padding-bottom: env(safe-area-inset-bottom);
  z-index: 20;
}
.tab {
  height: var(--tab-h);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--ink-3);
  position: relative;
}
.tab--on { color: var(--ink); }
.tab--on::before { content: ''; position: absolute; top: 0; left: 22%; right: 22%; height: 2px; background: var(--accent); }
.tab svg { stroke: currentColor; }
.tab__dot { position: absolute; top: 12px; right: 24px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }

/* ---------- sheet + toast ---------- */
.scrim { position: fixed; inset: 0; background: rgba(18, 21, 26, 0.44); z-index: 50; display: grid; place-items: end center; }
.sheet {
  width: 100%;
  max-width: var(--shell);
  background: var(--ground);
  border-radius: 16px 16px 0 0;
  padding: 8px 18px calc(20px + env(safe-area-inset-bottom));
  max-height: 88dvh;
  overflow: auto;
  animation: rise 0.24s cubic-bezier(0.2, 0.9, 0.3, 1);
}
@keyframes rise { from { transform: translateY(22px); opacity: 0; } }
.sheet__grab { width: 38px; height: 4px; border-radius: 2px; background: var(--hair); margin: 6px auto 14px; }
.sheet__title { font-size: 22px; margin: 0 0 4px; }

/* ---------- the takeover ----------
   One message, the whole screen, and the only thing in the app that gets it: a
   member has been picked to save the club streak and it expires at 20:00. As a
   panel among panels it carried the same weight as the away note.

   Under the toast (60) and over the sheet's scrim (50), so a failure still has
   somewhere to be said. */
.takeover {
  position: fixed;
  inset: 0;
  z-index: 55;
  display: grid;
  place-items: center;
  padding: 26px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 42%, #35150a 0%, #1a0c07 52%, #0b0709 100%);
  animation: takeover-in 0.26s ease both;
}
@keyframes takeover-in { from { opacity: 0; } }

.takeover__body { position: relative; z-index: 1; width: 100%; max-width: 360px; }
.takeover__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
  color: #fff;
  /* After the blast, not with it. */
  animation: takeover-rise 0.5s 0.2s cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
@keyframes takeover-rise { from { opacity: 0; transform: translateY(14px) scale(0.96); } }

.takeover__title {
  font-size: clamp(25px, 8vw, 33px);
  line-height: 1.04;
  margin: 0;
  text-wrap: balance;
}
.takeover__sub { margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.72); }
.takeover__clock {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  font-size: 30px;
  line-height: 1;
  color: var(--accent-lift);
}
.takeover__clock .mono { font-size: 11px; color: rgba(255, 255, 255, 0.5); }
.takeover .btn { width: 100%; }
.takeover__later {
  color: rgba(255, 255, 255, 0.66);
  border-color: rgba(255, 255, 255, 0.18);
  background: transparent;
}

/* The explosion. Rays are one repeating conic gradient masked to a ring rather
   than two dozen elements, so they open outward from behind the writing and
   never cross it; the rings are the shockwave and the core is the flash. All
   CSS — this appears at most once a day, and a graphics library for it would
   cost every member of every club a download they never use. */
.burst { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
.burst > * { position: absolute; grid-area: 1 / 1; }

.burst__core {
  width: 62vmax;
  height: 62vmax;
  border-radius: 50%;
  /* Bright enough in the middle that the writing sits on light rather than on
     the rays it is drawn over. */
  background: radial-gradient(circle,
    rgba(255, 226, 176, 0.5) 0%,
    rgba(255, 150, 60, 0.34) 26%,
    rgba(190, 70, 15, 0.16) 46%,
    transparent 68%);
  /* The second animation only touches opacity, so it cannot fight the first
     one's transform once the first has settled. */
  animation: burst-core 0.9s cubic-bezier(0.1, 0.75, 0.25, 1) both,
             burst-glow 4.5s 0.9s ease-in-out infinite;
}
@keyframes burst-core { from { transform: scale(0.12); opacity: 0; } 55% { opacity: 1; } to { transform: scale(1); opacity: 0.9; } }
@keyframes burst-glow { 0%, 100% { opacity: 0.9; } 50% { opacity: 0.6; } }

.burst__spin { width: 150vmax; height: 150vmax; animation: burst-turn 44s linear infinite; }
.burst__rays {
  position: absolute;
  inset: 0;
  /* Forty thin rays with a soft ramp on each edge rather than a dozen thick
     ones with a hard step: a conic gradient's hard stops alias into visible
     stairs at this size, and the wide flat bands read as planks rather than as
     light. The ramp is the anti-aliasing, at no cost — a blur filter over
     150vmax would be paid for on every phone that ever sees this.
     Two alternating widths so it looks thrown rather than machined. */
  background: repeating-conic-gradient(from -7deg,
    transparent 0deg,
    rgba(255, 146, 52, 0.34) 1.1deg,
    rgba(255, 206, 138, 0.58) 1.9deg,
    rgba(255, 146, 52, 0.34) 2.7deg,
    transparent 3.9deg,
    transparent 5.6deg,
    rgba(255, 132, 40, 0.22) 6.2deg,
    transparent 6.9deg,
    transparent 9deg);
  /* Open in the middle so nothing crosses the writing, and thinning at the
     edges so the corners are glow rather than stripes. */
  -webkit-mask-image: radial-gradient(circle, transparent 15%, #000 29%,
    rgba(0, 0, 0, 0.6) 41%, transparent 60%);
          mask-image: radial-gradient(circle, transparent 15%, #000 29%,
    rgba(0, 0, 0, 0.6) 41%, transparent 60%);
  animation: burst-open 1s cubic-bezier(0.1, 0.72, 0.24, 1) both;
}
@keyframes burst-turn { to { transform: rotate(360deg); } }
@keyframes burst-open { from { transform: scale(0.08); opacity: 0; } 40% { opacity: 1; } to { transform: scale(1); opacity: 1; } }

.burst__ring {
  width: 26vmax;
  height: 26vmax;
  border-radius: 50%;
  border: 2px solid rgba(255, 150, 60, 0.45);
  animation: burst-ring 2.8s cubic-bezier(0.15, 0.7, 0.2, 1) infinite;
}
.burst__ring--b { animation-delay: 0.55s; }
.burst__ring--c { animation-delay: 1.1s; }
@keyframes burst-ring {
  from { transform: scale(0.22); opacity: 0; }
  16% { opacity: 0.7; }
  to { transform: scale(4.4); opacity: 0; }
}

/* Still an explosion, just not a moving one. */
@media (prefers-reduced-motion: reduce) {
  .takeover, .takeover__card, .burst__core, .burst__rays, .burst__spin { animation: none; }
  .burst__ring { display: none; }
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tab-h) + 18px);
  transform: translateX(-50%);
  background: var(--ink);
  color: #fff;
  padding: 11px 16px;
  border-radius: 999px;
  font-size: 13px;
  z-index: 60;
  box-shadow: var(--lift-2);
  animation: rise 0.2s ease;
  max-width: min(92vw, 380px);
  text-align: center;
}

/* ---------- login ---------- */
.login { min-height: 100dvh; display: flex; flex-direction: column; justify-content: center; gap: 22px; padding: 30px 22px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field input, .field select, .field textarea {
  border: 1px solid var(--hair);
  background: var(--card);
  border-radius: var(--radius);
  padding: 12px 13px;
  min-height: 48px;
  width: 100%;
}
.field textarea { min-height: 74px; resize: vertical; }
.err { color: var(--accent-deep); font-size: 13px; }

/* ---------- dev toolbar ---------- */
.devbar {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--shell);
  z-index: 70;
  background: var(--ink);
  color: #fff;
  font-size: 12px;
}
.devbar__head { display: flex; align-items: center; gap: 8px; padding: 7px 12px; }
.devbar__body { padding: 0 12px 12px; display: flex; flex-direction: column; gap: 10px; }
.devbar select { background: #23272f; border: 1px solid #333944; color: #fff; border-radius: 7px; padding: 6px 8px; min-height: 34px; }
.devbar .mono { color: rgba(255, 255, 255, 0.5); }
.jump { display: flex; flex-wrap: wrap; gap: 6px; }
.jump button {
  border: 1px solid #333944;
  background: #1c2027;
  color: #fff;
  border-radius: 999px;
  padding: 6px 10px;
  font-size: 11px;
}
.jump button.on { background: var(--accent); border-color: var(--accent); }
.devpad { height: 34px; }

/* ---------- admin ---------- */
.admin { max-width: 1180px; margin: 0 auto; padding: 26px 24px 60px; background: var(--ground); min-height: 100dvh; }
.admin__grid { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 22px; align-items: start; }
/* A wide table scrolls inside its own box.
   The admin tables carry nine columns and a phone has 390px, so without this
   the right-hand columns — pending count, timezone, and the button that acts on
   the row — were simply cut off with no way to reach them. Scrolling the page
   sideways is not the alternative: a body that slides horizontally reads as a
   broken layout even when nothing else is wrong. */
.tablewrap { overflow-x: auto; overscroll-behavior-x: contain; }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 0 10px 9px 0; }
.table td { padding: 11px 10px 11px 0; border-top: 1px solid var(--hair-soft); font-size: 14px; }
.table tr:first-child td { border-top: none; }
@media (max-width: 900px) {
  .admin__grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- collectible cards ---------- */
.tcard {
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 14px;
  overflow: hidden;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  color: #fff;
  background: #0e1116;
  box-shadow: var(--lift-2);
  user-select: none;
}
.tcard__art { position: absolute; inset: 0; }
.tcard__art svg { width: 100%; height: 100%; display: block; }
.tcard__grain {
  position: absolute;
  inset: 0;
  opacity: 0.16;
  mix-blend-mode: overlay;
  background-image: radial-gradient(circle at 30% 20%, #fff 0.6px, transparent 0.7px),
    radial-gradient(circle at 70% 60%, #fff 0.5px, transparent 0.6px);
  background-size: 7px 7px, 11px 11px;
}
.tcard__foil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: color-dodge;
  background: linear-gradient(
    115deg,
    transparent 28%,
    rgba(255, 214, 170, 0.55) 42%,
    rgba(120, 220, 255, 0.5) 50%,
    rgba(255, 160, 220, 0.5) 58%,
    transparent 72%
  );
  background-size: 260% 260%;
  transition: opacity 0.25s ease;
}
.tcard--foil .tcard__foil { opacity: 0.85; }
.tcard__inner { position: relative; z-index: 2; display: flex; flex-direction: column; height: 100%; padding: 12px 13px 11px; }
.tcard__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.tcard__tier { border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 999px; padding: 2px 8px; }
.tcard__spacer { flex: 1; }
/* The card's name is the one thing that has to scale with the card.
   Everything else is set at a readable size and stays there — a flavour line at
   6px would be a picture of text rather than text — but a display word can shrink
   and still read. At 27px in the two-up grid, SECRET ADMIRER took two lines and
   those two lines cost more height than the card had, which is what pushed the
   flavour into being sliced through the middle of a line. cqw ties it to the
   card's own width, so it is 27px on the big reveal and about 21px in the grid,
   and no name can outgrow its card again. The plain declaration first: a browser
   without container queries drops the clamp and keeps the size it always had. */
.tcard { container-type: inline-size; }
.tcard__epithet { font-size: 27px; line-height: 0.92; text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45); }
.tcard__epithet { font-size: clamp(21px, 9.6cqw, 27px); }
.tcard__holder { font-size: 13px; font-weight: 600; opacity: 0.92; margin-top: 3px; }
.tcard__stats { display: flex; gap: 10px; margin-top: 10px; padding-top: 9px; border-top: 1px solid rgba(255, 255, 255, 0.2); }
/* overflow:hidden is the guard, not the plan. The labels are kept short at
   source (STAT_LABEL_MAX in src/domain/cards.js); this only makes a future long
   one truncate inside its own column instead of printing over its neighbour. */
.tcard__stat { flex: 1; min-width: 0; overflow: hidden; }
.tcard__stat b {
  display: block;
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  letter-spacing: -0.01em;
}
.tcard__stat .mono {
  display: block;
  font-size: 8px;
  opacity: 0.6;
  letter-spacing: 0.1em;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

/* THE FLAVOUR IS THE PART THAT GIVES WAY.
   A card is a fixed 5:7 rectangle with overflow hidden, and everything in it is
   free text: a two-word epithet takes two lines, and a long flavour takes four.
   Something has to yield when they are all long at once, and until now it was
   whatever happened to be last — which is the footer, carrying the week the card
   was cut in and its kudos count. Cards with a lot to say simply lost it.
   So the flavour is clipped at three lines — which is what the tightest card in
   the set (a two-line epithet plus a long flavour) leaves room for, so the cut
   always lands on a line boundary rather than slicing one in half — it is
   allowed to shrink below that if something longer ever turns up, and the
   footer is pinned out of the negotiation entirely. */
.tcard__flavour {
  font-size: 10.5px;
  line-height: 1.35;
  opacity: 0.78;
  margin-top: 9px;
  min-height: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
}
.tcard__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 9px;
  opacity: 0.62;
  flex: none;
}
.tcard--base { color: var(--ink); background: var(--card); box-shadow: var(--lift); }
.tcard--base .tcard__tier { border-color: var(--hair); }
.tcard--base .tcard__stats { border-top-color: var(--hair); }
.tcard--base .tcard__epithet { text-shadow: none; }

.cardgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 16px; }

/* Duplicates of the same card sit as a stack: offset layers behind, count in
   the corner. One card gets no layers, so a single card never looks like two. */
.stack { position: relative; }
.stack > .tcard { position: relative; z-index: 1; }
.stack::before, .stack::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 14px;
  background: var(--stack, #1b2029);
  box-shadow: 0 1px 3px rgba(18, 21, 26, 0.18);
  z-index: 0;
}
.stack::before { transform: translate(5px, 5px) scale(0.986); opacity: 0.6; }
.stack::after { transform: translate(10px, 10px) scale(0.972); opacity: 0.32; }
.stack--one::before, .stack--one::after { display: none; }
.stack--two::after { display: none; }
.stack__n {
  position: absolute;
  top: -9px;
  right: -9px;
  z-index: 3;
  min-width: 24px;
  height: 24px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: grid;
  place-items: center;
  border: 2px solid var(--ground);
  font-variant-numeric: tabular-nums;
}
.cardgrid--one { grid-template-columns: minmax(0, 1fr); }

/* Transform only — never opacity. A card that has not finished animating must
   still be visible, or a slow frame renders the section as a void. */
@keyframes deal { from { transform: translateY(14px) scale(0.97); } }
.deal { animation: deal 0.42s cubic-bezier(0.2, 0.9, 0.3, 1) both; }

.reveal { display: grid; place-items: center; gap: 14px; }
.reveal .tcard { width: min(74vw, 280px); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
}
