/* =============================================================================
   Team rotation viewer — dark theme.
   Committed to dark on purpose: this is a tool you stare at for a long time next
   to the game, so there is no light palette to swap to.

   Column widths come from display.js's `width` (a character count, since the same
   report also prints to a terminal), scaled by --cw. Change --cw to loosen or
   tighten the whole table at once.
   ============================================================================= */

*, *::before, *::after { box-sizing: border-box; }

:root {
  color-scheme: dark;

  /* surfaces, lightest on top of darkest */
  --bg:        #090b0f;
  --surface:   #12161d;
  --surface-2: #171c25;
  --surface-3: #1e2530;
  --line:      #222a36;
  --line-2:    #2c3543;

  /* text */
  --ink:   #e7ebf3;
  --ink-2: #b3bccb;
  --dim:   #7c8798;
  --faint: #57616f;

  /* accents */
  --accent: #5b9cff;
  --violet: #a98bff;
  --amber:  #f0b444;
  --mint:   #45d6a4;

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;

  /* square corners everywhere on purpose — the two knobs every rounded-corner rule in this
     file reads from, kept rather than deleted so a future change is one edit, not a hunt. */
  --r:   0px;
  --r-s: 0px;
  --sh-1: 0 1px 2px rgba(0, 0, 0, .5);
  --sh-drop: 0 18px 40px -22px rgba(0, 0, 0, .9);
  --sh-2: var(--sh-drop), 0 0 0 1px var(--line);

  /* A column's width is its character count from display.js scaled by --cw, plus --cpad for
     the cell's own horizontal padding — the terminal spelling of the report has no padding to
     pay for, so the character count alone clips here.
     One figure for every column, because the table is set in one font (see `.c` below). There
     used to be a second (`--cwt`, 6.7px) for the columns that were set in the proportional UI
     font — two metrics meant two column widths for one table, and the tracks no longer lined
     up with the character counts they were sized from. */
  --cw: 7.7px;
  --cpad: 7px;
  /* Extra breathing room on the leading column only, so the action name is not flush against
     the table edge. index.js adds it to that column's track width too — the two must agree. */
  --lead: 10px;
}

html { background: var(--bg); }

/*
 * The page itself never scrolls: it is exactly the viewport tall, the summary header keeps its
 * place at the top, and everything below scrolls inside <main>. That is what lets the column
 * headers stay put — `position: sticky` resolves against the nearest scrolling ancestor, so the
 * table can only pin its header row if the scrolling happens somewhere the header lives inside.
 * The old layout scrolled the table in its own box, which put the header out of reach.
 */
body {
  margin: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  color: var(--ink);
  font: 13px/1.5 var(--sans);
  -webkit-font-smoothing: antialiased;
  background: var(--bg);
}

#app {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

/* -------------------------------------------------------------------- topbar */

.topbar {
  flex: none;                       /* stays put; <main> below it is what scrolls */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 14px;
  padding: 14px 26px;
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}
.backlink {
  color: var(--ink-2);
  font-size: 12.5px;
  font-weight: 600;
  text-decoration: none;
}
.backlink:hover { color: var(--ink); }
/* the bar is the back link and nothing else, so the table page has no bar at all rather than
   an empty strip across the top of it (index.ts's own `renderComparison`/`renderDetail`) */
.topbar[hidden] { display: none; }

/* ------------------------------------------------------------------- loading */

/* An overlay over #app rather than a page in its own right (see index.html): the empty table —
   filters, header, whatever rows are already cached — is drawn underneath first and stays
   readable through the blur, so the page has visible structure the whole way through a run
   instead of a bare centered bar on nothing. The wash over the blur keeps the status line and
   bar legible against a table that is mostly light-on-dark text. */
#loading {
  position: fixed;
  inset: 0;
  /* over everything: the pinned headers (40), the search results and popovers (41), the stacked
     aside (41) and the hover panels (200) all paint under a run in progress. Paints over them
     only: clicks and keys go through to the page, so the boxes and the search bar keep working
     mid-run (typing only redraws its own list, see index.ts) rather than waiting on the solve */
  z-index: 300;
  pointer-events: none;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 16px;
  color: var(--dim);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(3px);
}
#loading[hidden] { display: none; }
/* the <p> tags carry the browser's own default margin, which would stack on top of the grid's
   gap above and give the bar/count an oversized, uneven-looking space above them */
#loading p { margin: 0; }

/* status text and the X/total count share one line above the bar — the count used to be its own
   <p> below the track, but an empty-to-filled paragraph there changed the centered block's total
   height once boot() started writing to it, nudging the bar up a few pixels mid-run. Both spans
   live in the status line's own line box instead, so the count filling in never changes anything
   the bar's position depends on. */
.loading-status {
  display: flex;
  align-items: baseline;
  gap: 8px;
}
/* what went wrong on the way up (index.html's own handlers), in full under the bar — the one
   part of the overlay that takes the pointer, so the text can be selected and copied */
.loading-error {
  pointer-events: auto;
  user-select: text;
  cursor: text;
  margin: 18px 0 0;
  max-width: min(720px, 90vw);
  max-height: 40vh;
  overflow: auto;
  padding: 10px 12px;
  border: 1px solid #ff5f56;
  border-radius: var(--r);
  background: color-mix(in srgb, #ff5f56 10%, var(--surface));
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.45;
  white-space: pre-wrap;
  text-align: left;
}

/* the progress bar sits under the "running every team's rotation…" line — a fixed-width track
   so the fill's own width is the only thing that moves as teams finish. Sized and bordered to
   read as an actual graphic at a glance rather than a thin, easy-to-miss line — the whole run
   can finish in well under a second, so it has to be unmistakable even in a brief glimpse. */
.progress-count {
  font-size: 13px;
  font-weight: 650;
  letter-spacing: .3px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.progress-track {
  width: 320px;
  height: 14px;
  border: 1px solid var(--line-2);
  background: var(--surface);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .35);
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 85%, white), var(--accent));
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 60%, transparent);
  transition: width .15s ease-out;
}
/* the gif under the bar: a block, so the grid's own gap spaces it and no inline baseline gap
   sits beneath it; its width/height attributes hold the slot before the frames arrive */
.loading-gif { display: block; }
/* Swapped for the one above the moment the error box is unhidden — the single signal both failure
   paths share, index.html's own window handlers and index.ts's `boot().catch`, so neither has to
   know about this. A background rather than an <img>: a `display: none` element's background is
   never fetched, so a clean load pays nothing for it. */
.error-gif {
  display: none;
  width: 200px;
  height: 200px;
  background: url("./error.gif") center / contain no-repeat;
}
#loading:has(.loading-error:not([hidden])) .loading-gif { display: none; }
#loading:has(.loading-error:not([hidden])) .error-gif { display: block; }

/* small uppercase section label — above the action log and each of the two rotation tables below
   it, so it carries its own top margin the way .gridwrap and .eventlog do rather than relying on
   .top's padding */
.summary-label {
  margin: 22px 0 8px;
  font-size: 12px;
  font-weight: 500;
  color: var(--dim);
}
/* the count's own aside, pushed to the far end of the heading's line: a sentence rather than a
   label, so it drops the uppercase the heading wears and reads a size up */
.summary-label:has(.hint) { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.summary-label .hint { text-align: right; }
/* The comparison table's own heading pins to the top of <main> with the column headings under it,
   so the team count and the hint survive scrolling down a long table. Scoped to `.tcbody`: the
   rotation tables' labels below belong to blocks of their own and scroll with them. */
.tcbody > .summary-label {
  position: sticky;
  top: 0;
  /* level with the column headings it sits above (both 5) — they never overlap, and the aside
     above them both stays at 6 */
  z-index: 5;
  background: var(--bg);
  /* Both gaps carried as padding, never margin: a margin sits outside the pinned box, so it
     scrolls away and leaves the heading flush to the scrollport edge with the grey column
     headings against its underside. As padding they are part of what pins, so the band above the
     count and the gap down to the table read the same scrolled as at rest — and `--headtop`,
     measured off this box, moves the column headings down with them. The lower 22px is the
     8px this used to carry plus `.tcwrap`'s own top margin, which it gives up below. */
  margin: 0;
  padding: 12px 0;
}

/* -------------------------------------------------------------- rotation tables */

/* Damage per rotation / Energy Requirements, side by side below the action log.
   Pinned to the left of <main>'s own sideways scroll: the action log below is wider than the
   scrollport and scrolls the page sideways, and these two would otherwise slide off with it.
   `width: 100%` is the scrollport rather than that scrolled-out width, so they stay in view and
   do their own scrolling instead. */
.rtables {
  display: flex;
  flex-wrap: wrap;
  /* the two tables sit close, side by side or one under the other; the row's own top margin
     stands in for the label's, which the blocks' labels give up (below) */
  gap: 12px 18px;
  margin-top: 22px;
  /* both blocks to the height of the taller: Damage Contribution's pies take up the slack
     (`grid-template-rows` off dprTable), so the two finish level */
  align-items: stretch;
  /* centred in the scrollport rather than run up against its left edge — the pair is narrower than
     the action log below, which is what the page's width is actually set by */
  justify-content: center;
  position: sticky;
  left: 0;
  width: 100%;
  box-sizing: border-box;
}
/* Each block is exactly as wide as its own table, no slack handed out: side by side the second
   table sits right after the first rather than out at the far edge. Two blocks share a line only
   while both fit on it (`flex-wrap` above); the moment they no longer both fit, Energy
   Requirements wraps under. `overflow-x` bites only for a table wider than the page on its own. */
.rtable-block {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow-x: auto;
  /* a column, so the table under the label can be handed the block's leftover height */
  display: flex;
  flex-direction: column;
}
.rtable-block .summary-label { margin-top: 0; }
/* Damage Per Rotation and Energy Requirements as one pair, so the second falls under the first
   rather than under Loadouts when the three no longer fit across. Its own wrap goes first: the
   row can only shrink this box down to its widest table, which is what makes Energy wrap under
   Damage while both stay beside Loadouts. */
.rstack {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
  /* passes the row's own stretch down to the table inside it */
  align-items: stretch;
  /* Its own width and no more: growing into the row's slack would push Damage Contribution off to
     the far edge instead of leaving the pair centred (`justify-content` above). It still shrinks,
     and wraps under Equipment when the two no longer fit across. */
  flex: 0 1 auto;
  /* `0`, not `min-content`: a min-content floor here held the stack at the width of the table
     inside it, and a table wider than the page then hung off *both* edges of a centred row with no
     way to reach either end of it. Shrinking instead caps the block at the line, which is what lets
     its own `overflow-x` scroll the table sideways. Wrapping is untouched — a line too narrow for
     both blocks still wraps this one under Equipment before either has to shrink. */
  min-width: 0;
}

/* The table is the grid, its rows `display: contents` — one grid for all of them, so a column
   is as wide as its own widest cell across every row (a row-per-grid could only line them up on
   fixed tracks, which is what used to cut `Havoc Rover` down to `Havoc Rov...`). No `overflow`
   of its own: clipping the corners would also make this a scroll container, and a track's auto
   minimum is dropped to zero inside one — the columns would go back to being squeezed under
   their own text. The corner cells carry the radius instead (below). */
.rtable {
  display: grid;
  min-width: min-content;
  /* stretched taller than its rows need, a table keeps its rows at their own height and leaves the
     slack below — only Damage Contribution spends it, on the one row that asks for it (`1fr`) */
  align-content: start;
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--sh-2);
}
/* The name column to its longest name and the five figure columns each to their own widest
   figure — the table is as wide as its contents, not the page. The pies' row spans every track and
   is wider than the figures come to (`.chartwrap`'s own minimum), and that slack goes to the five
   figure columns alone: the two Energy columns (`.energy`, the detail page's) hold their width. */
