/* The Lineup Generator workspace.
 *
 * ONE CHROME, 2026-09-11. site.css owns the design: tokens, type, masthead, hero, sections,
 * buttons, and the gp-* workspace vocabulary the front page's #products still is built from.
 * This file is only what a still does not need: the interaction states (hover, focus, pressed,
 * disabled), the form controls that sit in the gp-val slots, the real table's widths, and the
 * three panels the still has no equivalent for (Review, Entries, Research).
 *
 * Rules held here: every colour is a var(--...) from site.css, never a hex, so the light theme
 * follows the toggle for free. Every radius is var(--radius). No second hero plate: the page
 * plate is site.css's .hero__media--page, exactly as on every other page.
 */

/* ---- The frame ------------------------------------------------------------
   Same ruled housing as .gen-preview on the front page, minus user-select: none,
   because this one is worked in rather than looked at. */
.gen-frame {
  --gp-ink: var(--parchment);
  --gp-mono: var(--display);
  margin-top: var(--s5);
  border: 1px solid var(--line);
  /* The frame's own top edge, after the shared hairline. It sat flush against
     the page with only the panel background to separate it, so the workspace
     began without anything saying it had. Same device the rail steps use.
     It is a longhand after the shorthand, so it only works BELOW that line. */
  border-top: 2px solid var(--brass);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: inset 0 1px 0 var(--edge-hi), 0 24px 60px -30px var(--shadow);
  color: var(--gp-ink);
  /* --ui, not --display. The workspace is a dense product surface, which the type tokens
     say takes the interface face; it was set in Sora, the marketing headline face, while the
     page preloads only Inter and the new step heads were already in Inter. One face now. */
  font-family: var(--ui);
  font-size: 0.86rem;
  overflow: hidden;
}

/* The bar. Deliberately NOT .gp-bar: site.css defines .gp-bar twice, and the second
   definition (the exposure meter, 4.2rem wide and 5px tall) wins for anything it shares.
   Reported rather than patched into site.css, which this file must never edit. */
.gen-frame__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* GROUPED LEFT, NOT SPREAD. The earlier note here said the groups spread
     because the bar "simply ran out of content", and that was true when it
     carried four more strings. With only the title and the tabs left, spreading
     them put 1100px of nothing between two objects at 1440, so they read as two
     unrelated islands instead of a title and its controls. Bunched left the
     title labels the tabs, which is what it is for, and the slack becomes
     margin on one side rather than a hole in the middle. */
  justify-content: flex-start;
  gap: var(--s3) var(--s5);
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, var(--edge-hi), transparent);
}
/* The bar's .gp-slate and .gp-live rules are GONE. Both were dead: no element
   carries gp-slate anywhere any more (git grep finds it in stylesheets only),
   and every .gp-live element sits inside a panel, not in the bar. They were
   left behind when the bar was stripped to the title and the tabs. Dead rules
   in the block everybody edits are how the next person gets misled about what
   this bar contains. */

/* ---- Tabs ----------------------------------------------------------------
   ONE BLOCK, AND IT HAS TO STAY ONE. The 2026-09-18 restyle first appended a
   second set of rules 1800 lines below this one using the IDENTICAL selectors,
   so `.gp-tabs button[aria-selected="true"]` was declared twice at equal
   specificity and the right thing rendered only because the newer copy
   happened to sit later in the file. Nothing marked that dependency, and this
   header is where anybody would come to change tab styling, so the next edit
   here would have been silently overridden with no error and no failing test.

   The declarations below ARE the restyle. There is no second copy, and
   generator-tab-styles-live-in-one-block.test.mjs fails if one appears, however
   it is spelled.

   WHY THEY LOOK LIKE THIS. The bar used to carry four strings of grey text past
   the title; with those gone the tabs are the only controls up there, and a row
   of underlined words is not what the only control on a bar should look like.
   They echo the segmented switch this page already has: NFL/MLB and
   Classic/Showdown sit twenty pixels below as a joined group with a brass fill
   on the chosen one, so these are built the same way at a larger size. Somebody
   who has used that switch already knows how to read these, which is the
   argument for reusing a house pattern over inventing a prettier one. */
.gen-frame__bar .gp-tabs {
  display: flex;
  /* flex and gap BELONG TO THE ENCLOSURE, and they are the two the earlier
     "spread the tabs across the bar" pass also set (flex: 1 1 26rem and
     gap: var(--s4)). That pass ran before the tabs became a segmented group and
     its block sat LATER in the file, so folding these up here without deleting
     it would hand the group a 26rem basis and a var(--s4) gap. It is deleted. */
  flex: 0 1 auto;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
  min-width: 0;
  padding: 3px;
  border: 1px solid var(--line-soft);
  /* var(--radius), NOT a pill. Two tests require every non-zero radius on this
     page to be that one token, so 999px would be a second radius however well
     it looked. It is also exactly what .gp-seg below uses, which is the echo. */
  border-radius: var(--radius);
  background: var(--field-bg);
  box-shadow: inset 0 1px 0 var(--edge-hi);
}
.gp-tabs button,
.gp-tabs a {
  appearance: none;
  padding: 7px 16px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  font-family: inherit;
  font-size: 0.72rem;
  /* line-height came free from the `font: inherit` this rule used to carry.
     Without it a button falls back to line-height:normal and the tab loses
     a couple of pixels of height. */
  line-height: inherit;
  font-weight: 600;
  /* 0.12em set four uppercase words sprawling across the group. At 0.08em they
     read as one control instead of four labels that happen to be adjacent. */
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease, box-shadow 160ms ease;
}
.gp-tabs button:hover:not([aria-selected="true"]),
.gp-tabs a:hover {
  color: var(--brass-bright);
  background: color-mix(in srgb, var(--brass) 10%, transparent);
}
/* THE SELECTED TAB IS THE FRONT FACE OF THE PANEL IT OPENS.
   It was a solid brass fill, and directly above it, about ninety pixels up, the
   masthead's GENERATOR button is also a solid brass fill. One of those is an
   ACTION you can press and the other is STATE telling you where you are, and
   they were drawn identically, so the eye had to read the words to tell the
   page from the control. There is one solid brass object on this screen now and
   it is the one you can press.
   What the tab wears instead is the panel's own fill plus a 2px brass top edge,
   which is the exact edge .gen-frame wears at the top of this page. The tab and
   the panel underneath it become one piece of material, which is the thing a
   tab has always been pretending to be. Drawn with inset shadows rather than a
   border so the box does not change size between states and the row cannot
   shift by a pixel when you switch.
   No second radius: var(--radius) or nothing, same as everything on this page. */
.gp-tabs button[aria-selected="true"] {
  color: var(--text);
  /* A VEIL, NOT A SURFACE TOKEN, AND THE REASON IS MEASURED.
     This was var(--panel-2) and that was wrong twice over. First, funded.css
     aliases --panel-2 to --bg-lift at :root and loads after site.css, so on this
     page the token resolves to --bg-lift, which is DARKER than --panel rather
     than one step lighter as site.css's own scale intends. (No literal value is
     written here: the test that forbids a hex colour in this file reads the
     comments too, and naming the value cost a build to learn.) Second, and fatal even
     without that alias: the track this tab sits in is --field-bg, a 4% white
     VEIL, which composites to [27,28,36] over the bar. No opaque surface in the
     panel scale is lighter than that, so no choice from that scale could ever
     have put the tab in front. The tab was rendering as a near-black hole, which
     is the precise defect the old comment here claimed to have fixed.
     A veil of the same kind as the track, just stronger, is forward in dark and
     recessed in light, because --parchment flips with the theme. That is one
     declaration doing the right thing in both rather than a surface that happens
     to suit one of them. */
  background: color-mix(in srgb, var(--parchment) 10%, transparent);
  /* Top edge only. A full 1px ring around the tab turned it back into an
     outlined button, which is the shape this change exists to get away from. */
  box-shadow:
    inset 0 2px 0 var(--brass),
    0 2px 8px -4px var(--shadow);
}
/* THIS RULE IS REDUNDANT WITH THE BASE .gen-count RULE, AND IT STAYS ANYWAY.
   The badge used to borrow currentColor because it sat on a brass fill and had
   to escape it; with the fill gone, brass here is what the base rule already
   says. It was deleted as dead weight, which was correct about the
   rendering, and deleting it failed
   generator-tab-styles-live-in-one-block.test.mjs: that file requires the
   badge-inside-a-selected-tab role to be declared EXACTLY once, and zero is not
   one. The role is guarded because it is the pair most likely to be quietly
   broken by a future change to the selected tab's ground, which is exactly what
   just happened to it. It is restated rather than inherited so the guard has
   something to hold. */
.gp-tabs button[aria-selected="true"] .gen-count {
  border-color: var(--brass);
  color: var(--brass);
}
.gp-tabs button:focus-visible,
.gp-tabs a:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}
@media (max-width: 899px) {
  /* TWO BY TWO, not three and a stray. Four tabs in one wrapping row put the
     last one alone on a second line, which reads as a mistake rather than a
     layout. A grid gives all four the same box and the same two rows. */
  .gen-frame__bar .gp-tabs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    flex: 1 1 100%;
    gap: 2px;
  }
  .gp-tabs button,
  .gp-tabs a {
    padding: 8px 10px;
    letter-spacing: 0.06em;
    text-align: center;
  }
}
.gen-count {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border: 1px solid var(--brass);
  border-radius: var(--radius);
  color: var(--brass);
  font-weight: 600;
}

/* ---- Panels -------------------------------------------------------------- */
.gen-panel { min-width: 0; }
.gen-panel__head { padding: var(--s4) var(--s4) 0; }
.gen-panel__head h3 {
  margin: var(--s2) 0 var(--s2);
  font-family: var(--display);
  font-size: 1.05rem;
  letter-spacing: 0.01em;
}
.gen-state { padding: var(--s4); }
.gen-state .hud-card h3 {
  margin: 0 0 var(--s2);
  font-family: var(--display);
  font-size: 1.05rem;
}
.gen-state .hud-card p { margin: 0 0 var(--s3); color: var(--muted); }
.gen-state .hud-card p:last-child { margin-bottom: 0; }

.gen-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: var(--s3) 0 0;
  align-items: center;
}
.gen-actions--mini { gap: var(--s2); }

.gen-note {
  margin: var(--s2) 0 0;
  color: var(--muted);
  max-width: 60ch;
}
.gen-note:empty { display: none; }
.gen-note--err { color: var(--err); }