.rtable.dpr { grid-template-columns: max-content repeat(5, minmax(max-content, 1fr)); width: max-content; }
/* ...and on the page it takes the whole of its block's height, which is Equipment's height */
.rtable-block > .rtable.dpr { flex: 1; }
/* Loadouts turns the other two on their side — the row label down the left, one column per member
   (`--cols`, written on the element: a two-member team has one column fewer than a three). */
.rtable.loadout { grid-template-columns: max-content repeat(var(--cols), max-content); width: max-content; }

.rtrow { display: contents; }
/* the heading's own band is drawn by the cells, the rows themselves being box-less; no rule
   between rows, same as every other table here */
/* the four corner cells round with the table, the heading's band and the name column's wash
   being the ones that would otherwise square off over it */
.rtrow:first-child .c:first-child { border-top-left-radius: var(--r); }
.rtrow:first-child .c:last-child { border-top-right-radius: var(--r); }
.rtrow:last-child .c:first-child { border-bottom-left-radius: var(--r); }
.rtrow:last-child .c:last-child { border-bottom-right-radius: var(--r); }
/* Thinner than the rows under it — a heading is a label, not a row of its own, and these two
   tables are read for the figures. Its own line-height, not `.rtrow .c`'s 17px. */
.rtrow.rthead .c {
  background: var(--surface-3);
  /* the action log's heading band (`.head .c`), which carries no row border — and the same 7px
     the member headings beside it already take (`.c.mem` below), so all three tables open level */
  padding: 7px 10px;
  border-top: 0;
  line-height: 19px;
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--dim);
}
.rtrow .c {
  /* the action log's own row pitch (`.c` below: 7px + 19px + its 1px border), so the three
     tables read in one rhythm */
  padding: 7px 10px;
  border-top: 1px solid transparent;
  font-family: var(--mono);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  line-height: 19px;
  white-space: nowrap;
  position: relative;
}
.rtrow .c.num { text-align: right; color: var(--ink); }
/* ...but a heading over a figure column stays the heading grey, like the action log's own */
.rtrow.rthead .c.num { color: var(--dim); }
/* the resonator's own name column, in both tables — the same left bar and wash the action table's
   member column wears (`.c.member`), at the stronger 18% these tables want: they have no hover
   state to deepen into, so the resting weight is the only weight there is. */
.rtrow .c.name {
  background: color-mix(in srgb, var(--mem, transparent) 18%, var(--surface));
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--mem, transparent) 70%, transparent);
  /* ...and the name itself reads in that kit's own colour, the way it does everywhere else it is
     written (the action log's member column). The Total row carries no `--mem` and falls back. */
  color: var(--mem, var(--ink));
}
.rtrow.total .c { color: var(--ink); font-weight: 700; }

/* Loadouts wears the member's colour in its heading rather than down the first column the way the
   other two do (`.c.name` above) — here the member is the column, not the row. */
/* The heading holds a resonator's name and the label column's own first cell, and both read as
   content rather than as the band's small uppercase label type — a name is written the same
   everywhere it appears (the action log's own member column, `.c.member`: mono 13px). */
.rtrow.rthead .c.mem, .rtable.loadout .rthead .c.lbl {
  padding: 7px 10px;
  font-size: 13px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
}
.rtrow.rthead .c.mem {
  background: color-mix(in srgb, var(--mem, transparent) 22%, var(--surface-3));
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--mem, transparent) 70%, transparent);
  color: var(--mem, var(--ink));
}
/* The row's own label — a heading for its row, so it reads the heading grey the top band does,
   on the band's own ground: the first column is one unbroken strip from the corner cell down,
   the way the other two tables wash their name column (`.c.name` above). */
.rtable.loadout .c.lbl { background: var(--surface-3); color: var(--dim); }
/* Loadouts' footer row: a member's whole menu-stat list written into the cell rather than hidden
   behind a hover — its own two columns (the stat and its figure) inside the table's one. */
.rtrow .c.menustats { padding-top: 9px; padding-bottom: 9px; }
.c.menustats table { border-collapse: collapse; width: 100%; }
.c.menustats td { padding: 1.5px 0; }
.c.menustats td.k { color: var(--dim); padding-right: 18px; }
.c.menustats td.v { text-align: right; color: var(--ink); }

/* The requirement the Energy Regen menu stat is read against, written into its label. The figure
   alone carries the colour — the "(need" around it stays the label's own tone — underlined either
   way, red where the build's own ER falls short of it (the action table's own
   `.negative`/`.underspent` red) and green where it covers it. Amber between the two, where only
   the run's own slack (`ER_TOLERANCE`) closes the gap — the bar fills, but on nothing the build
   actually wears, and one point more of requirement would cost it a roll. */
.rtrow .erreq { text-decoration: underline; text-underline-offset: 3px; }
.rtrow .erreq.er-under { color: #e5484d; }
.rtrow .erreq.er-slack { color: var(--amber); }
.rtrow .erreq.er-met { color: #3ddc7a; }

/* Damage Contribution's two rules, both drawn on the cells rather than as boxes of their own (the
   rows are `display: contents`, so there is no row box to put a border on).

   The team's own row ruled off from the resonators above it. `.c` already reserves a transparent
   1px top border, so colouring it in shifts nothing. */
.rtable.dpr .rtrow.total .c { border-top-color: var(--line-2); }
/* ...and the Total column ruled off from the four loops, down the five rows that carry a figure —
   the heading above them keeps the band it shares with the rest. An inset shadow rather than a left
   border: the track is `max-content`, so a border would widen the column and shift the figures. */
.rtable.dpr .rtrow:not(.rthead):not(.dist) .c:nth-child(6) { box-shadow: inset 1px 0 0 var(--line-2); }
/* ------------------------------------------------- Damage Contribution: the Distribution row */

/* Every figure in the table is a cell the pies below can be read for; the one they are showing
   wears the outline. The cell is already `position: relative`, so the outline draws over its
   neighbours' edges rather than under them. */
.rtrow .c.dist-cell, .rtrow .c.name[data-dist-row] { cursor: pointer; }
.rtrow .c.dist-cell.sel { outline: 2px solid var(--accent); outline-offset: -2px; z-index: 1; }

/* the pies run the whole width of the table, the row's own label given up to them */
.rtrow.dist .c.distbody {
  grid-column: 1 / -1;
  /* The row is whatever Equipment leaves (the `1fr` track off dprTable), and this is the floor for
     when there is nothing to take it from — the block alone on its own line. The drawings are sized
     against `.chartwrap` rather than carrying their own aspect height up into here, so neither
     distribution pushes this table past Equipment beside it, and both scale into whatever it leaves
     instead. Low enough to fit inside the shortest Equipment on the roster (~195px of slack). */
  min-height: 190px;
  /* the floor matches the gap under a figure's own title, which is what puts a pie's centre exactly
     halfway between that title and the bottom of the cell */
  padding: 12px 10px 8px;
  /* ...but the team's own panes fill the row rather than centring in it, and every pixel of that
     floor is a row of the ranking that would otherwise be cut */
  white-space: normal;
  font-family: var(--sans);
  /* the row is as tall as Equipment leaves it, and both distributions take the whole of it — a pie
     centres in the box its own title leaves rather than in the row */
  display: flex;
  align-items: stretch;
}
/* Narrow enough that Equipment cannot be beside it (the two tables come to ~1500px between them),
   so there is no height to match and no reason to hold the charts down to the floor above: they go
   back to the taller of the two distributions, which is what they had before either had to level. */
@media (max-width: 1100px) {
  .rtrow.dist .c.distbody { min-height: 285px; }
}
/* split down the middle, each pie centred in its own half */
.pies { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.piefig { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 8px; width: 100%; }
/* the table's own heading type — these are two sub-headings inside one row, not content */
.piefig figcaption { font-size: 12px; font-weight: 650; color: var(--dim); }
/* Scales with its half rather than sitting at a fixed size; its frame is drawn wide enough for its
   own labels (panels.ts's pieSvg), so nothing of it runs out to the row's scroll box to be cut. The
   minimum is what keeps the two halves' labels off each other — the row carries it up into the
   table's own width. */
/* The box a drawing is measured by. It carries the width — its own minimum is what keeps the two
   halves' labels off each other, and the row hands that up to the table — while the svg inside fills
   it absolutely, so the ratio the viewBox implies never becomes a height the row has to find. */
.chartwrap { position: relative; flex: 1; min-height: 0; width: 100%; min-width: 330px; max-width: 480px; }
/* `meet` (the default `preserveAspectRatio`) letterboxes the drawing in whatever box it is given, so
   a shorter row shrinks it rather than stretching it out of shape. */
svg.pie, svg.bars { position: absolute; inset: 0; width: 100%; height: 100%; }
svg.pie { overflow: visible; }
/* a leader's label — the name never wears the wedge's colour, the line running to it carries that */
svg.pie text { font-family: var(--sans); }
svg.pie .nm { fill: var(--ink-2); }
svg.pie .pc { fill: var(--dim); font-family: var(--mono); }
/* Only the wedges take the pointer, so a leader or a label lying over one doesn't swallow the
   hover (the offset each slides by is written on its own group by pieSvg). */
/* The wedges sit back off the page so the labels lead: a faint fill inside an edge drawn at full
   strength in the wedge's own colour — the same colour, at the same strength, as the leader running
   off it, so a slice and its label are one mark. The edge is already fully opaque, so it is width
   that carries how hard it reads; a hovered wedge thickens it further on its way out.

   `will-change` is what keeps the move to itself: without it the whole <svg> is re-rasterised for
   the animation, and every label and seam in it shimmers a subpixel as it re-snaps. */
svg.pie .wedge {
  fill-opacity: .4;
  stroke-width: 1.5;
  transition: transform .12s ease-out, stroke-width .12s ease-out;
  will-change: transform;
}
svg.pie .slice:hover .wedge { stroke-width: 2.5; transform: translate(var(--ox, 0px), var(--oy, 0px)); }
/* The leader does not move with it — it redraws to the curve pieSvg parked on it, which is the same
   curve with only its rim end carried out. Its far end stays where the label is. */
svg.pie .leader { transition: d .12s ease-out; }
svg.pie .slice:hover .leader { d: var(--d-out); }
svg.pie .leader, svg.pie text { pointer-events: none; }
/* glyphs positioned at full precision rather than snapped to the device pixel, so a repaint of the
   box they sit in can never land them anywhere else */
svg.pie text { text-rendering: geometricPrecision; }
/* The team's own figure opens two panes rather than two pies: the rotation drawn out, and the
   ranking. Both take the full height of the row — the ranking needs it, since how much of it shows
   is exactly how much fits. */
.rtrow.dist .c.distbody:has(.teampanes) { padding-bottom: 12px; }
.teampanes { flex: 1; align-self: stretch; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
/* Sized off nothing inside: the chart's height is definite here (the figure fills the row) and a
   chart with an aspect ratio then asks for the width that height comes to, which widened the panes
   — and the table — past the pies'. The floor is the pies' own (`.chartwrap`), so the two rows agree. */
.teampanes .piefig { height: 100%; contain: inline-size; min-width: 330px; }
/* sized like the pies beside it, so the team's figures and a resonator's leave the table the same
   width; the chart takes the middle of what its title leaves, the way a pie does */
/* takes the height the pane leaves between the title and the key, which is pinned under it */
svg.bars .axis { stroke: var(--line-2); }
/* the chart's key, right under the chart rather than out on the slack below it: one even column
   per caster, whoever they are */
.barkey {
  margin: 0;
  padding: 0;
  width: 100%;
  list-style: none;
  display: grid;
  /* a floor of 0, not the names' own width: four names came to more than the chart's minimum and
     widened the pane — and the table with it — past what the pies beside it hold it at */
  grid-template-columns: repeat(var(--keys, 4), minmax(0, 1fr));
  font-size: 12px;
  color: var(--ink-2);
}
.barkey li { display: flex; align-items: center; justify-content: center; gap: 6px; min-width: 0; }
.barkey .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
/* The ranking runs to as many rows as the half has room for — it is rendered whole and cut down by
   `fitTopActions`. Absolute inside its own box, so a forty-row ranking contributes nothing to how
   tall the row wants to be: the row is Equipment's height, and the list is cut to that rather than
   stretching it. The chart beside it is what keeps the pair from collapsing when there is no
   height to inherit. `overflow` clips whatever slips past a re-measure. */
.topwrap { position: relative; flex: 1; width: 100%; min-height: 0; }
.topacts {
  position: absolute;
  inset: 0;
  overflow: hidden;
  margin: 0;
  padding: 0;
  list-style: none;
  /* Two columns: the name from the left with the slack behind it, the figure hard right. No padding
     inside a row and no gap between them — a row is exactly as tall as its line, which is what lets
     the bar and the band agree on a height and leaves each band flush with the one above and below
     it, the way the action log's own rows butt up. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-content: start;
  column-gap: 14px;
}
/* each row wears its caster's colour as a left bar, the way a source row does in a panel (`.pop
   td.s`) and a member does down the side of the table (`.c.name`) */
/* The row is a band in its caster's own wash, as far across the column as its damage is of the
   leader's (`--fill`, off panels.ts) — so the top action's runs the whole width, behind its figure
   and all, and the rest are read against it. A hard stop, not a fade: the band has an end. The wash
   is the table's own member column (`.c.name`): 18% of the colour over the surface.
   `subgrid` is what lets the row be a real box, which it must be to carry that band, and still put
   its name and figure in the list's own two columns so those stay aligned from row to row. */
.topacts li {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: stretch;
  /* the bar stays at the left edge whatever the band does, so even a thin one names its caster */
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--own, transparent) 70%, transparent);
  background: linear-gradient(to right,
    color-mix(in srgb, var(--own, transparent) 18%, var(--surface)) var(--fill, 0%),
    transparent var(--fill, 0%));
  font-size: 13px;
  line-height: 21px;
}
/* the name carries no ground of its own any more — the row behind it is the ground, and the
   column gap is all that separates it from the figure */
.topacts .nm {
  grid-column: 1;
  min-width: 0;
  /* the bar's own 2px first, then the 8 the log gives its action names (`.c.action`) */
  padding-left: 10px;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* ...and the figure is what the list is read for, so it takes the brightest ink and the far end */
.topacts .v {
  grid-column: 2;
  padding-right: 8px;
  text-align: right;
  color: var(--ink);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.topacts .pct { color: var(--dim); }
/* `fitTopActions` cuts a row by hiding it, and the rule above would otherwise out-specify the
   browser's own `[hidden] { display: none }` and keep painting it */
.topacts li[hidden] { display: none; }
.nodist { grid-column: 1 / -1; margin: 0; color: var(--dim); font-size: 12px; }

/* ---------------------------------------------------------------------- main */

/* the one scrolling box on the page, both axes */
main {
  flex: 1;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  /* No padding at the top: a sticky header pins to the scrollport edge, so any padding there
     leaves a band the rows scroll through above it. The gap moves to the table's own margin,
     which scrolls away like content should. */
  padding: 0 26px 420px;
}

/* ---------------------------------------------------------------- comparison table */

/* Three groups (see index.js's own comparisonFilters()): first the one holding the boxes belonging
   to neither role plus the search bar, then one per role, MDPS and supports. How many columns they
   stand in, and whether they stand beside the table or above it, is measured rather than guessed
   (see the responsive block below, and index.ts's own `fitSide()`). */
.tcfilters {
  display: flex;
  flex-direction: column;
  gap: 9px;
  /* level with the table's own count heading, which carries the same as padding so it survives
     being pinned (`.tcbody > .summary-label`) */
  margin-top: 12px;
  font-size: 12px;
  color: var(--dim);
}
/* `flex-start`, so one box opening its description doesn't stretch the others in its row to match */
.tcfilter-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-start; }
/* The note-and-search column leads wherever it stands: beside the table it is the corner the eye
   starts from, and above the table the search bar is the first thing on the page, ahead of the
   README and the role boxes (the `.tcsearchrow` order below). The warning is last either way. */
.tcfilters > .tcfilter-row.note { order: 0; }
/* Above the table the boxes stand in a column of their own at the left, one under the other,
   and the search bar with its filter chips takes the column beside them: a grid, the boxes
   auto-placed down the first track and the search row pinned to the second for the height of
   them all — four boxes, so four rows: spanning more opened a row gap for each empty row. */
.tclayout.stack .tcfilter-row.note { display: grid; grid-template-columns: 360px 1fr; gap: 10px; align-items: start; }
.tclayout.stack .tcsearchrow { grid-column: 2; grid-row: 1 / span 4; width: auto; }
.tclayout.stack .tcfilter-row.note > .tcopt { grid-column: 1; }
/* One box per axis: the name left, a labelled checkbox per role hard right, and the description
   opening under both (index.js's own `comparisonFilters()`). They share a basis so the boxes line
   up column-wise, and cap out rather than stretching to a screen's width on a wide window. */
.tcopt {
  /* fixed, not grown: the boxes are read down the columns as much as along the rows */
  flex: 0 0 360px;
  padding: 7px 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--surface);
}
/* an opened box keeps its colour: the arrow alone says it is open */
/* wraps so the Cost box's two dropdowns drop to a line of their own rather than overflow: the name
   has a zero basis, so they only ever wrap when the pair alone outgrows the box */
.tcopt-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; row-gap: 6px; }
/* the Team Cost and Teams dropdowns, hard right where a box's checkbox sits */
.tcselect {
  padding: 2px 6px; border: 1px solid var(--line-2); border-radius: 4px;
  background: var(--surface-3); color: var(--ink); font-family: var(--sans); font-size: 12px;
}
/* the name takes up the slack, which is the whole of what pins the box to the right edge */
.tcopt-name {
  flex: 1;
  display: flex;
  align-items: baseline;
  gap: 5px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.tcopt-name:hover { color: var(--accent); }
/* a role's own checkbox with its name at its left, the pair sitting hard right of the axis name */
.tcopt-role { display: flex; align-items: center; gap: 5px; color: var(--dim); cursor: pointer; white-space: nowrap; }
.tcopt-role:hover { color: var(--ink); }
.tcopt input[type="checkbox"] {
  flex: none;
  width: 15px;
  height: 15px;
  accent-color: var(--accent);
  cursor: pointer;
}
/* what the name opens, ruled off from it rather than floating under it */
.tcopt-desc {
  margin-top: 7px;
  padding-top: 7px;
  border-top: 1px solid var(--line);
  color: var(--dim);
  line-height: 1.5;
}
.tcopt-desc[hidden] { display: none; }
.tcopt-desc ul { margin: 0; padding-left: 15px; }
.tcopt-desc li { margin: 3px 0; }


/* The option search (index.js's own `searchResults()`): a plain themed input, with the top matches
   dropping in as rows directly beneath it. The list floats over the page rather than sitting in
   its flow — in flow, every open and close nudged everything below it down and back up again. */
/* The bar keeps its own width and its own place on the line whatever the chips beside it do, so
   a first chip appearing never steps it down (they wrap under it only when the column is too
   narrow to hold both). */
/* In the note group with Standards rather than a row of its own under every group: there, any
   column's boxes opening pushed it down; here only that one box does. */
/* the search bar, and the filter chips under it — the results list floats over the chips while
   the bar is in use (`.tcsearch-results`) */
.tcsearchrow { display: flex; flex-direction: column; align-items: stretch; gap: 10px; width: 100%; }
/* a block, not a flex column: a fixed-position child (the results, beside the table) takes its
   static position from flow — under the input — where a flex container would put it at the top */
/* `flex: none`: the row is a column now (`.tcsearchrow`), where a 360px basis would be a *height*
   and stood the chips a bar's worth of empty space below it */
/* 1px of padding all round: the box and every row of its list are inset a pixel, which is where
   the focus ring (drawn on the pixel outside whichever of them wears it) lands — inside this
   element rather than on a neighbour's edge, so no scroller or clip ever cuts it. The 362px keeps
   the box itself at the 360px a column beside the table is. */
.tcsearch { position: relative; display: block; flex: none; width: 100%; max-width: 362px; padding: 1px; }
.tcsearch input {
  display: block;
  width: 100%;
  padding: 6px 10px;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 12px;
}
/* One ring, one rule: the focused box wears it until a row of its list is walked to (`.sresult.sel`,
   filterbar.ts), and then that row wears it instead — the box keeps focus and the typing, only the
   ring moves. `:focus-within` rather than `:focus`, so a row holding focus still counts as the bar's. */
.tcsearch input:focus { outline: none; }
/* ...and whichever wears it stands over the other: the box over its list (z-index 41 below) while
   it is ringed, a ringed row over the rows beside it and, its list being above the unringed box,
   over the box — so the ring is whole either way, on pixels the neighbour would otherwise paint */
.tcsearch:focus-within:not(:has(.sresult.sel)) input,
.sresult.sel { outline: 1px solid var(--accent); outline-offset: 0; position: relative; z-index: 42; }
/* Absolute, so nothing below moves as it opens and closes — and nothing is under the input to
   drop onto, the chips standing beside the bar rather than under it (`.tcsearchrow`).
   Over the pinned table header (z-index 40) rather than under it, same as the column popover. */
.tcsearch-results {
  position: absolute;
  /* flush under the box, inside the same 1px gutter it sits in (`.tcsearch`'s padding), and
     unbordered: the ring only ever sits on the box or on one row, never round the list */
  top: calc(100% - 1px);
  margin-top: 0;
  left: 1px;
  right: 1px;
  z-index: 41;
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--surface-3);
}
/* `display: flex` above would beat the attribute otherwise (same as `.trow[hidden]` below) */
.tcsearch-results[hidden] { display: none; }
/* Beside the table the aside is a scroller (`.tcside` below), and a scroller clips an absolute
   child hanging under its bottom edge — so there the list is fixed instead: with every offset auto
   it still sits at its static position under the input, but escapes the clip. A column beside the
   table is always 360px, so the box inside its 1px gutter is 358px, and so is the list. Scrolling
   the aside closes it (index.ts). */
.tclayout:not(.stack) .tcsearch-results { position: fixed; top: auto; left: auto; right: auto; width: 358px; }
/* one row per match: the same hue wash and 2px left marker a name cell wears (`--mem`; gear
   picks wear the Tune Break's colour, same as their chips), no border of its own */
.sresult {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0;
  position: relative;
  padding: 0;
  border: 0;
  background: color-mix(in srgb, var(--mem, transparent) 14%, var(--surface-3));
  font-family: var(--sans);
  font-size: 12px;
  text-align: left;
  color: var(--ink);
  cursor: pointer;
}
/* the 2px marker a name cell wears, flush into the row's own corner so the two colours meet with
   nothing between them. Over the halves, which paint their hover background at static level. */
.sresult::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 1;
  width: 2px;
  background: color-mix(in srgb, var(--mem, transparent) 70%, transparent);
}
/* no wash on the row itself: the half under the pointer is what lights up (`.sact:hover` below).
   The one Tab has walked to is outlined instead — the same mark focus used to leave on it, so the
   list's own colours stay as they read untouched. */
/* the ring itself is the box's own rule above, shared: drawn on the pixel outside the row, not over
   its first one, so the marker keeps the whole of its own corner */
/* The row is one click target: the name, which axis the match is from dimmed after it, and "add"
   hard right. */
.sresult .sact { display: flex; align-items: center; align-self: stretch; padding: 5px 10px; color: var(--ink); }
.sresult .sact.inc { flex: 1; justify-content: space-between; gap: 12px; min-width: 0; }
.sresult .sact:hover { background: color-mix(in srgb, var(--ink) 10%, transparent); }
.sresult .sname { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.sresult .skind { color: var(--dim); font-size: 11px; }
.sresult.none { padding: 5px 10px; color: var(--dim); cursor: default; }


/* The row-cap refusal (table.js's own `rowCapWarning()`), popped at the pointer like a menu when a
   change is refused for pushing the table past `ROW_CAP`. Same signal red as a chip's own "✕"
   (see `.rchip.exc .box` below): this is a refusal, not a theme colour. Its width is capped so a
   sentence wraps rather than running off a narrow screen — `placeInView` does the rest. */
.ctxmenu.rowcap {
  max-width: min(320px, calc(100vw - 12px));
  padding: 8px 12px;
  border: 1px solid color-mix(in srgb, #ff5f56 45%, var(--line-2));
  background: color-mix(in srgb, #ff5f56 12%, var(--surface-3));
  color: var(--ink);
  font-size: 12px;
  line-height: 1.45;
}

/* One chip per resonator filter, filling the line to the right of the search bar (index.js's own
   resonatorChips()).
   Rounded and shadowed, unlike everything else in this file — they float over the page rather
   than sitting in its grid, and the pill shape is what says "this is a token you can dismiss",
   so they opt out of the square-corner rule at the top on purpose. */
.tcchips { display: flex; flex-direction: column; gap: 8px; }
/* the two sections the chips stand in — Include and Exclude — each its label and then its chips,
   wrapping; which way a filter runs is said here rather than inside the chip */
.chipsec { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.chiprow { display: flex; flex-wrap: wrap; gap: 8px; }
/* the one-line "Clear Filters" under the chips: plain underlined grey text, no button about it.
   The underline goes on the inner span: a button's own text-decoration doesn't reach its text in
   every browser (Firefox drops it). */
.clearall {
  /* the 4px under the text is where the underline sits: the aside is a scroll container and
     clips whatever paints past a child's own box, which a 3px-offset underline on a box that ends
     at the text does */
  align-self: flex-start; padding: 0 0 4px; border: 0; background: none; cursor: pointer;
  font-family: var(--sans); font-size: 12px; color: var(--dim);
}
.clearall span { text-decoration: underline; text-underline-offset: 3px; }
.clearall:hover { color: var(--ink); }
.chiplabel {
  font-size: 10px; font-weight: 650; text-transform: uppercase; letter-spacing: .7px;
  color: var(--dim);
}
.rchip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 11px;
  /* the whole pill carries that resonator's hue, the same wash-plus-edge their name cell in the
     table wears (`--mem`, set per chip — see `.trow .c.name`), so a chip is recognisable by colour
     across the page before it's read */
  border: 1px solid color-mix(in srgb, var(--mem, transparent) 55%, var(--line-2));
  border-radius: 999px;
  background: color-mix(in srgb, var(--mem, transparent) 14%, var(--surface-3));
  box-shadow: var(--sh-2);
  font-family: var(--sans);
  font-size: 12px;
  color: var(--ink);
  cursor: pointer;
}
.rchip:hover {
  border-color: color-mix(in srgb, var(--mem, transparent) 80%, var(--faint));
  background: color-mix(in srgb, var(--mem, transparent) 24%, var(--surface-3));
}
/* the right-click menu over a table cell (index.ts's own `showMenu()`): a small card of rows,
   over everything but the hover panels */
.ctxmenu {
  position: fixed; z-index: 150; min-width: 180px;
  border-radius: var(--r); overflow: hidden;
  background: var(--surface-3); box-shadow: var(--sh-2);
  display: flex; flex-direction: column;
}
.ctxitem {
  padding: 4px 12px; border: 0; background: none; text-align: left;
  font-family: var(--sans); font-size: 12px; color: var(--ink); cursor: pointer;
}
.ctxitem:hover { background: color-mix(in srgb, var(--ink) 8%, var(--surface-3)); }

/* a plain `hidden` attribute loses to `.trow`'s own `display: contents` on specificity otherwise
   (an attribute selector and a class selector tie, and author order decides the winner) */
.trow[hidden] { display: none; }

/* only as wide as the columns actually need, and left-aligned by being narrower than the page
   rather than by any alignment rule. A build-naming member cell (see index.js's memberLabel())
   can outgrow the viewport, and that's fine — <main> is the page's own scrollport on both axes. */
.tcwrap {
  /* no top margin: the gap up to the count heading is that heading's own bottom padding, so it
     survives being pinned (`.tcbody > .summary-label` above) */
  margin-top: 0;
  width: max-content;
  border-radius: var(--r);
  background: var(--surface);
  /* the drop shadow alone, never `--sh-2`'s 1px ring: a ring is drawn outside the box, so with the
     count heading pinned flush on top of this one its left and right edges ran up either side of
     the heading as a stray grey line, and its top edge under the heading as another */
  box-shadow: var(--sh-drop);
  /* `clip`, not `hidden`: `hidden` is a scroll container, and the header sticks to its nearest
     scrollable ancestor — which would be this box, which never scrolls, rather than <main>. */
  overflow: clip;
}
/* One grid for the whole table, not one per row: `max-content` columns can only line up across
   rows if every row's cells are tracks of the same grid, which is what `display: contents` on
   `.trow` buys — the row element keeps its class hooks (hover, [hidden], --ratio, the dataset
   the filter reads) but stops generating a box of its own. Anything that used to paint on that
   box — the separator, the header's sticky background, the hover wash — moves onto its cells. */
.tgrid {
  display: grid;
  /* three member names plus Team DPR, the baseline % and the detail link, the no-options-open default — a member's
     own DPR and the Tune Break's both live in a hover now (see index.ts's own `memberCell`/
     `sectionBreakdownPopover`). Overridden by an inline style whenever a Show ... Options box is
     open (see index.ts's own `comparisonTable()`): the weapon/echo/mainstat columns those add are
     conditional on the current filters, not something a static rule here can know. */
  grid-template-columns: repeat(3, max-content) repeat(3, max-content);
  align-items: stretch;
}
.trow { display: contents; }
/* Only the rows near the scroll position are in the grid (index.ts's own `drawWindow()`); a spacer
   stands in for the rest above and below, spanning every track so it takes a whole row's worth of
   height per row it replaces. */
.tgrid .vspace { grid-column: 1 / -1; }
.trow.thead .c {
  position: sticky;
  /* under the count heading, which pins above it — `fitSide()` measures that heading and sets the
     offset, since the hint beside it wraps to a second line on a narrow enough window */
  top: var(--headtop, 0px);
  z-index: 5;
  background: var(--surface-3);
  padding: 9px 10px;
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--dim);
}
/* every heading the same grey, the numeric ones included — `.c.num`/`.c.total` below paint the
   body's figures in full ink, and would otherwise win over the heading grey by source order */
.trow.thead .c.num, .trow.thead .c.total { color: var(--dim); font-weight: 650; }
.trow:not(.thead):hover .c { background: var(--surface-2); }
/* one font, one size, one weight — same reasoning as the rotation table's own `.c` below */
.trow .c {
  padding: 6px 10px;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  line-height: 17px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  position: relative;
  /* every cell reads from the top: a row is as tall as its tallest echo pick (a line per set),
     and the rest of the row lines up with that pick's first line */
  align-content: start;
}
.trow .c.num { text-align: right; color: var(--ink); }
/* a member's own name — full-strength text colour, a left bar and a background wash, same trick
   as the detail table's own member column (`--mem`, see index.js's `gearPopover`/`comparisonTable`) */
.trow .c.name {
  background: color-mix(in srgb, var(--mem, transparent) 10%, var(--surface));
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--mem, transparent) 70%, transparent);
}
.trow:not(.thead):hover .c.name { background: color-mix(in srgb, var(--mem, transparent) 18%, var(--surface)); }
/* full-strength ink, but the table's own 400 like every other cell: these two are what a row
   is read down, and they carry that on colour and the hue ramp rather than on weight */
.trow .c.total { color: var(--ink); }

/* a resonator's own name, clickable: left click filters the table down to teams fielding that
   name, right click drops every team that fields it — everywhere it appears, not just the row
   clicked (see index.js's own resonatorFilters). Nothing is drawn in the cell either way; the
   chips above the table are what show a set filter, and clicking one clears it. */
/* `flex-start`, not `center`: the name reads from the top of a tall row like every other cell */
.trow .c.name.res { cursor: pointer; display: flex; align-items: flex-start; gap: 7px; }
.trow .c.name.res .res-label { overflow: hidden; text-overflow: ellipsis; }

/* a weapon/echo/mainstat pick's own column: the same member colour wash as their name cell,
   `--mem` set alongside it in index.js's own optionCell(), so a pick reads as "this row's member"
   at a glance the same way the name column does. Clickable exactly like a resonator's own name —
   left click requires that pick, right click bars it (index.js's own weaponFilters/echoFilters/
   mainstatFilters) — only when it actually holds one: an empty cell (this member's own role has
   the axis closed) still carries the wash so the row reads as one colour block, it's just inert. */
.trow .c.option {
  background: color-mix(in srgb, var(--mem, transparent) 10%, var(--surface));
}
.trow .c.option:not(:empty) { cursor: pointer; }
/* the level and rank read inside the name cell (index.ts's own `memberLabel`), so no cell of
   their own */
.trow:not(.thead):hover .c.option { background: color-mix(in srgb, var(--mem, transparent) 18%, var(--surface)); }

/* The per-slot Personal/Compare columns, shown wherever that position compares anything (table.ts's
   own `dprAt`): the same member wash as that position's option cells, so a member's columns read
   as one block. */
.trow .c.slotdpr, .trow .c.slotcompare { background: color-mix(in srgb, var(--mem, transparent) 10%, var(--surface)); color: var(--mem, var(--ink)); }
.trow:not(.thead):hover .c.slotdpr, .trow:not(.thead):hover .c.slotcompare { background: color-mix(in srgb, var(--mem, transparent) 18%, var(--surface)); }
.trow.thead .c.huehead { cursor: pointer; }
.trow.thead .c.huehead:hover { color: var(--ink); }
/* a Slot heading opens and shuts that position's own Personal column (table.ts's `personalOpen`);
   its marker points down while the column is shut and swings right once it is open, so the heading
   says which way the next click goes */
.trow.thead .c.slothead { cursor: pointer; }
.trow.thead .c.slothead:hover, .trow.thead .c.slothead.open { color: var(--ink); }
.trow.thead .c.slothead .arrow { transform: rotate(90deg); }
.trow.thead .c.slothead.open .arrow { transform: none; }
/* each DPR header toggles its own column between exact and abbreviated figures (table.ts's own
   `dprExact`); the Team column opens on the exact figure, so abbreviating is its marked state, in
   full ink. Personal carries no mark: its Slot heading beside it is already in ink while the column
   is open, and a second white heading in the same block read as a second thing being said. */
.trow.thead .c.dprhead { cursor: pointer; }
.trow.thead .c.dprhead:hover,
.tgrid:not(.teamexact) .trow.thead .c.dprhead[data-dpr="team"] { color: var(--ink); }

/* The `% of Baseline` column: one continuous hue ramp, written per row as `--hue` by
   `rankVisible()` — red at the strongest team, green at the baseline, purple at the weakest.
   The hue is computed there rather than here because it is piecewise (two slopes meeting at the
   baseline), which `calc()` can express only as two rules that then have to agree at the seam.
   Painted only while the grid carries `.hued` — clicking the Team Compare heading (index.ts's own
   `hueShown`); off by default. */
.trow .c.baseline { cursor: pointer; }
.tgrid.hued .trow .c.baseline {
  background: color-mix(in srgb, hsl(var(--hue, 120) 60% 42%) 12%, var(--surface));
  color: hsl(var(--hue, 120) 60% 55%);
}
.tgrid.hued .trow:not(.thead):hover .c.baseline {
  background: color-mix(in srgb, hsl(var(--hue, 120) 60% 42%) 20%, var(--surface));
}
/* the pinned row: an outline only, since its colour is already the ramp's green */
.trow.isbaseline .c.baseline {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, hsl(var(--hue, 120) 65% 50%) 55%, transparent);
}