/* The slate's own roster rules, printed as a row of small plates. */
.gen-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s3);
}
.gen-meta span {
  padding: 2px 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
}
.gen-meta strong { color: var(--gp-ink); font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---- Full width -----------------------------------------------------------
   Asked for: "lineup generator should use full page width".

   site.css caps every .wrap at --wrap (1248px) plus gutters, which is right for a page of
   prose and wrong for a three-column tool: the rail, a table of 15 players per position and
   the built lineups were sharing 1248px on a 2560px screen with 650px of empty page either
   side. wrap--full exists only on this page and only on the tool, so the site's own measure
   is untouched everywhere it still belongs.

   The gutter stays. Content running into the edge of the glass is not full width, it is
   unreadable, and the cap simply moves out to the viewport. */
.wrap--full {
  max-width: none;
}
.gen-tool { padding-top: var(--s5); }

/* The frame's bar is the page heading now, so it carries the h1 rather than sitting under
   one. Matching the eyebrow it replaced, not shouting. */
h1.gp-brand {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
}
h1.gp-brand .tag { margin-left: var(--s2); }
.gen-frame__note {
  margin: 0;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  max-width: 78ch;
}
.gen-frame__note[hidden] { display: none; }

/* ---- The grid -------------------------------------------------------------
   site.css caps .gp-rail at 46rem and fades the cut, because the still is a picture of a
   rail rather than a rail. Here it is the real thing, so the cap and the mask come off. */
.gen-frame .gp-rail {
  max-height: none;
  overflow: visible;
  -webkit-mask-image: none;
  mask-image: none;
}

/* ---- RESIZABLE COLUMNS (2026-09-17) ---------------------------------------
   The Advanced tab is three columns and the right one holds lineups that are wider than
   20rem on a DraftKings classic roster. Rather than pick a new fixed pair of numbers, the
   two boundaries are draggable and the widths are two custom properties.

   WHY TWO CUSTOM PROPERTIES AND NOT TWO WIDTHS. A drag sets ONE number on .gp-grid
   (element.style.setProperty, which funded/public/_headers permits: style-src there carries
   'unsafe-inline', unlike live-site's). The browser then relays all three tracks itself. The
   alternative, writing a width onto each of the three columns, is three writes that can
   disagree with each other and with the gutters, which is how a resizer ends up one pixel
   off and drifting.

   THE CLAMP IS IN CSS, NOT ONLY IN JS. A stored width from a 2560px monitor, replayed on a
   1280px laptop, would push the middle column to nothing. clamp() with a floor and a ceiling
   means the worst a bad stored value can do is land on one of the two ends, and the middle
   track is minmax(0, 1fr) so it takes whatever is left without ever collapsing its content.

   NO TRANSITION ON grid-template-columns, deliberately. Animating a track list relays three
   scrolling columns every frame and it stutters on exactly the wide slate this feature is
   for. The drag is already frame-smooth because it writes one number inside a
   requestAnimationFrame; the only things that animate are the hairline and the grip. */
.gen-frame .gp-grid {
  --gp-rail-w: 18rem;
  --gp-built-w: 20rem;
  --gp-split-w: 9px;
}

/* The splitter itself. Below 1100px it is not in the layout at all: the grid is one column
   and a 9px sliver between stacked panels would be a line with no meaning and a control with
   nothing to control. */
.gen-frame .gp-split {
  display: none;
  position: relative;
  align-self: stretch;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: col-resize;
  /* The pointer must not scroll the page or select text while a drag is in flight. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
/* The hairline. This is the partition the three columns used to draw with their own
   border-right and border-left; it moved onto the splitter so there is one line rather than
   two with a gutter between them. */
.gen-frame .gp-split::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  transform: translateX(-50%);
  background: var(--line);
  transition: background-color 160ms ease, width 160ms ease;
}
/* The grip. Invisible until the splitter is worth touching, so the resting page is three
   columns with a rule between them and nothing else. */
.gen-frame .gp-split__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 3px;
  height: 2.5rem;
  transform: translate(-50%, -50%);
  border-radius: var(--radius);
  background: var(--brass);
  opacity: 0;
  transition: opacity 160ms ease, height 160ms ease;
  pointer-events: none;
}
.gen-frame .gp-split:hover::before,
.gen-frame .gp-split:focus-visible::before,
.gen-frame .gp-split.is-dragging::before {
  width: 2px;
  background: var(--brass);
}
.gen-frame .gp-split:hover .gp-split__grip,
.gen-frame .gp-split:focus-visible .gp-split__grip { opacity: 0.9; }
.gen-frame .gp-split.is-dragging .gp-split__grip { opacity: 1; height: 4rem; }
.gen-frame .gp-split:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -3px;
  border-radius: var(--radius);
}
/* While a drag is in flight the cursor is col-resize everywhere, not just over the 9px
   strip, and nothing on the page can be selected by the pointer that is dragging. */
.gen-frame.is-resizing,
.gen-frame.is-resizing * {
  cursor: col-resize;
  -webkit-user-select: none;
  user-select: none;
}
/* An iframe or a canvas would swallow the pointer the moment the drag crossed it. Nothing
   in the workspace is either today, and this costs nothing if one arrives. */
.gen-frame.is-resizing iframe,
.gen-frame.is-resizing canvas { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .gen-frame .gp-split::before,
  .gen-frame .gp-split__grip { transition: none; }
}

/* THE ORDER OF THESE TWO RULES IS THE WHOLE FEATURE. `.gen-frame .gp-split { display: none }`
   above and `display: block` here have IDENTICAL specificity, so source order decides, and
   with the media query written first the splitters were display: none at every width: five
   grid tracks laid out, two of them holding nothing, and no handle anywhere on the page.
   Caught by the browser proof on its first run. Do not move this block back up. */
@media (min-width: 1100px) {
  .gen-frame .gp-grid { grid-template-columns: clamp(13rem, var(--gp-rail-w), 32rem) var(--gp-split-w) minmax(0, 1fr) var(--gp-split-w) clamp(14rem, var(--gp-built-w), 40rem); }
  .gen-frame .gp-split { display: block; }
}

/* The Fine-tune row that explains the splitters, unhidden by generator-columns.js. */
.gen-frame .gp-row--cols { gap: 6px; }
.gen-frame .gp-row--cols .gp-row__title { color: var(--muted); }
.gen-frame .gp-row--cols .gp-mini { justify-self: start; }
.gen-frame .gp-rows { margin-bottom: var(--s2); }
.gen-frame .gp-row label { color: var(--muted); }
.gp-row--wide {
  display: grid;
  grid-template-columns: 1fr;
  gap: 4px;
  justify-content: stretch;
}
.gp-row--wide > label { justify-self: start; }

/* ---- Controls in the gp-val slots -----------------------------------------
   A select and a number field have to read as the same machined slot the still draws with
   a span, so they take .gp-val and lose the browser's own chrome. */
select.gp-val,
input.gp-val {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  /* site.css paints a gp-val slot with a flat black wash, which is a dark-theme assumption.
     The site's own field colour is a token and follows the light toggle. */
  background: var(--field-bg);
  border-color: var(--field-line);
  color: var(--text);
  line-height: 1.4;
  cursor: pointer;
  transition: border-color 160ms ease, color 160ms ease;
}
input.gp-val { cursor: text; min-width: 7rem; }
.gp-val--wide {
  width: 100%;
  min-width: 0;
  text-align: left;
}
select.gp-val--wide {
  padding-right: 22px;
  background-image: linear-gradient(45deg, transparent 50%, var(--brass) 50%),
    linear-gradient(135deg, var(--brass) 50%, transparent 50%);
  background-position: calc(100% - 13px) 52%, calc(100% - 8px) 52%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select.gp-val option { background: var(--panel); color: var(--text); }
select.gp-val:hover,
input.gp-val:hover { border-color: var(--brass); }
select.gp-val:focus-visible,
input.gp-val:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--brass);
}
.gp-out {
  min-width: 2.4rem;
  color: var(--brass);
  font-variant-numeric: tabular-nums;
}

.gen-range {
  width: 100%;
  height: 18px;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  cursor: pointer;
}
.gen-range::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--brass), var(--brass-deep));
}
.gen-range::-moz-range-track {
  height: 5px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, var(--brass), var(--brass-deep));
}
.gen-range::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border: 1px solid var(--brass-bright);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.gen-range::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: 1px solid var(--brass-bright);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.gen-range:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The real toggle: a checkbox behind the still's .gp-toggle switch. */
.gen-toggle { cursor: pointer; }
.gen-toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.gen-toggle:has(input:checked) { color: var(--brass); }
.gen-toggle:has(input:checked) i {
  border-color: var(--brass);
  background: color-mix(in srgb, var(--brass) 22%, transparent);
}
.gen-toggle:has(input:checked) i::after { left: auto; right: 2px; background: var(--brass); }
.gen-toggle:has(input:focus-visible) i { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ---- Buttons: the still's gp-cta and gp-mini, made real -------------------- */
button.gp-cta,
button.gp-mini {
  appearance: none;
  border: 0;
  font: inherit;
  cursor: pointer;
  transition: filter 160ms ease, border-color 160ms ease, color 160ms ease;
}
button.gp-cta { border: 1px solid var(--brass-deep); }
button.gp-cta:hover { filter: brightness(1.06); }
button.gp-cta:focus-visible,
button.gp-mini:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
button.gp-cta[disabled] {
  background: none;
  border-color: var(--line);
  color: var(--muted);
  cursor: default;
  filter: none;
}
button.gp-mini {
  border: 1px solid var(--brass);
  background: none;
  color: var(--brass);
}
button.gp-mini:hover { border-color: var(--brass-bright); color: var(--brass-bright); }
button.gp-mini--ban { border-color: var(--line); color: var(--muted); }
button.gp-mini--ban:hover { border-color: var(--danger); color: var(--danger); }
button.gp-mini[aria-pressed="true"] {
  background: color-mix(in srgb, var(--brass) 20%, transparent);
  color: var(--gp-ink);
}
button.gp-mini--ban[aria-pressed="true"] {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  color: var(--gp-ink);
}

.gen-cta-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.gen-cta-bar button.gp-cta { margin-left: auto; }
.gen-cta-bar--wide { margin: var(--s4) var(--s4) 0; }
.gen-cta-note {
  color: var(--muted);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
}

/* ---- Player faces ----------------------------------------------------------
   The initials sit in the box and the photo sits on top of them, so a player with no
   headshot is a finished design rather than a hole. Statyx serves NFL faces and none at
   all for MLB, so the no-photo state is what half our slates draw for every row. */
.face {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 1.9em;
  height: 1.9em;
  overflow: hidden;
  border: 1px solid var(--line);
  /* Not a circle. Every other corner on this page is var(--radius) and one round avatar in
     a table of square-cornered plates reads as a mistake, not as a choice. */
  border-radius: var(--radius);
  background: var(--bg-lift);
  vertical-align: middle;
}
.face__initials {
  font-size: 0.62em;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--muted);
  line-height: 1;
}
.face__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* DK headshots are 512x372 and framed high, so centring the box crops the chin. */
  object-position: 50% 18%;
}
.pool-name {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.pool-name__text { min-width: 0; }
/* site.css sets `.gp-lineup li span { width: 2.6rem; font-family: mono; color: muted }` for
   the slot label, and the face IS a span inside that li, so the unqualified rule would pin
   it to 2.6rem of monospaced grey. These two carry a class more than it does, deliberately,
   and they are the reason the face is not styled from .face alone. */
.gp-lineup li span.face {
  width: 1.9em;
  height: 1.9em;
  margin-right: 0.35em;
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}
.gp-lineup li .face span.face__initials {
  width: auto;
  font-family: inherit;
  font-size: 0.62em;
  color: var(--muted);
}

/* ---- Chips: filters, modes, detail ---------------------------------------- */
button.gp-chip {
  appearance: none;
  font: inherit;
  background: none;
  cursor: pointer;
  text-align: left;
  transition: border-color 160ms ease, color 160ms ease, background 160ms ease;
}
button.gp-chip:hover { border-color: var(--brass); color: var(--gp-ink); }
button.gp-chip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
button.gp-chip[aria-pressed="true"],
button.gp-chip[aria-selected="true"] {
  color: var(--gp-ink);
  border-color: var(--brass);
  background: color-mix(in srgb, var(--brass) 16%, transparent);
}
button.gp-chip[aria-disabled="true"] { opacity: 0.55; cursor: default; }
/* A chip that cannot be picked must not offer itself. The hover above lit the border in
   brass for every chip, disabled ones included, so a card that does nothing when clicked
   looked exactly as clickable as the four that work. */
button.gp-chip[aria-disabled="true"]:hover { border-color: var(--line); color: inherit; }
.opt-rail { gap: 6px; }
.opt-rail:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; }
.opt-card__glyph { display: none; }
.opt-card__title { display: block; }
.opt-card__tag {
  display: block;
  color: var(--muted);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
button.gp-chip[aria-selected="true"] .opt-card__tag { color: var(--brass); }
.opt-detail { margin-top: var(--s3); }
.opt-detail__line { margin: 0 0 var(--s2); color: var(--muted); }

.gen-sports { margin-bottom: var(--s3); }
/* The contest-type segment is the sport segment's twin and shares every rule it has. It sits
   directly under it, so only the gap between them is its own. */
.gen-styles { margin-bottom: var(--s4); }
.gen-sports button,
.gen-styles button {
  appearance: none;
  font: inherit;
  background: none;
  color: var(--muted);
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease;
}
.gen-sports button:hover,
.gen-styles button:hover { color: var(--gp-ink); }
.gen-sports button:focus-visible,
.gen-styles button:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.gen-sports button[aria-pressed="true"],
.gen-sports button[aria-checked="true"],
.gen-styles button[aria-pressed="true"],
.gen-styles button[aria-checked="true"] {
  background: var(--brass);
  color: var(--bg);
  font-weight: 600;
}
.gen-filters { margin-bottom: var(--s3); }

/* ---- The pool table -------------------------------------------------------
   The still's .gp-table draws nine narrow columns. The real one holds names, so the two
   text columns get room and everything else stays on its figures. */
.gen-scroll { overflow-x: auto; max-width: 100%; }
/* A floor on the table only where there is room for one. At phone width site.css has already
   dropped three columns, so the five that are left fit the gutter and nothing scrolls
   sideways: a workspace that pushes the whole page off screen is not a workspace. */
@media (min-width: 641px) {
  /* Scoped to the pool. This floor reached the four-column Teams and Stacks tables on Review
     too, and pushed their Max % inputs off the right edge of their own track. */
  .gen-frame .gp-pool .gp-table,
  .gen-frame .lineup-grid { min-width: 34rem; }
}
/* Positional rules, scoped to the one table they were measured on. Unscoped they gave the
   wrong column 40% in five of the six tables and right-aligned eight of nine roster slots. */
.gen-frame .gp-pool .gp-table th:nth-child(2) { width: 40%; }
.gen-frame .gp-pool .gp-table td:nth-child(n + 4) { text-align: right; }
.gen-frame .gp-pool .gp-table th:nth-child(n + 4) { text-align: right; }
/* The class the script already writes on every numeric cell. Honour it everywhere. */
.gen-frame .gp-table td.num,
.gen-frame .gp-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.gen-frame .gp-table td:last-child,
.gen-frame .gp-table th:last-child { text-align: right; white-space: nowrap; }
.gen-frame .gp-table tbody tr:nth-child(2) { background: none; }
.gen-frame .gp-table tbody tr:hover { background: color-mix(in srgb, var(--brass) 8%, transparent); }
.gp-table tr.unconfirmed td { color: var(--muted); }
.gp-table tr.banned td:nth-child(2) { text-decoration: line-through; }
.gp-table .flag {
  margin-left: 6px;
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.gp-table .flag--lock { border-color: var(--brass); color: var(--brass); }
.gen-search input {
  width: 100%;
  min-width: 8rem;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
  color: var(--text);
  font: inherit;
}
.gen-search input:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.gen-search { flex: 1 1 9rem; min-width: 0; }
.gen-sort { flex: 0 0 auto; }
.gp-more:empty { display: none; }
/* The pool is top 15 per position, so on a full slate it is hundreds of rows. Give it its own
   scroll instead of letting it set the height of the whole page: the rail, the pool and the
   built lineups have to stay side by side to read as one workspace. */
.gen-frame .gp-pool .gen-scroll {
  max-height: 30rem;
  overflow-y: auto;
}
@media (min-width: 1100px) {
  .gen-frame .gp-pool .gen-scroll { max-height: 46rem; }
}

/* ---- Built lineups -------------------------------------------------------- */
.gen-frame .gp-lineups { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 1100px) {
  .gen-frame .gp-lineups { max-height: 44rem; overflow-y: auto; }
}
.gp-lineup li em {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-style: normal;
}
.gp-lineup li b {
  margin-left: auto;
  color: var(--muted);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
/* ---- A ROW THAT OPENS -------------------------------------------------------------------
   The caret, and the four numbers it reveals. Everything here is already on the page in the
   pool table; what was missing was a way to read it from the lineup, which is where the keep
   or drop decision is actually made.

   THE CARET IS A FIXED-WIDTH COLUMN, not a floated glyph. Nine rows whose names start at nine
   different x positions depending on whether a caret is open is a list that looks broken, so
   the button occupies the same box open or shut and only its mark rotates. */
.gp-open {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}
.gp-open::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transform-origin: 30% 50%;
  transition: transform 120ms ease;
}
.gp-open.is-open::before { transform: rotate(90deg); }
.gp-open:hover { color: var(--brass-bright); }
.gp-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gp-open::before { transition: none; } }

/* The detail is its own row rather than an overlay, for the same reason the Replace list is:
   it has to push the rest of the card down instead of covering the next two names. */
.gp-detail-row { padding: 0; }
.gp-detail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
  gap: var(--s2) var(--s3);
  margin: var(--s2) 0 var(--s3) 1.1rem;
  padding: var(--s3);
  border-left: 2px solid var(--brass-deep);
  background: var(--panel-2);
  border-radius: var(--radius);
}
.gp-detail__cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.gp-detail__k {
  font-size: var(--t-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
}
.gp-detail__v { font-family: var(--mono); color: var(--text); white-space: nowrap; }

.gp-lineup.is-dropped { opacity: 0.62; }
.gp-lineup.is-dropped header { text-decoration: line-through; }

/* ---- Review --------------------------------------------------------------- */
.review-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: var(--s3);
  margin: var(--s4) var(--s4) 0;
}
.review-stat {
  display: grid;
  gap: 2px;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-lift);
}
.review-stat__k {
  color: var(--muted);
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.review-stat__v { color: var(--gp-ink); font-variant-numeric: tabular-nums; }
.review-grid {
  display: grid;
  gap: var(--s4);
  margin: var(--s4) var(--s4) 0;
}
@media (min-width: 1100px) {
  .review-grid { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
}
.review-side { display: grid; gap: var(--s4); align-content: start; min-width: 0; }
@media (min-width: 1100px) {
  /* Same reason as the pool: a full slate's exposure table is forty rows and would set the
     height of the page instead of sitting inside the frame. */
  .review-grid .gen-scroll { max-height: 40rem; overflow-y: auto; }
}
.review-block { min-width: 0; margin: 0 var(--s4) var(--s4); }
.review-grid .review-block { margin: 0; }
.review-block__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s2);
}
.review-block__head .gp-kicker { margin: 0; }
/* site.css sets the first column of a gp-table in small brass type, which is right when that
   column is a position code (the pool) and wrong when it is a player or a team name. */