/* Every column's own widest cell, in one row that takes no height and is never seen (built by
   index.ts's own `comparisonTable()`). The tracks are `max-content`, so without it a column would
   be only as wide as the hundred rows currently drawn need, and the whole table would change
   width under the reader as a longer cell scrolled into the window. */
.trow.tghost .c {
  height: 0;
  padding-top: 0;
  padding-bottom: 0;
  border: 0;
  overflow: hidden;
  visibility: hidden;
  pointer-events: none;
}

/* the row's own link cell, last in the row: "view rotation" and a small arrow marker,
   navigating to that team's own full rotation/event-log page */
.gotodetail { cursor: pointer; }
/* the comparison table's own link cell reads quiet and lowercase beside the figures */
.trow .c.gotodetail { color: var(--dim); text-transform: lowercase; }
/* the Team Avg DPR figure opens its breakdown on a click, not a hover (see `wireSourcePanels`) */
.c.teamdpr { cursor: pointer; }
.gotodetail:hover { color: var(--ink); }
/* ...and the Slot headings, which open that position's own Personal DPR column rather than
   navigating — same marker, so both places that open something read the same. */
.gotodetail .arrow, .tcopt-name .arrow, .trow.thead .c.slothead .arrow {
  display: inline-block;
  margin-left: 4px;
  color: var(--faint);
  transition: color .12s, transform .12s;
}
.gotodetail:hover .arrow { color: var(--accent); transform: translateX(2px); }
.tcopt-name:hover .arrow { color: var(--accent); }
/* a Slot heading's marker lifts with the heading itself rather than picking up the accent — the
   whole cell is the target, so the two read as one thing lighting up */
.trow.thead .c.slothead:hover .arrow { color: var(--ink); }
/* the ordinary disclosure reading: pointing right at a shut box, swung down once it is open */
.tcopt-name .arrow { transform: none; }
.tcopt.open .tcopt-name .arrow { transform: rotate(90deg); }

/* the gear popover's own two-column shell reuses .pop/table/.k/.v wholesale (see below) — no
   extra rules needed beyond a touch more breathing room since its rows run longer than a stat
   trace's ever do */
.pop.gear td.v { padding-left: 14px; }
.pop .pct { color: var(--dim); font-weight: 400; }
/* Its figures are left-aligned rather than flush right (the `.pop td.v` default every stat-trace
   panel keeps): each is a figure and its share of the whole ("41,203 (37%)"), so the two read as
   one column all the way down instead of being pushed apart by their own lengths.
   `tabular-nums` still lines the digits themselves up, so the numbers stay comparable row to row. */
.pop.breakdown td.v { text-align: left; }

/* Actions rides in a table of its own (index.js's own `damagePopover()`) so the longest action
   name sizes only its own label column, leaving Node/Type/Type 2 and the Total row above at their
   natural tag widths. */

/* The final-damage panel is a product, not a column of amounts: every row below the leading stat
   is a multiplier, so its figures read left-aligned — flush right pushed the `x` signs apart and
   left the factors in a ragged column no one could compare down. `tabular-nums` (inherited from
   `.pop td.v`) still lines the digits themselves up. */
.pop.damage td.v { text-align: left; }

/* The action hover is a list of what the cast *is* — Skill, Fusion, Basic — not of figures, so its
   values read left-aligned against their labels rather than flush right on lengths that mean
   nothing next to each other. */
.pop.info td.v { text-align: left; }

/* The comparison table's own Total DPR hover is the detail page's damage-per-rotation table
   whole, not a panel of rows — so the panel is only the frame around it: no padding of its own,
   and the radius moved up here onto the box that already carries the border and shadow, with the
   table inside it flattened and clipped rather than drawing a second card on top of the first. */
.pop.dpr { padding: 0; border-radius: var(--r); overflow: hidden; }
.pop.dpr .rtable { border-radius: 0; box-shadow: none; }
/* the hint that the cell underneath is a link rides in the table's own corner cell (index.js's
   own `dprTable()`), which is empty on the detail page — no row of its own, and it already wears
   the heading row's small uppercase */
.pop.dpr .rthead .c:first-child { white-space: nowrap; }

/* The resonator hover's three scopes sit side by side: local (with this member's gear above it),
   global, then enemy debuffs. Stacked they ran a page tall on a well-buffed member, and the three
   are separate scopes rather than one running list. Each column is its own table so every name
   column still sizes to its own longest cell. */
.pop.buffs .cols, .pop.gear .cols { display: flex; align-items: flex-start; gap: 26px; }
/* the substat column's roll counts, in ink beside their dim stat names — a single roll fades,
   stat and count both, so the rolls that were actually chosen stand out; the total under them is
   ruled off like any panel's own foot (see `.pop .sum` below) */
.pop.gear td.n { color: var(--ink); text-align: right; }
.pop.gear tr.one td { color: var(--faint); }

/* ---------------------------------------------------------------------- table */