.gen-frame .review-table td:first-child {
  color: var(--gp-ink);
  font-family: var(--display);
  font-size: inherit;
}
.review-pct {
  width: 3.6rem;
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
  color: var(--text);
  font: inherit;
  text-align: right;
}
.review-pct:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
.exp-bar {
  display: inline-block;
  min-width: 3.2rem;
  padding: 1px 5px;
  border-radius: var(--radius);
  background: linear-gradient(90deg, color-mix(in srgb, var(--brass) 45%, transparent) var(--exp, 0%), transparent var(--exp, 0%));
  font-variant-numeric: tabular-nums;
}
.review-none { color: var(--muted); text-align: center; }
.lineup-grid td, .lineup-grid th { font-size: 0.76rem; }
.gp-table tr.is-dropped td { opacity: 0.62; }
.gp-table tr.is-locked td:first-child { color: var(--brass); }
.keep-lineup { accent-color: var(--brass); }

/* ---- Entries -------------------------------------------------------------- */
.entries-layout {
  display: grid;
  gap: var(--s4);
  padding: var(--s4);
}
@media (min-width: 1100px) {
  .entries-layout { grid-template-columns: 18rem minmax(0, 1fr); }
}
.entries-side {
  min-width: 0;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-lift);
  align-self: start;
}
.entries-side .gp-kicker:first-child { margin-top: 0; }
.entries-main { min-width: 0; display: grid; gap: var(--s4); }
.entries-main .review-block { margin: 0; }
.entries-main .gen-cta-bar--wide { margin: 0; }
.entries-drop {
  display: block;
  position: relative;
  margin-top: var(--s3);
  padding: var(--s5) var(--s4);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--muted);
  text-align: center;
  cursor: pointer;
  transition: border-color 160ms ease, color 160ms ease;
}
.entries-drop:hover,
.entries-drop.is-over { border-color: var(--brass); color: var(--gp-ink); }
.entries-drop input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.entries-drop:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The same drop zone, standing in the 18rem rail beside the slate picker rather than in the
   Entries panel. A file IS the choice here: it says how many lineups and for which contests,
   so it sits in step 1 next to the slate and has to survive a narrow column. */
.entries-drop--inline {
  padding: var(--s3);
  font-size: 0.9em;
}
.entries-drop--inline .entries-drop__text {
  display: block;
  overflow-wrap: anywhere;
}
.plan-from, .plan-count {
  width: 4.2rem;
  padding: 1px 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
  color: var(--text);
  font: inherit;
  text-align: right;
}
.contest-name { white-space: normal; min-width: 12rem; }

/* ---- Research and the Statyx boards ---------------------------------------
   The soft-fail handshake in statyx-softfail.js adds .is-live to a slot only once its
   iframe has really painted, so an unmounted or challenged board is covered rather than
   shown as an empty frame claiming to be live. */
.gen-research {
  margin: 0 var(--s4) var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}
.gen-research h3 {
  margin: var(--s2) 0;
  font-family: var(--display);
  font-size: 1.05rem;
}
.gen-research__boards {
  display: grid;
  gap: var(--s4);
  margin-top: var(--s4);
}
@media (min-width: 60rem) {
  .gen-research__boards { grid-template-columns: 1fr 1fr; }
}
.gen-research__slot {
  min-width: 0;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-lift);
}
.gen-research__slot .gp-kicker { margin-top: 0; }
.gen-research__name {
  margin: 0 0 var(--s3);
  font-family: var(--display);
  font-size: 0.94rem;
  letter-spacing: 0.01em;
}
.gen-research__slot statyx-widget {
  display: block;
  min-height: 12rem;
  border-radius: var(--radius);
  overflow: hidden;
}
.gen-research__slot:not(.is-live) statyx-widget {
  opacity: 0.35;
  filter: grayscale(1);
  pointer-events: none;
}
.research-soon__tag {
  margin: var(--s2) 0 0;
  color: var(--muted);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.is-sport-off { display: none; }
.gen-panel .research-soon,
.gen-panel .gen-research__boards { margin-inline: var(--s4); }
.gen-panel .gen-sports,
.gen-panel .gen-styles,
.gen-panel > .gen-actions { margin-inline: var(--s4); }
.gen-panel > .gen-actions { margin-bottom: var(--s4); }

/* ---- The foot ------------------------------------------------------------- */
.gp-foot--fine {
  border-top: 0;
  padding-top: 0;
  text-transform: none;
  letter-spacing: 0.02em;
}

/* ---- Status lines inside the frame ---------------------------------------- */
.gen-frame .status { margin-top: var(--s2); }
.gen-frame .block-note { margin-top: var(--s3); }
.block-note--err { color: var(--danger); }

/* ---- Phone ---------------------------------------------------------------- */
@media (max-width: 640px) {
  .gen-frame { font-size: 0.82rem; }
  .gen-frame__bar { gap: var(--s2) var(--s4); }
  /* NOT `flex-wrap: wrap; gap: var(--s3)`, which is what stood here. It predates the tab
     group becoming a segmented control and sat 900 lines LATER in the file than the 2x2 grid
     that replaced it, so it quietly won: the four tabs kept the grid but took a var(--s3)
     gap, which reads as four separate buttons rather than one control with four cells. The
     2px gap is the group's, declared once, up with the rest of the group. */
  .gp-tabs { gap: 2px; }
  .gen-cta-bar button.gp-cta { margin-left: 0; width: 100%; }
  .gen-frame .gp-table th,
  .gen-frame .gp-table td { padding: 6px 4px; }
  .gen-frame .gp-table td:nth-child(2) { white-space: normal; }
  .gen-frame .gp-table .pool-acts { padding-right: 0; }
  .gen-frame .gp-table .pool-acts button.gp-mini {
    min-height: 24px;
    display: block;
    width: 100%;
    margin: 0 0 3px;
    padding: 2px 5px;
    text-align: center;
  }
  .gen-frame .gp-table .pool-acts button.gp-mini:last-child { margin-bottom: 0; }
}

/* ---- THE PHONE GETS ITS WIDTH BACK -------------------------------------------------------
   Measured at 390x844 with the tool open: 65px of masthead, then 48px of black, then a frame
   inset 24px on each side, leaving the card 316px of a 390px screen. On a tool that is 81%,
   and the 74px it spends on gutters is the difference between a lineup row that fits a name
   and one that ellipsises it.

   The tool is what somebody came to this page for, so it starts near the top and reaches
   nearer the edges. Everything here is scoped to .page-generator: .section and .wrap are
   site.css's, shared with every other page, and their gutters are right everywhere else. */
@media (max-width: 640px) {
  .page-generator .gen-tool { padding-top: var(--s4); }
  .page-generator .gen-tool > .wrap { padding-left: var(--s3); padding-right: var(--s3); }
  .page-generator .gen-frame { margin-top: var(--s3); }
  /* The bar carries the page heading and four tabs on two rows, 131px of chrome above a
     390px-wide tool. Less air around them, same two rows. */
  .page-generator .gen-frame__bar { gap: var(--s2) var(--s3); padding: var(--s2) var(--s3); }
  .page-generator .gen-easy { padding: var(--s4) var(--s3) var(--s5); }
  /* The three blocks are cards inside a card inside a frame. One border less of inset each
     side is 16px more for the thing inside them. */
  .page-generator .easy__card,
  .page-generator .easy__out,
  .page-generator .easy__intel { padding: var(--s4) var(--s3); }

  /* ---- THE ROSTER ROW, ON 390px -------------------------------------------------------
     The row is a caret, a slot label, a face, the name and a Replace button. Every one of
     those except the name has a fixed width, so the name is what gives when the row runs
     out: measured at 390px it had 90px for "Christian McCaffrey", which needs 135. Taking
     the gaps from 12px to 8px and the slot label from 2.6rem to 2.2rem hands it 22 of those
     back, and the face goes, which is the other 40.

     THE FACE IS THE ONE THAT GOES, and it is a deliberate order of preference. It is an
     identity cue for a name that is already written beside it; the name is the thing being
     read. Dropping a decoration before truncating the content is the rule, not a shortcut. */
  .page-generator .gp-lineup li { gap: var(--s2); }
  .page-generator .gp-lineup li > span:not(.face) { width: 2.2rem; }
  .page-generator .gp-lineup li span.face { display: none; }
  .page-generator .gp-swap { padding: 2px var(--s1); }
}

@media (prefers-reduced-motion: reduce) {
  .gp-tabs button,
  .gp-tabs a,
  button.gp-cta,
  button.gp-mini,
  button.gp-chip,
  .gen-sports button,
  .gen-styles button,
  .entries-drop,
  select.gp-val,
  input.gp-val { transition: none; }
}

/* ---- Top pick -------------------------------------------------------------
 * The built lineups are sorted best first (generator-pool.js lineupsBestFirst), so the first
 * card is the highest projection we built. This says so, quietly: a brass hairline down the
 * card's leading edge and one small caps label beside "Lineup 1". No colour fill, no second
 * accent, because the card already carries a brass footer and two golds fight.
 * Nothing is badged when only one lineup was asked for; "best of one" is noise, and the badge
 * is not drawn at all in that case, so this styling never applies.
 */
.gp-lineup.is-top { border-color: var(--brass-deep); box-shadow: inset 3px 0 0 0 var(--brass); }
.gp-lineup.is-top header { color: var(--brass-bright); }
.gp-lineup__top {
  margin-left: 0.5em;
  padding: 1px 6px;
  border: 1px solid var(--brass-deep);
  /* var(--radius), not a pill: generator.test.mjs requires every radius in this file to be
     the one token, so the page cannot grow a second radius scale. A tag, not a pill. */
  border-radius: var(--radius);
  font-family: var(--gp-mono, inherit);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--brass-bright);
  white-space: nowrap;
}

@media (prefers-contrast: more) {
  .gp-lineup.is-top { border-color: var(--brass-bright); }
}


/* =========================================================================
   THE RAIL AS A PATH (2026-09-15)
   Three numbered decisions, one action, and two <details> for everything a
   first-timer does not need yet. The numerals are the signature: engraved
   brass, the same metal as the mark, and the sequence is real (you cannot
   make lineups yours before you have a slate and a count), which is the one
   condition under which numbering earns its place. Everything resolves to a
   token so the light theme follows for free.
   ===================================================================== */

.gp-step {
  position: relative;
  padding: var(--s3) 0 var(--s4);
  border-bottom: 1px solid var(--line-soft);
}
.gp-step:first-child { padding-top: 0; }
.gp-step--yours { border-bottom: 0; padding-bottom: var(--s3); }

.gp-step__head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 var(--s3);
  color: var(--text);
  font-family: var(--ui);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
}
.gp-step__n {
  flex: none;
  display: inline-grid;
  place-items: center;
  width: 26px;
  height: 26px;
  /* --radius, not 50%: the house rule is one radius token everywhere on this page, and a
     rounded square reads as a struck numeral rather than a badge, which suits the mark. */
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--btn-gilt-text);
  background: var(--metal-gold);
  box-shadow: 0 1px 0 var(--edge-hi) inset, 0 1px 2px var(--edge-lo);
  font-variant-numeric: tabular-nums;
}
.gp-step__lead { margin: -4px 0 var(--s3); }

/* The count row: the label carries the range so the one required field says its bounds. */
.gp-hint {
  margin-left: 6px;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--muted);
  text-transform: none;
}
.gp-row--count input.gp-val {
  min-width: 6.5rem;
  font-size: 1.05rem;
  padding: 6px 10px;
}
.gp-row--count input.gp-val:invalid { border-color: var(--field-line); }

/* "or", ruled, between the number and the file. */
.gp-or {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  margin: var(--s2) 0;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.gp-or::before,
.gp-or::after { content: ""; height: 1px; background: var(--line-soft); }

/* Variety: the second of the two tweaks. Ends labelled, because a bare 0 to 100 slider
   with the word Randomness above it told nobody which end was safe. */
.gp-variety { margin-top: var(--s3); }
.gp-variety > label {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 6px;
  color: var(--text);
  font-weight: 500;
}
.gp-variety__ends {
  display: flex;
  justify-content: space-between;
  margin: 4px 0 0;
  font-size: 0.72rem;
  color: var(--muted);
}

/* The action. Bigger than every other control on the rail, brass, and never grey unless the
   source genuinely has no numbers. */
.gen-frame .gp-rail .gen-cta-bar {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s2);
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
/* The note is a sibling ABOVE the pinned bar now, not a child of it, so these no longer say
   .gen-cta-bar. `order` went with the move: it existed to put the note under the button inside
   the bar's own flex box, and there is no such box around it any more. The :empty rule still
   earns its place, because an empty note is the normal state and it would otherwise reserve a
   line of blank rail above Generate on every default build. */
.gen-frame .gp-rail .gen-cta-note { text-align: center; }
.gen-frame .gp-rail .gen-cta-note:empty { display: none; }
.gen-frame .gp-rail .gen-cta-bar button.gp-cta--big {
  order: 1;
  margin-left: 0;
  width: 100%;
  min-height: 48px;
  font-size: 0.95rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  box-shadow: var(--glow-gold-soft);
  transition: transform 120ms ease, box-shadow 160ms ease, filter 160ms ease;
}
.gen-frame .gp-rail .gen-cta-bar button.gp-cta--big:hover:not([disabled]) { filter: brightness(1.06); box-shadow: var(--glow-gold); }
.gen-frame .gp-rail .gen-cta-bar button.gp-cta--big:active:not([disabled]) { transform: translateY(1px); }
.gen-frame .gp-rail .gen-cta-bar button.gp-cta--big:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
button.gp-cta[disabled] { opacity: 0.5; cursor: not-allowed; box-shadow: none; }

/* Fine-tune and Saved profiles: native <details>, styled as quiet drawers. */
.gp-more-set {
  margin-top: var(--s3);
  border-top: 1px solid var(--line-soft);
}
.gp-more-set > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
  cursor: pointer;
  list-style: none;
  color: var(--muted);
}
.gp-more-set > summary::-webkit-details-marker { display: none; }
.gp-more-set > summary::before {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 160ms ease;
  margin-right: 2px;
}
.gp-more-set[open] > summary::before { transform: rotate(45deg); }
.gp-more-set > summary .gp-kicker { margin: 0; color: inherit; }
.gp-more-set > summary:hover { color: var(--text); }
.gp-more-set > summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--radius); }
.gp-more-set > .gp-rows { margin-top: var(--s1); }
.gp-row__note {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.78rem;
}
.gp-row__note:empty { display: none; }

/* The kickers inside the rail that are not step heads (pool, built) keep the mono look;
   only the three step heads take the sentence-case, numbered form above. */

/* The row-level Lock button, so a locked player reads as locked at a glance. */
button.gp-mini[aria-pressed="true"]:not(.gp-mini--ban)::before {
  content: "\2713\00a0";
}

/* Phone: the rail stacks above the pool, so the whole path is the first screen and Generate
   is reachable before any table. Step heads stay, drawers stay shut. */
@media (max-width: 640px) {
  .gen-frame .gp-rail .gen-cta-bar button.gp-cta--big { min-height: 52px; }
  /* The label, its "1 to 150" hint and a 6.5rem input do not share one line at 390px: the
     input was pushed past the right edge and read "How n". Stack them. */
  .gp-row--count {
    display: grid;
    grid-template-columns: 1fr;
    gap: 6px;
  }
  .gp-row--count input.gp-val { width: 100%; min-width: 0; text-align: left; }
}

/* Reduced motion: no glow transition, no chevron spin. */
@media (prefers-reduced-motion: reduce) {
  .gen-frame .gp-rail .gen-cta-bar button.gp-cta--big,
  .gp-more-set > summary::before { transition: none; }
}

/* Review's five headline numbers were the same size as the body text beside them. */
.review-stat__v { font-size: var(--t-lead); line-height: 1.15; }

/* One height for the two scrollers, so the pool and the built lineups end on the same line. */
.gen-frame { --gen-scroll-h: 46rem; }

/* The loading card a first paint shows before the slate lands, in place of a 184px frame
   that jumped to full height. Same shape as the other .gen-state cards. */
#loading-state .hud-card { text-align: center; }
#loading-state .hud-card h3 { color: var(--muted); font-weight: 500; }

/* ==========================================================================================
   THE TOOL IS THE PAGE, AND IT HAS EDGES (2026-09-16)

   The tool was asked to fill the page rather than sit inside one, and the left column was
   asked to read as sections with rules between them and between the table's columns.

   THE MISSING DIVIDERS WERE A BUG, NOT A STYLE CHOICE. site.css gives .gp-rail its right
   border and .gp-built its left border inside `@container (min-width: 700px)`. A container
   query needs an ancestor carrying `container-type`, and the only two in the codebase are
   .gen-preview and .macbook__bezel, both parts of the HOMEPAGE MOCK. The real tool has no
   container anywhere above it, so those two rules have never once applied here and the three
   columns have been running together with nothing between them. That is the mess. The real
   grid comes from the @media rule above instead, which is why the columns were laid out
   correctly while the borders silently were not.
   ========================================================================================== */