/* No longer a scroll box of its own — <main> scrolls, so the header row can pin to it. */
/* the log and its own label as one block, centred on the page while the log is narrower than the
   scrollport — and, once it is wider, left where it is, since `auto` margins have nothing to hand
   out (which is also what keeps the label level with the log's left edge either way) */
.rotation-block { width: max-content; margin-inline: auto; }
.gridwrap {
  margin-top: 22px;
  background: var(--surface);
  box-shadow: var(--sh-2);
  /* exactly its columns' own width (every track is a fixed calc(), see index.js's colWidth) —
     no stretching to the page, which left the last column trailing a run of empty surface */
  width: max-content;
}
/* under its "action log" label the label's 8px is the gap — the 22px would collapse over it */
.summary-label + .gridwrap { margin-top: 0; }
main::-webkit-scrollbar { width: 10px; height: 10px; }
main::-webkit-scrollbar-track { background: transparent; }
main::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border: 3px solid var(--bg);
}
main::-webkit-scrollbar-thumb:hover { background: var(--faint); }

.grid { min-width: max-content; position: relative; }

/* The white box around a column that has been clicked, or is being dragged: one element laid
   over the grid rather than an outline on each of the column's own cells (index.js's own
   columnBox). The cells cannot carry it — a triggered row fades its whole cell, `opacity` taking
   the box-shadow with it, and the 1px rule between rows crosses the column's two sides — so on
   them the outline came out dimmed in places and cut into segments down the table. */
.colbox {
  position: absolute;
  top: 0;
  bottom: 0;
  border: 1px solid #fff;
  pointer-events: none;
  /* over the pinned heading (z-index 40), so the box closes across it rather than under it */
  z-index: 41;
  transition: transform .16s ease;
}

/* The white box around a block of cells a press ran over (index.js's own cellBox) — the column
   box's reasoning above, bounded to the block's own rows and columns rather than running the
   table's whole height. No transition: it tracks the pointer, and easing it would leave it
   trailing the drag. */
.cellbox {
  position: absolute;
  border: 1px solid #fff;
  pointer-events: none;
  z-index: 41;
}

.r {
  display: grid;
  grid-template-columns: var(--cols);
  align-items: stretch;
  background: var(--surface);
  /* The table's run-out, so the trailing figure is not flush against its right edge. On the row
     rather than in the outermost tracks, where it used to be (see index.js's colWidth): a column
     must not change width by being dragged to or from an end. Nothing matching on the left — the
     member column's colour bar belongs at the table's own edge, and the two text columns carry an
     indent of their own below. */
  padding-right: var(--lead);
}

/* One font, one size, one weight for every cell in the table — the action and member columns
   used to be set in the proportional --sans while the stat columns were set in --mono, and no
   amount of matching `font-weight` hid that: --sans renders visibly heavier and wider at the
   same nominal weight. Nothing below re-declares font-family, font-size, or font-weight; a
   column differentiates itself with colour and alignment only. */
.c {
  /* more on the right than the left: every numeric column is right-aligned, so that is the
     side its digits actually run up against — the table's own right edge, a neighbouring
     column, and the outline a lifted column is drawn with (see wireColumnDrag) */
  padding: 7px 5px 7px 2px;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  /* Explicit and whole, not the inherited `line-height: 1.5` ratio, so a row's height is a whole
     number of pixels and rows can't leave hairline gaps between themselves. */
  line-height: 19px;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  position: relative;
}
/* alignment and colour only — the type itself is `.c`'s, same as every other column */
.num { text-align: right; color: var(--ink); }

/* head — pinned to the top of <main> so the column names survive scrolling. No border of its own:
   a 2px strip of page background used to sit above it, inside the table's own ring, which at rest
   read as a gap cut out of the table's top edge. The header is opaque, so rows sliding under it
   stay hidden either way. */
.head {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--surface-3);
}
/* the one place in the table that does re-declare weight — a column heading has to read as a
   heading. Same family and size as the cells under it, still. */
.head .c {
  /* one more pixel of right padding than the cells below (`.c`), so a heading sitting right
     against its column's own right edge is not quite as tight as the figures under it */
  padding: 7px 6px 7px 2px;
  /* smaller than the cells below on purpose: uppercase + letter-spacing at the body size would
     outrun the narrow columns, whose tracks are sized off their values, not their headings */
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: var(--dim);
  background: var(--surface-3);
  /* a heading picks its whole column up and slides it to a new place — index.js's own
     wireColumnDrag, which writes the outline and the slide as a stylesheet of its own */
  cursor: grab;
  user-select: none;
}
/* The action heading takes the same indent as the names under it (`.c.action` below), rather
   than starting 6px to their left. Keyed off `data-col`, which wireColumnDrag writes onto every
   heading — the colour is why it can't just wear `.c.action` itself, which would repaint the
   heading in full-strength ink. */
.head .c[data-col="action"], .head .c[data-col="member"] { padding-left: 8px; }

/* while a column is up: the grab cursor everywhere, and nothing selectable under the pointer */
body.coldrag, body.coldrag .head .c { cursor: grabbing; }
body.coldrag { user-select: none; }
/* body rows. Always positioned, so the hidden expand checkbox has something to anchor to;
   raised on hover so a source panel opening downward is not painted under the next row. */
/* Rows off screen are skipped entirely — no style, no layout, no paint — until they scroll near.
   An action log runs to hundreds of rows and each carries a column's worth of cells, so this is
   most of what scrolling one used to cost. Safe for the grid's own width: every row's columns are
   fixed `calc()` tracks (see colWidth), not content-measured, and the header row is never skipped,
   so `max-content` still resolves to the same width whatever is currently rendered.
   `auto` in the intrinsic size means "remember what this row last measured", so the scrollbar
   settles after a row has been seen once instead of guessing forever. */
.step {
  position: relative;
  content-visibility: auto;
  contain-intrinsic-size: auto 28px;
  /* Each step paints its own wash (the same `--m` its row reads, see rotationTable) and starts
     1px up inside the step before it, cells and all. At a fractional zoom the rows land on
     subpixel edges, and an edge blended against whatever lay behind — the near-black page, or a
     wash a shade off the cell's own — came out as a dark hairline between rows, a notch in the
     member column's colour bar at every row; overlapping, the later row's cells cover the boundary
     pixel outright. A step's paint containment (content-visibility) clips anything a *cell* could
     reach up with, so the step's own box is the one thing that can overlap. */
  background: color-mix(in srgb, var(--m, transparent) 4%, var(--surface));
  margin-top: -1px;
}
/* ...but not over the heading, nor over the 1px loop bar (`.loopline`), which it would cover */
.step:first-child, .loopline + .step { margin-top: 0; }
/* The rule between rows is drawn by the cells rather than across the whole step, so that the
   member column can sit it out: as one line over the full width it cut that column's colour bar
   into a dash per row. Transparent rather than absent there, so the cells all keep the same
   height and their text stays on one line. */
/* ...and transparent everywhere now: the rows run unruled, the border kept only so every row
   stays the height the intrinsic size above assumes */
.step > .r > .c { border-top: 1px solid transparent; }
/* Where a loop begins: a bar the full width of the table, ahead of the first row of Loop 1, 2
   and 3 (index.js's own rotationTable), its name hung off the table's right edge and centred on
   it. An element of its own rather than a border on the row's cells, so it runs straight across
   the member column's colour bar — the one line that should — and the row under it drops its own
   grey rule so the two don't stack. */
/* over the rows (which stack in source order, so the row after would otherwise paint across its
   label), under the pinned heading (40) and the hover panels (200) */