@media (min-width: 1100px) {
  /* ---- full page ----------------------------------------------------------------------
     --masthead is site.css's own token, already used as `calc(100svh - var(--masthead))` at
     its line 706, so the height is exact with nothing measured in JavaScript.
     svh, not vh: on a phone vh counts the space behind the address bar. It is moot at this
     breakpoint and it is the right unit to reach for anyway.

     The footer stays in flow below the tool. It carries the responsible gambling notice
     (1-800-GAMBLER, the 18+/21+ line), which is not something to delete for vertical space,
     so you reach it by scrolling past the workspace. */
  .page-generator .gen-tool { padding-top: 0; padding-bottom: 0; }
  .page-generator .gen-tool > .wrap { max-width: none; padding-left: 0; padding-right: 0; }

  .page-generator .gen-frame {
    margin-top: 0;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
    height: calc(100svh - var(--masthead));
    min-height: 34rem;
    display: flex;
    flex-direction: column;
  }
  .page-generator .gen-frame > .gen-frame__bar,
  .page-generator .gen-frame > .gen-frame__note { flex: none; }

  /* :not([hidden]) on both. The panels and the workspace are revealed by REMOVING the hidden
     attribute, and a bare `display: flex` here outranks `[hidden] { display: none }`, which
     would paint Build, Review and Entries on top of each other. */
  .page-generator .gen-panel:not([hidden]),
  .page-generator #workspace:not([hidden]) {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .page-generator .gen-frame .gp-grid { flex: 1; min-height: 0; }

  /* Three independent scrollers, so the page itself never moves while you work. */
  .page-generator .gen-frame .gp-rail,
  .page-generator .gen-frame .gp-pool,
  .page-generator .gen-frame .gp-built {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* ---- the partitions the container query never delivered -----------------------------
     THEY MOVED ONTO THE SPLITTERS ON 2026-09-17 and the borders came off. The splitter is a
     9px track sitting exactly where these two borders were, and it draws the same hairline
     down its middle (.gp-split::before). Leaving the borders on as well painted two rules
     with a 9px gutter between them, which reads as a seam rather than a partition. The rule
     is still there, in one place, and it is now the thing you grab. */
  .gen-frame .gp-rail { border-right: 0; border-bottom: 0; }
  .gen-frame .gp-built { border-left: 0; border-top: 0; }
}

/* ---- column rules on the player table ---------------------------------------------------
   Scoped to .gp-pool. Review's tables are built from the same .gp-table vocabulary and read
   as blocks of figures rather than a grid, so ruling them too would be noise, and the
   homepage mock uses .gp-table as well. */
.gen-frame .gp-pool .gp-table th,
.gen-frame .gp-pool .gp-table td { border-right: 1px solid var(--line-soft); }
.gen-frame .gp-pool .gp-table th:last-child,
.gen-frame .gp-pool .gp-table td:last-child { border-right: 0; }

/* ---- the rail reads as sections ---------------------------------------------------------
   The steps were divided by --line-soft, a 9% white that disappears against the panel. The
   gold-tinted --line is the rule this product draws everywhere else it means "these are
   different things". */
.gen-frame .gp-rail .gp-step { border-bottom: 1px solid var(--line); padding: var(--s4) 0; }
.gen-frame .gp-rail .gp-step:first-child { padding-top: 0; }

/* The one required field was 6.5rem wide holding a 9-character placeholder, so it shipped
   reading "How mai". The box is wider and the placeholder is an example rather than a
   restatement of the label two inches to its left. */
.gp-row--count input.gp-val { min-width: 7.5rem; }

@media (min-width: 1100px) {
  /* THE PRIMARY ACTION CANNOT BE BELOW THE FOLD OF ITS OWN COLUMN. Making the rail a scroller
     put Generate under it: the three steps plus the style cards are taller than the viewport,
     so a first-time subscriber saw every control and no button. It rides the bottom of the
     rail instead, and the LAST CHILD carries the clearance so Fine-tune and Saved profiles can
     still be scrolled clear of the bar rather than ending underneath it.

     The clearance is a margin on that child and NOT padding on the rail, which was the first
     attempt and put the bar 5.5rem up the column, floating over the style cards. A sticky
     element is constrained to its containing block, which is the parent's CONTENT box: adding
     bottom padding to the scroller moves that edge up and the bar can no longer reach the
     bottom of the scrollport it is supposed to be pinned to. */
  .page-generator .gen-frame .gp-rail > :last-child { margin-bottom: 4.5rem; }
  .page-generator .gen-frame .gp-rail .gen-cta-bar {
    position: sticky;
    bottom: 0;
    z-index: 2;
    margin: var(--s3) calc(var(--s4) * -1) 0;
    padding: var(--s3) var(--s4);
    background: var(--panel);
    border-top: 1px solid var(--line);
  }

  /* A FLEX CONTAINER BLOCKIFIES ITS CHILDREN. .gp-seg is display: inline-flex so it sizes to
     its two words; the moment the panel became a flex column it was stretched to the full
     width of the workspace and the sport switch read as an empty rule across the page. */
  .page-generator .gen-panel:not([hidden]) > .gp-seg { align-self: flex-start; }
}

@media (min-width: 1100px) {
  /* REVIEW AND ENTRIES NEED A SCROLLER OF THEIR OWN. Build's content is handled by the three
     columns, each of which scrolls. These two panels had nothing, and .gen-frame has carried
     `overflow: hidden` since it was written: harmless while the frame had no height, and a
     silent guillotine the moment this change gave it one.

     Measured at 1200x800 with 30 lineups before the fix: #panel-review's scrollHeight was
     2360 against a clientHeight of 543, its last block sat 523px below the frame, and walking
     up the ancestors found no scroll container at all. That is 1817px of exposure table, and
     the per-lineup checkboxes that drop a lineup from the upload, unreachable by any means. */
  .page-generator #panel-review:not([hidden]),
  .page-generator #panel-entries:not([hidden]) { overflow-y: auto; overscroll-behavior: contain; }

  /* ---- BEGINNERS IS TWO COLUMNS AT THIS WIDTH, LIKE ADVANCED ---------------------------
     #panel-beginners was in the rule above until 2026-09-21, which made the whole tab one
     scroller: the form, then the lineups a screen and a half below it. The owner asked for
     the shape Advanced already has, "options on the extreme left, lineups on the right,
     using the full space", and the panel now gets it from the same three ingredients the
     Advanced grid uses. Nothing is measured in JavaScript.

     1. The panel is already `flex: 1; min-height: 0` in a frame that is
        `calc(100svh - var(--masthead))` tall, so .easy taking `flex: 1` fills it exactly.
     2. `min-height: 0` on both tracks, because a grid item's default `min-height: auto`
        refuses to shrink below its content and the overflow would move to the page instead.
     3. One scroller per column, `overscroll-behavior: contain` so reaching the end of the
        lineups does not start scrolling the document behind them.

     The gap goes to zero and the rail takes a right border, so the seam is the same hairline
     the Advanced splitter draws rather than a second kind of divider on the same tool. */
  .page-generator .gen-easy { padding: 0; }
  .page-generator .gen-easy .easy {
    flex: 1;
    min-height: 0;
    /* 26rem, NOT 24, AND IT IS THE HEIGHT THAT DECIDES IT. The owner asked for "more room to
       those options on the left", which reads like a width, and widening does help the one
       control holding a long value (a slate name). But the reason it has to be 26 is
       vertical: measured at 1152x720 with the no-slate hint showing, 24rem wraps the lede,
       the note and two hints one line further and the rail runs 8px past the fold. 26rem is
       0px. Narrowing this again means finding those 8px somewhere else first.

       IT COSTS THE STAGE 32px, and at 1152 that is 16px off each of the two lineup cards, so
       a long surname ellipsises a little sooner. That is the trade, taken deliberately: the
       right column is the one he said will always scroll, and the full slate name is printed
       in the strip at the top of it either way. */
    grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
    max-width: none;
    gap: 0;
    margin: 0;
    align-items: stretch;
  }
  .page-generator .gen-easy .easy > .easy__card,
  .page-generator .gen-easy .easy > .easy__stage {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* The card stops being a card here and becomes the rail: the border, the radius and the
     inset highlight are what made it read as a panel floating in the middle of the page,
     which is the thing that is going away. */
  .page-generator .gen-easy .easy > .easy__card {
    border: 0;
    border-right: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
  }
  /* The stage stops being `display: contents` and becomes the right-hand column: its own
     grid, so the slate strip and the output space evenly whichever of them is showing.
     align-content: start, or two short cards stretch down a tall column. */
  .page-generator .gen-easy .easy > .easy__stage {
    display: grid;
    align-content: start;
    /* A token off each, 2026-09-21. This is the outer frame of everything on the right and
       it was spending 48px of the column's height on its own edges. */
    gap: var(--s4);
    min-width: 0;
    padding: var(--s4);
  }
  /* AND THE WAITING CARD COMES BACK, because now there IS a second column to explain. It is
     answered by the output's own hidden attribute through :has(), the same trick the frame
     note above uses, so there is exactly one record of whether a build has landed. A second
     copy of that in JavaScript is a second copy that can be wrong. */
  .page-generator .gen-easy .easy__await { display: block; }
  .page-generator .gen-easy .easy__stage:has(#easy-out:not([hidden])) .easy__await { display: none; }
  .page-generator .gen-easy .easy__more { margin-left: 0; margin-right: 0; text-align: left; }

  /* ---- THE BUTTON IS PINNED, because two columns alone did not put it on screen ---------
     Measured in Chromium at 1152x720, 1280x800 and 1440x900 after the two-column change: the
     rail's own content is 948px tall, so "Build my lineups" was below the fold on all three
     and the complaint that started this ("an easier way where they don't have to scroll
     down") was still true. Pinning it is what makes the claim honest.

     THE CLEARANCE IS ZERO PADDING ON THE SCROLLER, NOT BOTTOM PADDING. A sticky element is
     constrained to its containing block's CONTENT box, so padding-bottom on the rail lifts
     the bar off the bottom and floats it over the last step. The same trap is written up on
     .gp-rail in this file; this is the second element to meet it.

     The negative inline margins bleed the bar to the rail's edges so the hairline reads as a
     partition rather than a boxed-in strip, and the opaque background is what stops the steps
     showing through as they scroll under it. */
  .page-generator .gen-easy .easy > .easy__card { padding-bottom: 0; }
  .page-generator .gen-easy .easy__act {
    position: sticky;
    bottom: 0;
    margin: var(--s2) calc(var(--s5) * -1) 0;
    padding: var(--s2) var(--s5) var(--s3);
    background: var(--bg-lift);
    border-top: 1px solid var(--line);
  }
  /* THE PINNED BAR IS THREE ROWS, NOT FIVE. At full width the count field and the button
     were stacked, each 46px plus its own gap, and the bar came to 250px of a 530px rail:
     measured in Chromium at 1152x720, only two of the four steps were left on screen. The
     number is narrow by nature (three digits, capped at 150), so it takes a fixed column and
     the button takes what is left. 100px back, which is a third step. */
  .page-generator .gen-easy .easy__act > .easy__finish {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    gap: var(--s2) var(--s3);
    align-items: center;
    padding-inline: 0;
  }
  /* EVERY ROW IS EXPLICIT. Column-only placement is not enough here: auto-placement never
     goes backwards, so a full-width hint declared between the field and the button takes row
     3 and pushes the button to row 4, leaving the space beside the field empty. Rendered
     exactly that way on the first attempt. Naming the rows costs four more declarations and
     cannot be reordered into the wrong shape by an edit to the markup. */
  /* TWO ROWS, NOT THREE. The question and its range answer the same thing, so they share the
     top line: the label on the left and the range on the right, above the field and the
     button. That is one row back out of a bar that had four. */
  .page-generator .gen-easy .easy__act > .easy__finish { grid-template-columns: auto minmax(0, 1fr); }
  .page-generator .gen-easy .easy__act .easy__ask {
    grid-area: 1 / 1 / 2 / 2;
    margin: 0;
    font-size: 0.95rem;
  }
  .page-generator .gen-easy .easy__act .gp-hint {
    grid-area: 1 / 2 / 2 / 3;
    margin: 0;
    text-align: right;
  }
  .page-generator .gen-easy .easy__act .easy__count {
    grid-area: 2 / 1 / 3 / 2;
    width: 4.5rem;
    justify-self: start;
  }
  .page-generator .gen-easy .easy__act .easy__go { grid-area: 2 / 2 / 3 / 3; margin: 0; }
  .page-generator .gen-easy .easy__act .status { grid-area: 3 / 1 / 4 / -1; margin: 0; }
  /* 42px, not 46. Both are above the 24px minimum target size and comfortably above it; the
     four pixels are the difference between the last step being on screen and not. */
  .page-generator .gen-easy .easy__act > .easy__finish > .easy__count,
  .page-generator .gen-easy .easy__act > .easy__finish > .easy__go { height: 42px; }


  /* The last of it, all measured. The rail was still 82px over at 1152x720 after the copy
     and the bar came down, and these are the four places left that cost nothing to read. */
  .page-generator .gen-easy .easy > .easy__card { padding-top: var(--s4); }
  /* THE WORST STATE IS THE ONE THAT HAS TO FIT, not the tidy one. When a sport publishes
     nothing, a hint appears inside the steps box and the rail went 17px over at 720px tall.
     These two tokens are the last of the air, and they come off boxes rather than off
     anything anybody reads. */
  .page-generator .gen-easy .easy__steps { padding: var(--s1) var(--s3); }
  .page-generator .gen-easy .easy__more { margin-top: var(--s1); }
  /* THE LAST 28px, and it comes off the three selects rather than off anything anyone
     reads. They are min-height: 46px, which is the size the phone needs and more than a
     pointer does; 40px is still well clear of the 24px minimum target size, and this rule is
     inside a min-width: 1100px block so no touch layout is affected. Three selects, 18px. */
  .page-generator .gen-easy .easy__step:not(.easy__step--file) .gp-val { min-height: 40px; }
  .page-generator .gen-easy .easy__card > .easy__h { font-size: 1.05rem; margin-bottom: var(--s1); }
  .page-generator .gen-easy .easy__lede { margin-bottom: var(--s1); }

  /* ---- AND WHILE A BUILD IS RUNNING, WHICH IS THE STATE I MISSED ----------------------
     Measured before it was fixed: pressing Build unhides #easy-busy INSIDE the pinned bar,
     and the rail went 99px past the fold at 1152x720 and 51px at 1366x768. That is not an
     edge case, it is the box for every successful build on the tab this whole pass exists
     to fix, and my own browser check never clicked the button so it never saw it.

     Nothing is lost by collapsing the top line while it runs. The question "How many
     lineups?" and its range are answered: the number is in the field below and the build
     using it has already started, so neither can be acted on until it finishes. The field
     and the button stay, because the button is where focus is when the build begins and
     hiding a focused control drops focus to the body.

     The other half is the busy copy, which was three lines in a 26rem column and is one. */
  .page-generator .gen-easy .easy__act:has(#easy-busy:not([hidden])) .easy__ask,
  .page-generator .gen-easy .easy__act:has(#easy-busy:not([hidden])) .gp-hint { display: none; }

  /* THE STATUS LINE IS CLIPPED, NOT HIDDEN, and the difference is the whole point. It is the
     aria-live region: `display: none` takes it out of the accessibility tree and a screen
     reader stops being told the build started, which is the one reader who cannot see the
     bar sweeping. Clipped to a pixel it keeps announcing and costs no height. Its sentence
     is also already on screen for everyone else, in the busy block directly below it. */
  .page-generator .gen-easy .easy__act:has(#easy-busy:not([hidden])) .status {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* And the block itself wore 48px of margin, padding and border around 42px of content,
     which is right in a panel with room and wrong in a rail with none. The elapsed clock
     goes on the same line as the sentence: they are one fact, how long this has been
     running, split across two rows. The status line above it stays exactly as it is, because
     it is the aria-live one and the busy block is not. */
  .page-generator .gen-easy .build-busy {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    column-gap: var(--s3);
    margin-top: var(--s2);
    padding: var(--s2) var(--s3);
  }
  .page-generator .gen-easy .build-busy__bar { grid-column: 1 / -1; }
  .page-generator .gen-easy .build-busy__what { grid-column: 1; margin-top: var(--s2); }
  .page-generator .gen-easy .build-busy__clock { grid-column: 2; margin-top: 0; }

  /* ---- ONCE A FILE IS DROPPED, THE INSTRUCTION IS REPLACED BY THE OUTCOME --------------
     Two states still ran past the fold at 720px tall after the busy fix: a REJECTED file,
     whose note is two lines, at 17px, and an accepted file while a build runs, at 8px. Both
     are reachable and both were found by sweeping the panel's states rather than by looking
     at the tidy one.

     The hint under the zone says what dropping a file will do. The note says what dropping
     THIS file did. While the note is there the hint is answered, so it goes, which is worth
     more than the 16px: two sentences about the same file, one of them hypothetical, is the
     kind of thing a reader has to stop and reconcile. */
  .page-generator .gen-easy .easy__step--file:has(#easy-entries-note:not([hidden])) > .gp-hint {
    display: none;
  }
  .page-generator .gen-easy .easy__step--file > .gen-note { line-height: 1.35; }

  /* ---- THE SPACE ABOVE THE FIRST LINEUP ------------------------------------------------
     Owner, 2026-09-21, pointing at the gap: "This is still a lot of white space until
     lineups appear, please reduce these sizes so less space is occupied." Measured at
     1152x720 before this: 369px of stage before the first card.

     Where it went, and what each of these is:

       the output was a card inside a card      48px  two borders, two backgrounds and two
                                                      lots of padding around a list of cards.
                                                      The lineups ARE the cards. The output
                                                      keeps its heading and loses its box.
       the heading and the count                37px  "Your lineups are ready" and "20
                                                      lineups, built from today's numbers."
                                                      are one fact on two rows. One row now,
                                                      the confirmation left and the count
                                                      right, which is also where the eye
                                                      already goes for a figure.
       the list's own margin                    24px  the count line above it already carries
                                                      the gap.
       the stage, the strip and the banner      ~32px  a token off each.
  */
  .page-generator .gen-easy .easy__out {
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
  }
  .page-generator .gen-easy .easy__out > .easy__h {
    grid-area: h;
    margin: 0;
    font-size: 1.05rem;
  }
  .page-generator .gen-easy .easy__done { grid-area: count; margin: 0; text-align: right; }
  .page-generator .gen-easy .easy__out {
    display: grid;
    /* minmax(0, auto), NOT auto. The list spans both tracks, and a bare `auto` track will
       grow to that spanning item's min-content, which is a row of lineup cards: the cards
       ran off the right edge of the stage with their Replace buttons cut off. The floor of
       0 lets the track be sized by the heading, which is what it is for. */
    grid-template-columns: minmax(0, auto) minmax(0, 1fr);
    grid-template-areas:
      "h      count"
      "status status"
      "list   list";
    align-items: baseline;
    column-gap: var(--s4);
    row-gap: var(--s2);
  }
  .page-generator .gen-easy .easy__out > .lineup-status { grid-area: status; margin: 0; }
  .page-generator .gen-easy .easy__out > .easy__list { grid-area: list; margin: 0; }
  /* The strip's own edges. Its height is set by the actions column, not the slate, so
     every token here is a token off the run above the first lineup. */
  .page-generator .gen-easy .easy__intel { padding: var(--s3) var(--s4); }
  .page-generator .gen-easy .lineup-status { padding: var(--s1) var(--s3); }

  /* AND THE TOP, which the owner named in the same breath. The bar carries the page heading
     and four tabs, 65px of a 720px window before the tool starts. Less air, same two rows. */
  .page-generator .gen-frame__bar { padding: var(--s2) var(--s4); }
  .page-generator .gen-easy .easy__act .build-busy { margin-top: var(--s3); }

  /* The download is one button, not a bar across a 1056px column. It reads as a control at
     22rem and as a banner at full width, and the thing it is next to is a list of cards. */
  .page-generator .gen-easy .easy__stage .easy__go { max-width: 22rem; }

  /* ---- THE LEFT RAIL FITS, WITH NOTHING TO SCROLL ---------------------------------------
     The owner, 2026-09-21, after using the two-column build: the right column will always
     scroll because of how much it shows, and the left should not scroll at all. Measured at
     1152x720 and 1280x720 before this pass: 530px of rail holding 847px of content, so 317px
     over, and the same rail is 269px over on a 1366x768 laptop.

     Where the 317 came from, measured rather than guessed:

       the two blocks about the Advanced tab   168px   they said the same thing twice
       the lede                                 76px   three lines of reassurance
       the pinned bar                          207px   a label row, a field row, a two-line
                                                       hint and two tokens of padding
       the four steps                          362px

     The copy half is in generator.html. This is the rest: every token of padding inside a
     384px column is a token off the last step, and the pinned bar is the one block a reader
     looks at once and then never again, so it is the one that gives up the most. */
  .page-generator .gen-easy .easy__steps { gap: var(--s1); padding: var(--s3); margin-bottom: var(--s2); }
  .page-generator .gen-easy .easy__lede { margin-bottom: var(--s3); }
  .page-generator .gen-easy .easy__card > .easy__h { font-size: 1.1rem; }
  /* The entries zone is the tallest control in the rail and its text is one line's worth. */
  .page-generator .gen-easy .easy__step .entries-drop { padding: var(--s2) var(--s3); }
  .page-generator .gen-easy .easy__step .gp-hint { line-height: 1.35; }
  /* The label column was 6.5rem, which left the slate name truncating in the control beside
     it. CONTEST TYPE is the longest of the three and it fits 5.5rem, so the select gets the
     rest: the owner's own screenshot shows "DK MLB Main 2026-" cut off. */
  /* The three labels are SPORT, CONTEST TYPE and SLATE, and the middle one sets the column.
     A hair smaller lets it hold one line, and those px go to the control beside it, where
     the slate name is. The width itself is set on the real rule further down this block: an
     earlier attempt declared a second rule with the same selector and the same specificity
     UP HERE, which loses the cascade to the one below and did nothing at all while carrying
     a comment claiming it did. Caught by reading the computed value in a browser. */
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .gp-kicker { font-size: 0.62rem; }
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .gp-val { padding-inline: 10px 30px; }

  /* ---- THE THREE CHOOSERS GO ON ONE LINE EACH -----------------------------------------
     Stacked, each of them is a label row and a control row, about 86px, and four of those
     plus the heading do not fit a 530px rail: measured at 1152x720 with the bar pinned, two
     of the four steps were on screen. Side by side they are ~50px each and all four are,
     which is the difference between a form you read and a form you hunt through.

     NOT THE FILE STEP. Its control is a drop zone two lines tall with a hint under it, so
     the label belongs above it rather than beside it, and squeezing it into a 6.5rem gutter
     would be the one place this shape does not fit. */
  .page-generator .gen-easy .easy__step:not(.easy__step--file) {
    /* 5rem, was 6.5rem. See the note above: the label is smaller now, so the column can be
       too, and the difference goes to the select that holds the slate name. */
    grid-template-columns: 26px 5rem minmax(0, 1fr);
    column-gap: var(--s2);
  }
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .easy__step-n { grid-row: 1; }
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .gp-kicker { grid-column: 2; }
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .gp-val { grid-column: 3; }
  .page-generator .gen-easy .easy__step:not(.easy__step--file) > .gp-hint { grid-column: 2 / -1; }

  /* THE SLATE READS ACROSS THE TOP OF THE STAGE, not down its side. In the centred layout
     the intel was a tall card beside a tall form. At the head of a wide column the same four
     facts are a strip: what you are building on at the left, the three figures at the right,
     with the rule between them doing the work the card's border used to. */
  .page-generator .gen-easy .easy__intel {
    display: grid;
    /* TWO COLUMNS, NOT THREE. What you are building on, and what to do with the file.
       Three was the first attempt and it does not fit: at 1152 the stage is 736px, the three
       tracks want about 800, and the one that gave way was the slate name, which came out
       over three lines with "WHAT YOU ARE / BUILDING ON" above it. The figures belong on the
       left anyway. They ARE what you are building on, so they read as a row under the lock
       rather than as a separate column beside it.

       The actions track is `auto` and its content ships hidden, so before a build it
       collapses to nothing and the strip is exactly what it was. */
    /* FOUR ROWS, AND THE LOCK AND THE FIGURES DO NOT SHARE ONE. Sharing was tried and it
       does not fit: at 1152 the left column is 416px and the lock plus the figures want 418,
       so "SALARY CAP" ran under the upload link. It was measurable only once each label's
       OWN rect was read, because the label is nowrap and was hanging out of a cell sized to
       "$50,000" while every container rect said the two were clear.

       The alternative was shortening a label to make it fit, which is how a strip ends up
       saying CAP at somebody about to spend money. The row costs 24px. */
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "kicker actions"
      "slate  actions"
      "lock   actions"
      "facts  actions";
    align-items: start;
    column-gap: var(--s4);
  }
  .page-generator .gen-easy .easy__facts {
    grid-area: facts;
    margin-top: var(--s2);
    padding-top: var(--s2);
    border-top: 1px solid var(--line);
    /* START, not end. Justified to the end they overflowed their own track leftwards when it
       was narrower than they are, and "GAMES 2" landed on top of "Locks ... PDT". Started
       from the left they simply take the room after the gap and the rule. */
    justify-content: flex-start;
    gap: var(--s5);
    min-width: 0;
    padding-left: 0;
    border-left: 0;
  }
  /* SALARY CAP came out over two lines the moment these shared a row with the lock. A label
     that wraps reads as two labels. */
  .page-generator .gen-easy .easy__fact dt { white-space: nowrap; }
  /* AND THE BOX HAS TO BE AS WIDE AS THE LABEL. With the label set to nowrap and the row
     allowed to shrink, "SALARY CAP" simply overflowed its own cell and landed on the upload
     link, while every rect I measured said the two were clear: the cell's box was the width
     of "$50,000" and the label hung out of it. flex: none is what makes the box tell the
     truth about its content. */
  .page-generator .gen-easy .easy__fact { flex: none; }
  .page-generator .gen-easy .easy__actions {
    grid-area: actions;
    align-self: center;
    display: grid;
    gap: var(--s1);
    /* 17rem, measured rather than guessed twice. "Download the DraftKings file" is the
       longest label this button takes: it needs 217px and gets 225 here, where 14rem gave it
       184 and 16rem gave it 209, and both wrapped it onto two lines. The left column has the
       slack now that the figures sit under the slate rather than beside it. */
    width: 17rem;
    margin: 0;
    padding: 0 0 0 var(--s4);
    border: 0;
    border-left: 1px solid var(--line);
  }
  .page-generator .gen-easy .easy__actions > .gp-cta { width: 100%; min-height: 42px; }
  .page-generator .gen-easy .easy__actions > .gen-note { margin: 0; font-size: 0.72rem; line-height: 1.35; }
  .page-generator .gen-easy .easy__intel > .gp-kicker { grid-area: kicker; }
  .page-generator .gen-easy .easy__slate { grid-area: slate; }
  .page-generator .gen-easy .easy__lock { grid-area: lock; margin-bottom: 0; }
  .page-generator .gen-easy .easy__facts {
    grid-area: facts;
    display: flex;
    gap: var(--s5);
    margin: 0;
    padding: 0 0 0 var(--s5);
    border-top: 0;
    border-left: 1px solid var(--line);
  }
  .page-generator .gen-easy .easy__fact {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    margin: 0;
  }

  /* ONE SCROLLBAR PER PANEL. .review-grid .gen-scroll caps its tables at 40rem and scrolls
     them, which was right when the page itself scrolled and the cap was the only thing
     stopping a table running off the bottom. Now the panel scrolls, so the cap produced two
     stacked scrollbars: measured at 1440x900 with 30 lineups, #panel-review at 2360/643 and
     .gen-scroll at 1561/640, both live at once. The panel is the scroller; the tables are
     just tables. */
  .page-generator #panel-review .gen-scroll { max-height: none; overflow-y: visible; }

  /* The two compliance lines are direct children of the frame, after the panels, so in a flex
     column they are flex items like any other. Without this they are shrinkable, and they are
     the last text on the page that may quietly get smaller. */
  .page-generator .gen-frame > .gp-foot { flex: none; }
}


/* ==========================================================================================
   THE BEGINNERS TAB

   The Build rail is three steps, a drawer of eight dials and a table of 132 players. This is
   one slate, one number and one button, centred, with nothing else on it. Somebody who has
   never played daily fantasy should be able to finish without learning a single term.
   ========================================================================================== */
.gen-easy { padding: var(--s5) var(--s4) var(--s6); }
/* TWO PANELS, THEN THE RESULTS ACROSS BOTH. The agreed reference layout for this flow puts
   the Basic tab out this way: the steps on the left, what you are building on beside them, and
   the lineups full width underneath. It was one 34rem column, which put the slate name and the
   lock in the MIDDLE of the form and left the built lineups in a strip narrow enough that three
   cards wrapped their own headers. */
/* ONE COLUMN IS THE BASE AND THE TWO-COLUMN TOOL IS THE OVERRIDE, at 1100px. Written this
   way round because the narrow layout is not the wide one squeezed: below 1100 the three
   blocks run in an order the wide layout cannot express, slate first because it is what you
   check before you start, then the form, then the lineups last because they do not exist
   yet. Above it the slate and the lineups share the right-hand column. */
.easy {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: var(--s5);
  max-width: 34rem;
  margin: 0 auto;
}
/* display: contents, so the stage's children take their own places in that single column
   instead of travelling together as one block. Without it the finished lineups ride ABOVE
   the form, because the stage has to sit either before the form or after it and the slate
   inside it has to be before. Seen on a phone, not reasoned about: "Your lineups are ready"
   and three full cards came before "Build your lineups". */
.easy__stage { display: contents; }
.easy__intel { order: -1; }
.easy__card { order: 0; }
.easy__out { order: 1; }
/* Below the two-column tool the strip is one column, so the actions follow the figures
   rather than sitting beside them. Their own block, with a rule above, so the download does
   not read as part of the salary cap. */
.easy__actions {
  display: grid;
  gap: var(--s2);
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}
.easy__actions[hidden] { display: none; }
.easy__actions > .gen-note { margin: 0; }

/* The waiting card is a signpost to a column that does not exist at this width, and the
   output lands directly under the button here, where it is already obvious. */
.easy__await { display: none; }
.easy__await {
  padding: var(--s6) var(--s5);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
}
.easy__await-h {
  margin: 0 0 var(--s2);
  font-family: var(--ui);
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--text);
}
.easy__await-d { margin: 0 auto; max-width: 42ch; color: var(--muted); }

.easy__intel {
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
}
.easy__intel .gp-kicker { margin-top: 0; }
.easy__facts {
  margin: var(--s4) 0 0;
  padding: var(--s4) 0 0;
  border-top: 1px solid var(--line);
}
.easy__fact {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s2);
}
.easy__fact:last-child { margin-bottom: 0; }
/* `.easy__fact { display: flex }` outranks the user agent's `[hidden] { display: none }`,
   so a fact the slate does not carry was painting its label beside an empty value. The
   markup's own comment says a row with nothing behind it is hidden rather than printed as
   a dash, which is what this makes true. */
.easy__fact[hidden] { display: none; }
.easy__fact dt {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.easy__fact dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--brass);
}
.easy__card,
.easy__out {
  padding: var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-lift);
  box-shadow: inset 0 1px 0 var(--edge-hi);
}
.easy__h {
  margin: 0 0 var(--s2);
  font-family: var(--ui);
  font-size: var(--t-lead);
  font-weight: 600;
  color: var(--text);
}
.easy__lede { margin: 0 0 var(--s4); color: var(--muted); }
.gen-easy .gp-kicker { margin-bottom: 4px; }
.easy__slate { margin: 0; font-size: 1.05rem; font-weight: 600; color: var(--text); }
.easy__lock { margin: 2px 0 var(--s5); color: var(--muted); font-variant-numeric: tabular-nums; }

.easy__ask {
  display: block;
  margin-bottom: var(--s2);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text);
}
input.easy__count {
  width: 100%;
  padding: 10px 14px;
  font-size: 1.35rem;
  text-align: left;
}
.gen-easy .gp-hint { display: block; margin: 6px 0 var(--s5); }

button.easy__go {
  width: 100%;
  min-height: 52px;
  font-size: 1rem;
}
.gen-easy .status { margin-top: var(--s3); }

.easy__done { margin: 0 0 var(--s4); color: var(--muted); }
.easy__list {
  display: grid;
  /* minmax(0, 1fr), NOT the implicit `auto` this had. An auto track will not size below its
     content, and a lineup card's content does not shrink willingly: the header's two labels
     are `white-space: nowrap` and the roster row carries a caret, a slot label, a face and a
     Replace button before the name gets any room. Measured on a phone at 390px: the list was
     314px wide and the card inside it 372px, so the salary read "$50,0" and the button read
     "REPLA", both cut off at the card's own edge. The floor of 0 is what lets the track hold
     the card to the width it was given, and the name's own `min-width: 0` does the rest. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s3);
  margin-top: var(--s5);
}
/* Wider than one card on a roomy screen, because twenty lineups in a single column is a long
   scroll for something the page has just told you is ready.

   17rem, not the 15rem it was. The old floor was chosen against a 52rem centred column, where
   it bought a third card; against the full-width stage it buys a sixth one, and a DraftKings
   classic row (position, headshot, name, a Replace button) truncates the surname at that
   width. Measured at 1152px: "Christian McCaffrey" rendered as "Christ...". */
@media (min-width: 1100px) {
  .easy__list { grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); }
}

.easy__more {
  max-width: 34rem;
  margin: var(--s5) auto 0;
  color: var(--muted);
  text-align: center;
}
button.easy__link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--brass);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
button.easy__link:hover { color: var(--gold-bright); }

@media (max-width: 640px) {
  .gen-easy { padding: var(--s4) var(--s3) var(--s5); }
  .easy__card,
  .easy__out { padding: var(--s4); }
}

/* The frame's lede explains what the Review and Entries tabs are for. On Beginners that is a
   sentence about two tabs the reader has not met, above a card whose whole claim is that there
   is nothing to learn. :has(), so the tab state stays in one place (the hidden attribute) and
   no second copy of "which tab is showing" has to be kept in JavaScript. */
.gen-frame:has(#panel-beginners:not([hidden])) > .gen-frame__note { display: none; }

/* ---- the Beginners panel's three steps ------------------------------------------------
   Sport, then contest type, then slate, in the order a person decides. Numbered because the
   review's complaint was that the panel never ASKED: a numbered step reads as a
   question, an unlabelled select reads as a setting somebody else already chose.

   Every value here is a token. The page allows exactly one radius, var(--radius), or none,
   which is why the step number is a square with the shared radius rather than the circle it
   was first written as: a bespoke 50% would have been a second radius on a page that has one.
   Caught by generator-three-steps.test.mjs, which also refuses raw hex. */
.easy__steps {
  display: grid;
  gap: var(--s3);
  margin: var(--s4) 0 var(--s5);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
}
.easy__step {
  display: grid;
  grid-template-columns: 26px 1fr;
  align-items: center;
  gap: var(--s1) var(--s3);
  margin: 0;
}
.easy__step-n {
  display: grid;
  width: 26px;
  height: 26px;
  place-items: center;
  grid-row: span 2;
  color: var(--brass);
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
}
.easy__step .gp-kicker { grid-column: 2; margin: 0; }
.easy__step .gp-val { grid-column: 2; width: 100%; }
.easy__step .gp-hint { grid-column: 2; margin: 0; }
/* A select with one option is not a choice, so it reads as a statement rather than a control. */
.easy__step .gp-val:disabled { opacity: 1; cursor: default; }

/* Step 4 is the optional entries file. The zone, its hint and its answer all sit in the same
   column as the selects above, so the four steps read as one list and not as a form with a
   file picker bolted under it. */
.easy__step--file { align-items: start; }
.easy__step .entries-drop { grid-column: 2; margin: 0; }
.easy__step .gen-note { grid-column: 2; margin: 0; }
.easy__opt {
  margin-left: var(--s1);
  color: var(--muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

@media (max-width: 560px) {
  .easy__steps { padding: var(--s3); }
}

/* THE BUILD WAIT. Indeterminate by design: one POST goes out and nothing reports back until it
   answers, so a bar with a position would be inventing one. The bar says work is running and
   the clock says how long, both of which are true. Tokens only, no raw hex and one radius. */
.build-busy {
  margin: var(--s4) 0 0;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
}
.build-busy__bar {
  overflow: hidden;
  height: 4px;
  border-radius: var(--radius);
  background: var(--line);
}
.build-busy__bar > span {
  display: block;
  width: 40%;
  height: 100%;
  border-radius: var(--radius);
  background: var(--brass);
  animation: build-busy-sweep 1.5s ease-in-out infinite;
}
.build-busy__what {
  margin: var(--s3) 0 0;
  font-family: var(--ui);
  font-size: 0.9rem;
}
.build-busy__clock {
  margin: var(--s1) 0 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--muted);
}
@keyframes build-busy-sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
/* A sweeping bar is exactly the kind of movement this setting is about, so it stops rather than
   slowing down. The card still says what is happening and the clock still counts, so nothing
   this panel tells anybody depends on the animation. */
@media (prefers-reduced-motion: reduce) {
  .build-busy__bar > span { width: 100%; animation: none; }
}

/* WHY THIS LINEUP. Two or three lines under a lineup card, in the plainest thing on the page:
   the beginner reading them has just met the word "projection" and does not need a second
   typeface to decode. Muted, so the roster above stays the thing the eye lands on first. */
.gp-why {
  margin: var(--s2) 0 0;
  padding: var(--s2) 0 0;
  border-top: 1px solid var(--line);
  list-style: none;
}
.gp-why li {
  margin: 0 0 var(--s1);
  font-family: var(--ui);
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--muted);
}
.gp-why li:last-child { margin-bottom: 0; }

/* THE CARD HEADER WRAPS BETWEEN ITS TWO PARTS, NOT INSIDE THEM. "Total salary $49,800 /
   $50,000" is a much longer label than the bare figure it replaced, and in a three-across grid
   the flex row squeezed both sides until "LINEUP 1" broke across two lines and the figure broke
   after the slash. Neither part breaks now; the row does, and the salary drops onto its own
   line when there is no room beside the name. Seen in a browser at 1280, not reasoned about. */
.gp-lineup header { flex-wrap: wrap; gap: 0 var(--s3); }
.gp-lineup header > span { white-space: nowrap; }

/* REPLACE A PLAYER. The button sits at the end of the roster row, quiet until the row is
   hovered or the button is focused, because twenty cards of nine rows each is a hundred and
   eighty buttons and they must not compete with the names. Always reachable by keyboard. */
.gp-swap {
  margin-left: auto;
  padding: 2px var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.55;
  transition: opacity 120ms ease, color 120ms ease, border-color 120ms ease;
}
.gp-lineup li:hover .gp-swap,
.gp-swap:focus-visible,
.gp-swap[aria-expanded="true"] {
  opacity: 1;
  color: var(--brass);
  border-color: var(--brass);
}
/* Never hidden on a touch screen, where there is no hover to reveal it. */
@media (hover: none) {
  .gp-swap { opacity: 1; }
}

.gp-swaps-row { display: block; padding: 0 !important; }
.gp-swaps {
  margin: var(--s1) 0 var(--s2);
  padding: var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
}
.gp-swaps__row {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  width: 100%;
  padding: 4px var(--s2);
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.gp-swaps__row:hover, .gp-swaps__row:focus-visible { background: var(--line); }
.gp-swaps__who { flex: 1 1 auto; font-family: var(--ui); font-size: 0.8rem; }
.gp-swaps__num {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--muted);
  white-space: nowrap;
}
.gp-swaps__none {
  margin: 0;
  padding: 4px var(--s2);
  font-family: var(--ui);
  font-size: 0.76rem;
  color: var(--muted);
}

/* THE CONTROLS HAVE TO LOOK LIKE CONTROLS. Review asked for the drop downs to be easier to
   see, with the profile picker as the example. The shared field hairline is
   rgba(255,255,255,.16) on a near-black panel, which is almost nothing, so a select read as a
   line of text with a word on it. --muted is a real mid-tone in BOTH themes (it is the
   secondary text colour), so the edge is visible on black and on parchment without a second
   token and without a hex. The brass hover and focus states above still win over it. */
.gp-val,
select.gp-val,
input.gp-val,
textarea.gp-val {
  border-color: var(--muted);
}
/* And the caret is bigger, because a 5px arrow is not a signpost. */
select.gp-val--wide {
  background-size: 6px 6px, 6px 6px;
  background-position: calc(100% - 14px) 52%, calc(100% - 8px) 52%;
}

/* The drop zone gets the same treatment as the selects, for the same reason: it is a control
   and the shared hairline made it read as a paragraph with a box drawn faintly round it. */
.entries-drop { border-color: var(--muted); }

/* ---- LAYOUT PASS, 2026-09-18 ----------------------------------------------
   Four things found by looking at the shipped page rather than at the markup,
   which is the only way any of them show up.
   -------------------------------------------------------------------------- */

/* 1 · THE HEADER BAR SPREADS, instead of bunching left and leaving the right
   two thirds empty. The bar carries the title, the tabs, the trust line, the
   slate and the lock time, and they were laid out as a row that simply ran out
   of content. Spread the groups, and let the tabs spread inside their own
   share, so the whole width is used and every label has room. */
/* The bar spread and the frame's brass top edge BOTH LIVE IN THEIR OWN BLOCKS
   at the top of this file now, folded there on 2026-09-18 with the tab restyle.
   Three declarations for .gen-frame__bar, .gen-frame__bar .gp-tabs and
   .gen-frame were here, 1700 lines below the canonical blocks and winning on
   source order alone, which is what let a later tab edit be silently
   overridden. Do not reintroduce a second block for any of them. */

/* 2 · THE SPORT AND CONTEST-TYPE SWITCHES CENTRE, and the dead space above them
   goes. They sat hard left under a note that ends two thirds of the way across,
   which read as the panel having lost its left column. */
.gen-frame .gen-sports,
.gen-frame .gen-styles { margin: 0; }
.gen-frame #panel-build > .gp-seg:first-of-type { margin-top: var(--s3); }
.gen-switches {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: var(--s3);
  /* Tight. The note above ends two thirds across and this row is two small chips, so generous
     padding on both left a band of nothing between the header and the first control. */
  padding: var(--s2) var(--s4) var(--s3);
}
.gen-switches:empty { display: none; }

/* 3 · ONE WIDTH FOR EVERY CONTROL IN THE RAIL. Generate was full width, 48px
   tall and uppercase while the controls above it were inline chips of four
   different widths, so the button read as belonging to a different page. The
   rail's own controls now share a width and the button comes down to meet them
   rather than the other way round: it is still the primary action, it just is
   not twice the size of everything it sits under. */
.gen-frame .gp-rail .gp-step > .gp-seg,
.gen-frame .gp-rail .gp-step > select,
.gen-frame .gp-rail .gp-step > .gp-drop { width: 100%; }
.gen-frame .gp-rail .gen-cta-bar button.gp-cta--big {
  min-height: 42px;
  font-size: 0.85rem;
  letter-spacing: 0.08em;
}
@media (max-width: 720px) {
  .gen-frame .gp-rail .gen-cta-bar button.gp-cta--big { min-height: 46px; }
}

/* 4 · THE BEGINNERS PANEL'S CONTROLS ARE ONE SIZE. Three selects, a number and a
   button, all full width of their column and all the same height, so the eye
   reads one column of steps instead of five differently shaped things. */
.easy select,
.easy input[type="number"],
.easy input[type="text"],
.easy .gp-cta,
.easy .btn {
  width: 100%;
  min-height: 46px;
  box-sizing: border-box;
}

/* 5 · THE ENTRIES TABLE GETS COLUMN LINES. Six columns of short values with no
   vertical rule is a grid the eye has to reconstruct, and the last two are the
   ones somebody types into. The rule is the same hairline the panel borders
   use, and it stops before the last column so the table does not read as a box
   inside a box. */
#contest-table th:not(:last-child),
#contest-table td:not(:last-child) { border-right: 1px solid var(--line); }
#contest-table th,
#contest-table td { padding-inline: var(--s3); }

/* The how-to that now sits under the table rather than over it. */
.entries-howto {
  padding: 0 var(--s4) var(--s4);
  border-top: 1px solid var(--line);
  margin-top: var(--s4);
  padding-top: var(--s4);
  max-width: 78ch;
}
.gen-panel__head--tight { padding-bottom: 0; }

/* Both switches ship hidden and generator.js reveals them, so the wrapper has to collapse
   when neither is showing or it leaves exactly the band of dead space this pass removed.
   :empty cannot see it: the children exist, they are just [hidden]. */
.gen-switches:not(:has(> :not([hidden]))) { display: none; }

/* The standing note under the bar was padded like a section when it is one sentence of
   orientation. Half the vertical room, same text. */
.gen-frame .gen-frame__note { padding-block: var(--s2); }

/* THE COUNT AND THE BUTTON LINE UP WITH THE SELECTS ABOVE THEM. Measured in a browser rather
   than guessed: the three step controls render 375px wide at x=414, and the count field and
   the build button rendered 447px wide at x=359, so the column visibly stepped out at exactly
   the point somebody is about to press the thing.

   PADDING ON A WRAPPER, NOT A WIDTH ON THE CONTROLS. width:auto on an input sizes to its
   content, not to its container, so it collapsed to 132px; a width:calc() would have to
   re-derive the step box's border, padding, marker and gap and would drift the day any of
   them moved. Padding the wrapper with the SAME tokens the step grid uses makes width:100%
   land exactly on the select column, with no arithmetic to keep in step. */
.easy__finish {
  padding-left: calc(1px + var(--s4) + 26px + var(--s3));
  padding-right: calc(1px + var(--s4));
}
.easy__finish > .easy__count,
.easy__finish > .easy__go {
  display: block;
  width: 100%;
  height: 46px;
  padding-block: 0;
  box-sizing: border-box;
}
@media (max-width: 900px) {
  /* On a phone the marker column is the difference between a usable field and a cramped one. */
  .easy__finish { padding-inline: 0; }
}

/* THE PRESET CARDS ARE ONE SIZE. They were a wrapping row sized by their own labels, so
   "Balanced", "Bold", "Steady", "Spread Out" and "Your Own Dials" came out five different
   widths and the rail read as five unrelated controls rather than one choice with five
   answers. A two-column grid gives every card the same box and lets the longest label set
   the height for all of them, which is the point: they are alternatives, not a list. */
.gen-frame .gp-rail .opt-rail {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  gap: 6px;
}
.gen-frame .gp-rail .opt-rail > button.gp-chip {
  width: 100%;
  box-sizing: border-box;
}



/* ---- lineup status -------------------------------------------------------
   Whether today's lineups are out. Sits above the lineups because it decides
   whether they are worth entering.

   THE STATE IS IN THE WORDS FIRST AND THE COLOUR SECOND. A beginner should be
   able to read this with the colour ignored, or not seen at all, so the left
   accent reinforces the headline rather than carrying it. There is no green in
   this palette, so brass is the positive: gold reads as done on this brand,
   grey as incomplete, and danger as not ready. */
.lineup-status {
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line-soft);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius);
  background: var(--bg-lift);
}
/* ---- THE HEADLINE IS THE CONTROL, AND THE (i) IS DRAWN, NOT TYPED ------------------------
   The detail sits behind a native <details> so it needs no JavaScript and is reachable by
   keyboard. The summary IS the headline, so one line is all this costs when closed.

   THE GLYPH IS AN ::after, deliberately. paintLineupStatus writes the headline with
   textContent, which replaces every child, so an (i) written as markup inside the summary
   would be wiped the first time the banner painted. A pseudo-element cannot be. */