.loopline { position: relative; z-index: 10; height: 1px; background: #39ff14; }
.loopline > span {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 8px;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .6px;
  color: #39ff14;
}
.loopline + .step > .r > .c { border-top-color: transparent; }
.loopline + .step > .r > .c.member { --rule: transparent; }
/* the member column's own rule (see `.c.member`) is off like every other row rule now: drawn, it
   read as a break in the column's colour bar at every row */
.step > .r > .c.member { border-color: transparent; --rule: transparent; }

/* Every row carries a faint wash of its own *action's* colour (white/green for generic gear,
   otherwise whoever's kit it belongs to) — deliberately faint, since the strong "whose turn is
   this" signal lives entirely in the member column instead (see below), not competing with it. */
.step > .r        { background: color-mix(in srgb, var(--m, transparent) 4%, var(--surface)); }
/* the row under the pointer, not the whole group: inside an opened group the label row would
   otherwise stay lit while its parts are read, a band above them */
.step > .r:hover  { background: color-mix(in srgb, var(--m, transparent) 8%, var(--surface)); }

/* the action name reads in plain full-strength white — "whose turn" is entirely the member
   column's own signal (see below), so the action name doesn't also compete for it. A little
   breathing room before the name: it isn't column 0 any more, so it no longer picks up the
   lead-in `.r > .c:first-child` gives the member column beside it. Colour only — the type is
   `.c`'s, identical to every stat column. */
/* Both text columns are indented wherever they sit — the action names have always been, and the
   member names need it to clear their own colour bar. The headings match (`.head .c[data-col]`). */
.c.action, .c.member { padding-left: 8px; }
.c.action, .c.name { color: var(--ink); }
.c.avg { color: var(--ink); font-weight: 700; }
/* the Avg column wears the same member wash the member column does (`.c.member` below, minus its
   colour bar) — a column of totals picked out from the figures that fed it, in whose turn it is */
.grid .r:not(.head):not(.totalrow) > .c.avg { background-color: color-mix(in srgb, var(--m, transparent) 10%, var(--surface)); }
.grid .r:not(.head):not(.totalrow):hover > .c.avg { background-color: color-mix(in srgb, var(--m, transparent) 18%, var(--surface)); }

/* mv/energy/concerto/offtune: a dotted underline when a stat buff actually moved this cell's own
   value, not just its usual carried/declared trace (see display.ts's own ReportRow.buffed) — and
   on a forte gauge, when this cast wiped the bar before its own delta landed, which is the CLEAR
   row its panel carries. White, so it reads as "something other than the plain figure is in play"
   without implying anything's wrong the way the concerto-underspend colour below does. */
.c.buffed { text-decoration: underline dotted #fff; text-underline-offset: 3px; }

/* concerto only: an outro that fired without a full 100-point bar banked — a real "we didn't
   have enough" case, so it reads as an error, in red. The colour alone: the underline stays the
   `.buffed` note's, drawn only when a buff moved the cell, and takes the red with it then. */
.c.underspent { color: #e5484d; }

/* forte gauges only: the gauge itself has gone negative — same red treatment as underspent
   concerto, since it's the same "ran out" story. */
.c.negative { color: #e5484d; }

.c.buffed.underspent, .c.buffed.negative { text-decoration-color: currentColor; }

/* The member column: its own colour system, sourced from `--mem` (the member's own colour, set
   inline per cell) rather than `--m` (the row's action colour) — a left bar and a background
   wash of its own, all independent of whatever the rest of the row is tinted by. Again: colour
   only. */
/* The bar is a background rather than an inset shadow: a shadow is drawn inside the padding box,
   which stops it at the border above, while a background paints out to the border box and so runs
   straight through it. Colour and wash are set separately so the hover below can restate one
   without having to repeat the other. */
.c.member {
  background-image:
    linear-gradient(to bottom, var(--rule, transparent) 1px, transparent 1px),
    linear-gradient(to right,
      color-mix(in srgb, var(--mem, transparent) 70%, transparent) 2px, transparent 2px);
  /* the rule picks up 2px along, past the bar; both layers are measured from the border box, so
     they cover the 1px the transparent border above reserves for them */
  background-origin: border-box;
  background-position: 2px 0, 0 0;
  background-size: calc(100% - 2px) 100%, auto;
  background-repeat: no-repeat;
  background-color: color-mix(in srgb, var(--mem, transparent) 10%, var(--surface));
}
.step .r:hover .c.member { background-color: color-mix(in srgb, var(--mem, transparent) 18%, var(--surface)); }

/* total row */
.totalrow {
  background: var(--surface-3);
  border-top: 1px solid var(--line-2);
}
.totalrow .c { color: var(--ink); }
.totalrow .c:first-child { border-bottom-left-radius: var(--r); }
.totalrow .c:last-child  { border-bottom-right-radius: var(--r); }

/* ------------------------------------------- chains: click a row for its parts */

.chain > .r { cursor: pointer; }
/* sits after the action name, so the names themselves stay left-aligned down the column */
.caret {
  display: inline-block;
  width: 11px;
  margin-left: 3px;
  color: var(--dim);
  transition: transform .14s ease;
}
.tgl { position: absolute; opacity: 0; pointer-events: none; }
.tgl:checked ~ .r .caret { transform: rotate(90deg); color: var(--accent); }
.tgl:focus-visible ~ .r { outline: 1px solid var(--accent); outline-offset: -1px; }

.parts { display: none; }
.tgl:checked ~ .parts { display: block; }

/* The follow-ups a group's own casts queued, while the group reads as one beat: ordinary rows in
   every way — same wash, same weight — just sitting after the group instead of inside it. Opening
   the group hides them here and `.parts` shows them back in the place they actually resolved in,
   so the same hit is never on screen twice. */
.tgl:checked ~ .spill { display: none; }

/* A field (a summon's whole run of hits — index.ts's own rotationTable): the hits sit hidden in
   the places they actually fired until the field's own row is opened, which is where they appear.
   The rules that show them are generated per field, since a hit can be nested anywhere — inside
   another group's spill, or as a copy among its parts — and no sibling selector reaches it from
   the field's row. */
[data-fh] { display: none; }
/* ...and once shown they read as the field's own, indented exactly as an opened group's parts are
   (`.parts .name` below). A copy sitting among a group's parts is already indented as one. */
[data-fh] .c.action { padding-left: calc(var(--lead) + 18px); }
.spill > .r       { background: color-mix(in srgb, var(--m, transparent) 4%, var(--surface)); }
.spill > .r:hover { background: color-mix(in srgb, var(--m, transparent) 8%, var(--surface)); }
/* a folded spill row is a chain of its own inside the block (index.ts) — same wash as its plain
   neighbours, its label row and its opened hits alike */
.spill .chain > .r       { background: color-mix(in srgb, var(--m, transparent) 4%, var(--surface)); }
.spill .chain > .r:hover { background: color-mix(in srgb, var(--m, transparent) 8%, var(--surface)); }

/* An opened group reads as the rows it always was: the same wash at the same strength as a
   top-level row, off each row's *own* member colour (see partRows), not the group's and not a
   flatter surface. Only the indent below marks them as sitting inside the group. */
.parts > .r       { background: color-mix(in srgb, var(--m, transparent) 4%, var(--surface)); }
.parts > .r:hover { background: color-mix(in srgb, var(--m, transparent) 8%, var(--surface)); }
.parts .c { padding-top: 4px; padding-bottom: 4px; }
.parts .name { padding-left: calc(var(--lead) + 18px); }

/* A triggered follow-up, a zero-damage hit, or an outro isn't a rotation beat of its own —
   thinner and dimmer, so scanning the table isn't slowed reading past them. Only the text fades:
   the fill colour is halved off whatever colour each cell already has (the member column's own,
   avg's ink), so the washes and colour bars behind the text stay at full strength — `opacity`
   faded those too. Text-fill rather than `color`, since `currentColor` inside `color` would read
   the parent's colour, not the cell's. */
.r.short .c { padding-top: 2px; padding-bottom: 2px; -webkit-text-fill-color: color-mix(in srgb, currentColor 50%, transparent); }
.parts .r.short .c { padding-top: 1px; padding-bottom: 1px; }

/* ------------------------------------------------------- hover source panels */

.has {
  cursor: help;
}
/*
 * The panel is laid out against the viewport, not its cell, and index.js puts it in place.
 *
 * It cannot be `absolute`: a cell clips its own overflow — that is what gives a long action
 * name its ellipsis — and the table scrolls inside its own box, so between the two there is no
 * way for a box anchored to a cell to escape. Both clip on the axis the panel has to grow
 * along. `fixed` is resolved against the viewport instead and ignores both, at the cost of
 * needing real coordinates, which is the whole of what the script does.
 */
.pop {
  display: none;
  position: fixed;
  /* Above the pinned column header. Nothing between the panel and the page root sets a
     z-index, so this competes at the top level rather than inside a row's own context. */
  z-index: 200;
  /* tighter top and bottom than sides: the rows inside already carry their own leading, so the
     box only needs enough to keep the first and last off the border */
  padding: 5px 11px;
  border: 1px solid var(--line-2);
  /* opaque, not translucent: the table underneath it is dense and shows straight through */
  background: var(--surface-3);
  box-shadow: 0 20px 44px -12px rgba(0, 0, 0, .95), 0 0 0 1px rgba(0, 0, 0, .35);
  white-space: nowrap;
  text-align: left;
  /* the exact same font/size/weight the table's own stat columns use (see `.num`/`.c` below) —
     no separate scale for a popover just because it floats above the table instead of sitting
     in it. */
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  color: var(--ink-2);
}

/* A one-line panel: prose, not figures, so it reads in the page's own face rather than the
   tabular one every other panel uses. */
.pop.tip { font-family: inherit; font-size: 12px; color: var(--ink-2); }

/* No `width: 100%` — that stretched every panel out to `.pop`'s own box (which used to carry a
   flat `min-width: 250px`, gone now too) rather than letting the table size itself, so a
   two-column panel's columns weren't each sized to their own longest cell, just proportioned
   across whatever width the box happened to be. Default `table-layout: auto` with no forced
   width sizes every column to the widest cell it actually holds, and the panel's own box (no
   min-width of its own any more) then hugs that. */
.pop table { border-collapse: collapse; }
.pop td { padding: 1.5px 0; }
/* the buff's own name reads full-strength, with a tiny left bar in whoever granted it colour
   (see `.c.member` above — same trick, --own instead of --mem); the stat it fed sits in the
   dimmer tone the name itself used to carry */
.pop td.s {
  color: var(--ink);
  padding-left: 6px;
  padding-right: 14px;
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--own, transparent) 70%, transparent);
}
/* A figure panel's own headings (Final Damage, Motion Value, Crit Rate, Off-tune...) read flush
   right, over the column of numbers they head, rather than out on the left away from them. Only
   the stat panels: a resonator's buff list and an action's own fields are read as text, left. */
.pop.stat .sec td { text-align: right; }

/* label and value differ by colour and alignment only — the type is `.pop`'s, for all three */
.pop td.k { color: var(--dim); padding-right: 14px; }
.pop td.v {
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.pop tr:first-child.sec td { padding-top: 0; }
/* the rule is what marks a total or a summary row; the text keeps the rows' own weight and size
   (the cell block's own panel is a plain list, detail.ts's `blockPanel` — no rule, nothing to sum) */
.pop .sum td { padding-top: 5px; border-top: 1px solid var(--line-2); }
.pop .sum td.v { color: var(--ink); }

/* a group heading — base / bonus / flat, a scope ("Basic Dmg Bonus"), or the column's own name on
   a panel with no groups of its own. A group carries no subtotal of its own, so the heading is the
   only thing separating one from the next. The first heading in a panel drops its top padding:
   `.pop`'s own box already sets the gap above it. */
.pop .sec td {
  padding: 7px 0 2px;
  font-size: 10px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .7px;
  color: var(--dim);
}
.pop tr:first-child.sec td { padding-top: 0; }
/* the dot/tune constant heading (panels.ts's `empty`) names what this one row scales off rather
   than heading a group, so it reads in the rows' own type — the same font "Relative" and every
   other label in the panel carries — instead of the small uppercase a group heading uses */
.pop .sec.plain td {
  font-size: inherit;
  font-weight: inherit;
  text-transform: none;
  letter-spacing: normal;
}
/* A loadout row. Its value column is a list of gear names, not numbers — left-aligned so they
   read down as one column of text rather than being pushed apart by their own lengths. The Avg
   DPR panel left-aligns its own figures too (see `.pop.breakdown td.v` above), so the two
   sections share one edge; this rule is what carries the alignment in the detail page's own
   loadout-only panel, which is not a `.breakdown`. The menu-stats section under it (`.stat`)
   keeps the same left edge even though its own column is numbers, not names — one popover,
   one edge for its whole value column. */
.pop .gear td.v, .pop .stat td.v { text-align: left; }

/* The hover on an action's own name: what it scales off, its element, its damage type, every
 * declared amount it carries — a label/value row apiece, its two columns auto-sized to their own
 * longest cell exactly like every other panel's (see `.pop table` above). Carries no rules of its
 * own at all: it is the same box, the same dim `.k` label, and the same white mono `.v` value as
 * every stat panel above, just with no total row. */

/* -------------------------------------------------------------- event log */

.eventlog {
  margin-top: 26px;
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--sh-2);
  overflow: hidden;
}
.eventlog h2 {
  margin: 0;
  padding: 11px 15px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-3);
  font-size: 10px;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .7px;
  color: var(--dim);
}
.eventlog ol {
  margin: 0;
  padding: 10px 16px 12px 38px;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.85;
  color: var(--ink-2);
}
.eventlog li::marker { color: var(--faint); font-size: 10px; }

/* ------------------------------------------------------------------- error */

.error {
  margin: 26px;
  padding: 16px 18px;
  border: 1px solid #6b2a2a;
  border-left: 3px solid #ff6b6b;
  border-radius: var(--r-s);
  background: rgba(255, 107, 107, .06);
}
.error h2 { margin: 0 0 6px; font-size: 14px; color: #ff8f8f; }
.error p { margin: 0 0 12px; color: var(--ink-2); }
.error pre {
  margin: 0;
  padding: 12px;
  border-radius: var(--r-s);
  background: rgba(0, 0, 0, .4);
  color: var(--dim);
  font-family: var(--mono);
  font-size: 11.5px;
  white-space: pre-wrap;
  overflow-x: auto;
}
.error code {
  padding: 1px 5px;
  background: rgba(0, 0, 0, .45);
  font-family: var(--mono);
  color: var(--amber);
}

/* ------------------------------------------------------------------- responsive
   Last in the file on purpose: every rule here overrides a base one above it, so source
   order is what decides it — same specificity either way. */
/* Narrow first: the filters stack above the table and the table has the width to itself — which
   is also the source order, so a phone and a screen reader get the same reading. From 900px the
   window is wide enough for the aside to stand beside the table at all; whether it actually does,
   and in how many columns, is then measured against the table (index.ts's own `fitSide()`).
   `main` is still the one scrollport on both axes, so the aside sticks to its top-right corner
   rather than scrolling away from a table taller or wider than the window. */
.tclayout { display: flex; flex-direction: column; }

@media (min-width: 900px) {
  .tclayout { flex-direction: row; align-items: flex-start; gap: 20px; }
  /* An empty band the aside's own width, so the table reads as centred on the page rather than
     in what is left of the page: 370px and a 20px gap stand either side of it, and `.tcbody`'s
     two auto margins split the remaining slack evenly between them. */
  .tclayout::before { content: ""; flex: 0 1000 370px; }
  /* the table's own column: `min-width: 0` so a wide table overflows `main` (which scrolls)
     rather than pushing the aside off the side of it. The auto margins are what centre it, and
     the huge shrink factor on the band above is what spends itself first: a table too wide to
     sit centred eats the band, sliding left, and only once it is gone does anything else give. */
  .tcbody { min-width: 0; }
  /* centred either way: beside the table the empty band above is what it centres against, and
     stacked the aside is narrowed to the table's own width and centred with it (`fitSide()`), so
     the boxes and the search bar still keep the line the table keeps */
  .tcbody { margin-left: auto; margin-right: auto; }
  .tcside {
    order: 1;
    flex: none;
    position: sticky;
    top: 0;
    right: 0;
    /* over the table's own pinned header (z-index 5), under the hover panels (200) */
    z-index: 6;
    /* the narrowest of the three, until `fitSide()` has measured which one this table can afford
       (the 10px for its own scrollbar) */
    width: 370px;
    /* `0`, not `auto`: an auto margin here would take a third share of the slack and pull the
       table off centre. `.tcbody`'s own autos hand the aside the page's right edge instead. */
    margin-left: 0;
    background: var(--bg);
    /* its own scroller, capped at the scrollport's height by `fitSide()`: a column of open boxes
       taller than the window scrolls on its own, and a wheel over it never reaches the table */
    overflow-y: auto;
    overscroll-behavior: contain;
    /* the scrollbar's 10px is always reserved, scrolling or not: taken out of the content width
       only when the boxes grow taller than the window, it left the columns 544px to stand in and
       the second one wrapped under the first — a 360px hole beside the boxes, and a scrollbar at
       the far side of it */
    scrollbar-gutter: stable;
  }
  .tcside::-webkit-scrollbar { width: 10px; }
  .tcside::-webkit-scrollbar-track { background: transparent; }
  .tcside::-webkit-scrollbar-thumb { background: var(--line-2); border: 3px solid var(--bg); }
  .tcside::-webkit-scrollbar-thumb:hover { background: var(--faint); }
}
/* One column of boxes, 360px (`.tcopt` above) plus the 10px its own scrollbar always has
   reserved (`scrollbar-gutter` above), against the page's right edge (`margin-left: auto`). */
/* Not even one column's worth of room beside the table, which only the page can know
   (index.ts's own `fitSide()`) — so the aside goes above it and spends the full width instead. */
/* as wide as the table rather than the scrollport: a sticky aside can only slide within its own
   containing block, and one exactly the aside's width leaves it nowhere to go */
.tclayout.stack { flex-direction: column; width: max-content; min-width: 100%; }
/* nothing to centre between once the aside is above the table, and as a column item the band
   would only open a gap above it */
.tclayout.stack::before { display: none; }
/* Width and side margins are `fitSide()`'s: it narrows the aside to the table's own width and
   centres the pair, and falls back to the scrollport's width hard left for a table too wide to
   centre in it. Sticky to the scrollport's left rather than static, for that second case: above a
   table wider than the window (a phone) `main` scrolls sideways, and the boxes would ride off the
   left edge with the table's first columns. They stay put instead, and being no wider than the
   scrollport is what leaves the aside room to stick in. */
.tclayout.stack .tcside {
  position: sticky;
  left: 0;
  top: auto;
  right: auto;
  order: 0;
  align-self: flex-start;
  width: auto;
  margin-left: 0;
  background: none;
  overflow: visible;
  /* sticky is a stacking context, so the search results' own z-index (41) only counts inside
     it: the aside itself has to clear the pinned table header (40) for the list to land on top */
  z-index: 41;
}
/* Above the table the boxes keep their 360px and stand at the left, one under the other, rather
   than stretching to the aside's width (the scrollport's, which `fitSide()` sets); the edge gap
   is the phone's 12px rather than the desktop 26px. */
main.stack { padding: 0 12px 420px; }
.tclayout.stack .tcopt { flex: 0 1 360px; max-width: 100%; }

/* a phone: one box per line, full width, and less of the page given over to margin */
@media (max-width: 560px) {
  main { padding: 0 12px 420px; }
  /* no room for two columns: one, every box the full width (`flex: none`, since down a column the
     360px basis above would be a *height*), and the search bar with its chips last of all — under
     Enable Matrix Buffs, where the thumb is, rather than at the top of a column of boxes nobody
     scrolled past yet. Its own DOM order is already last, so nothing has to be ordered here; up
     top it also lost the first pixel of its focus ring to `main`'s own clip. */
  .tclayout.stack .tcfilter-row.note { display: flex; flex-direction: column; }
  .tclayout.stack .tcsearchrow { width: 100%; }
  .tclayout.stack .tcopt { flex: none; width: 100%; }
  .tcopt { flex: 1 1 100%; }
  .tcsearch { max-width: none; }
}

/* The line the README opens on is the warning, not a note: it is the one thing in that box a
   reader has to have read, so it carries the table's own red (`.c.underspent`) and an underline. */
.tcopt[data-note="readme"] .tcopt-desc li:first-child {
  color: #e5484d;
  text-decoration: underline;
}
/* the warning is the line, not the bullet beside it, which keeps the colour every other one has */
.tcopt[data-note="readme"] .tcopt-desc li:first-child::marker { color: var(--dim); }

/* ------------------------------------------------------------------ tutorial */

/* The first-run walkthrough (page/tutorial.ts, which measures and places everything in here):
   a card in the empty band left of the table with a curved arrow into the tenth row. Fixed to
   the viewport, so scrolling the table below leaves the pair where they are. Over the pinned
   headers, popovers and hover panels (200), under a run in progress (#loading, 300). */
.tut {
  position: fixed;
  inset: 0;
  z-index: 250;
  /* the page keeps working around it — only the card itself takes clicks */
  pointer-events: none;
}
.tut[hidden] { display: none; }

/* no viewBox: the path is drawn straight in the viewport pixels the card was measured in */
.tut-arrow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* A solid line at the width of the ring it leads to (below), which is dashed — the line is the
   one thing on screen the eye follows end to end, and a broken one reads as a border. */
.tut-path {
  fill: none;
  stroke: #3ddc7a;
  stroke-width: 2px;
  stroke-linecap: round;
}
.tut-arrow marker path { fill: #3ddc7a; }

/* the fill is translucent rather than the whole card, so the page reads through it while its own
   text stays at full ink */
/* Whatever the tutorial is pointing at, ringed in the card's own green so the arrow lands on
   something the eye can already see. `.c` in the selector for the distribution cells, whose own
   selected outline (`.rtrow .c.dist-cell.sel`) would otherwise outrank it. */
/* Inside its own box, not outside it: a ring drawn past the edge is clipped by whatever the target
   stands in — the menu clips its lines (`overflow: hidden` for its corners), and the aside clips
   the search bar the same way it clips the Clear Filters underline — and a neighbouring cell would
   paint its background over the rest. */
.tut-target, .c.tut-target {
  /* dashed rather than dotted: the dashes an outline draws are three times its width, which is as
     long as CSS lets them be without drawing the ring by hand — and at the arrow's own width, so
     the line and the ring it lands on are the same mark */
  outline: 2px dashed #3ddc7a;
  outline-offset: -2px;
}
/* ...and over its neighbours all the same, so a cell wearing an inset outline of its own (the
   selected figure's `z-index: 1`) cannot paint across the edge they share. Not on the search bar:
   it is already a positioned box, and a stacking context there would trap its results list under
   the table's pinned header. */
.c.tut-target, .rchip.tut-target, .ctxitem.tut-target {
  position: relative;
  z-index: 2;
}

.tut-box {
  position: absolute;
  pointer-events: auto;
  padding: 14px 16px;
  border: 1px solid #3ddc7a;
  background: rgba(18, 87, 47, .78);
  color: #fff;
  box-shadow: var(--sh-drop);
  font-size: 12.5px;
  line-height: 1.55;
}
.tut-box p { margin: 0; }
/* a line of text at the bottom left, not a button: the card is asking for something else */
.tut-buttons { display: flex; justify-content: flex-start; margin-top: 12px; }
.tut-skip, .tutstart {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: #3ddc7a;
  text-decoration: underline;
  cursor: pointer;
}
.tut-skip { font-size: 11px; }
.tut-skip:hover, .tutstart:hover { color: #8bf0b4; }