.lineup-status__h {
  margin: 0;
  font-weight: 650;
  letter-spacing: 0.01em;
  color: var(--text);
}
summary.lineup-status__h {
  display: flex;
  align-items: center;
  gap: var(--s2);
  cursor: pointer;
  list-style: none;
}
summary.lineup-status__h::-webkit-details-marker { display: none; }
summary.lineup-status__h::after {
  content: "i";
  flex: none;
  display: grid;
  place-items: center;
  width: 17px;
  height: 17px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--field-bg);
  color: var(--muted);
  font-family: var(--ui);
  font-size: 11px;
  font-style: italic;
  font-weight: 600;
  line-height: 1;
}
summary.lineup-status__h:hover::after,
.lineup-status__more[open] > summary.lineup-status__h::after {
  border-color: var(--brass);
  color: var(--brass);
}
/* ---- THE GAMES, INSIDE THE DISCLOSURE ----------------------------------------------------
   A row per fixture, each carrying a mark AND a word. The mark is drawn rather than typed:
   a tick from two borders, the same construction the homepage's .ease list uses, so no
   glyph, no icon font and nothing that renders as an emoji on one platform and a box on
   another.

   --good is the first green in this palette and it is never the only signal here. Its
   contrast against brass is 1.21:1, which is luminance rather than hue, so to a reader with
   red-green colour blindness this green and the page's gold are close to the same colour.
   The word beside it is what carries the state; the colour agrees with it. */
/* WRAPPING FLEX, NOT A GRID OF EQUAL CELLS. As a grid the rows were stretched to a shared
   track width and the state ended up at the far right of each cell, a hand's width from the
   fixture it described: "ATL at GB" and then a gap and then NOT SET YET. Each row is its own
   tight group now and the rows pack across and wrap, which is how a list of short facts
   wants to sit. */
.lineup-status__games {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s5);
  margin: var(--s3) 0 0;
  padding: 0;
  list-style: none;
}
.lineup-status__games[hidden] { display: none; }
.lineup-status__game {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 0.86rem;
}
.lineup-status__teams {
  white-space: nowrap;
  color: var(--text);
}
.lineup-status__word {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.lineup-status__mark {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: var(--radius);
}
/* SETTLED: a tick, two borders and a rotation. */
.lineup-status__game[data-state="ready"] .lineup-status__mark::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 0;
  width: 5px;
  height: 10px;
  border: solid var(--good);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.lineup-status__game[data-state="ready"] .lineup-status__word { color: var(--good); }
/* WAITING: a ring, open, because the thing it is about has not happened yet. */
/* The house allows one radius token and no other, so this is a square ring rather than the
   circle it was first drawn as. Two tests refuse a second radius and both are right: a page
   with two corner radii reads as two designs. */
.lineup-status__game[data-state="waiting"] .lineup-status__mark {
  border: 1px solid var(--warn);
}
.lineup-status__game[data-state="waiting"] .lineup-status__word { color: var(--warn); }
/* NO WORD EITHER WAY: a bar, which is neither a promise nor a warning. */
.lineup-status__game[data-state="unknown"] .lineup-status__mark::after {
  content: "";
  position: absolute;
  left: 2px;
  top: 6px;
  width: 10px;
  height: 1px;
  background: var(--line);
}

summary.lineup-status__h:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 3px;
  border-radius: var(--radius);
}
.lineup-status__d {
  margin: var(--s2) 0 0;
  color: var(--muted);
  font-size: 0.94em;
  line-height: 1.5;
}
.lineup-status--confirmed { border-left-color: var(--brass); }
.lineup-status--confirmed .lineup-status__h { color: var(--brass-bright); }
.lineup-status--waiting { border-left-color: var(--danger); }
.lineup-status--waiting .lineup-status__h { color: var(--danger); }
/* Partial keeps the neutral accent: it is genuinely in between, and colouring it
   like a failure would train people to ignore the one that is a failure. */
