 /* Self-hosted (see the /fonts mount above) - a variable-weight woff2, so
    one file covers every font-weight:400/500/700 use on this page rather
    than needing three separate static instances. */
 @font-face{font-family:'Roboto';font-style:normal;font-weight:100 900;
   font-display:swap;src:url(/fonts/roboto-variable.woff2) format('woff2')}
 /* CSS variables for the main surfaces (page background/text, table
    borders, zebra stripe, the jump-drawer's background) PLUS the accent/
    heading/muted/tint families below, added for Dark Mode - every rule
    that used to hardcode one of these hex values now points at the var
    instead, with the SAME default, so light mode renders byte-identical
    to before. The Settings > Dark mode checkbox already existed and
    already added body.dark (see the `dark` entry in the DISP table
    further down) - this was the missing CSS half. Status/flag badges are
    deliberately untouched here: they're small self-contained chips (their
    own background paired with their own text colour, e.g. always-black
    text on a light tint - explicit earlier user decision), so their
    contrast never depended on the page background and doesn't need to
    change when it does. */
 :root{--page-bg:#fff;--page-text:#1a1a1a;--border:#ddd;--stripe:#ececec;
   --panel-bg:#fff;--brand:#0f2c52;
   --heading:var(--brand);--accent:#2b6cb0;--accent-2:#3a6ea5;
   --muted:#666;--muted-2:#888;--input-bg:#fff;
   --tint-blue:#eef2f7;--tint-yellow:#FDFCEB;--tint-red:#fff6f6;
   --tint-green:#f4f8f4;--amber:#8a6d00;--success:#137333}
 body.dark{
   --page-bg:#15181d;--page-text:#e4e6eb;--border:#333a44;
   --stripe:#1e2229;--panel-bg:#1c2027;
   --heading:#8ab4e8;--accent:#5b9bd5;--accent-2:#6fa8dc;
   --muted:#a0a8b4;--muted-2:#8890a0;--input-bg:#20242c;
   --tint-blue:#1f2b3d;--tint-yellow:#3a341a;--tint-red:#3a2222;
   --amber:#e0b84d;--success:#4caf72;
   --tint-green:#182a1c}
 body{font-family:'Roboto',system-ui,sans-serif;margin:0;padding:1rem 0 3rem;
   background:var(--page-bg);color:var(--page-text)}
 /* Backstop against whole-page sideways panning on phones (explicit user
    report: a table wider than the screen outside any overflow-x:auto
    wrapper let iOS grab and drag the entire page "in circles"). Every
    wide table is supposed to live in its own scroller (.field-scroll,
    .heat-results, .heat-splits, .splits, .combined, ...); clip - not
    hidden, which would make body itself a scroll container - simply
    refuses page-level horizontal overflow if one ever escapes again. */
 html,body{overflow-x:clip}
 /* Outer shell scales with the viewport and centers the WHOLE page as a
    unit. Everything that should read as "one column" - the navbar, the
    content (.page-content), and the fixed footer - lines up on the shell's
    inner edges, since none of them carry a max-width of their own.
    .page-content used to be capped narrower than the shell to reserve the
    space to its right for the inline Live View sidebar; that sidebar is
    drawer-only on larger screens now, so the cap was just leaving the
    compiled/by-heat/start-list tables narrower than the navbar above them
    (explicit user request to match). It grows a step on very wide screens
    (1600px tier below) rather than staying pinned forever. */
 /* Operator broadcast banner (Admin screen's Broadcast button) - Index page
    only, per explicit user request. Amber like the venue Admin's own
    broadcast bar (a deliberately different accent from the site's navy/
    green so it reads as an active announcement, not routine chrome), full
    content width like the tables below it. */
 .broadcast-banner{display:flex;align-items:center;gap:.6rem;
   background:#fff3c4;border:1px solid #e0c060;border-radius:.4rem;
   padding:.6rem 1rem;margin:.75rem 0;font-weight:600;color:#5a4a12}
 /* SVG now (was an emoji character), so it's sized/coloured like the nav
    bar's own icons rather than via font-size. */
 .broadcast-banner .bb-icon{flex:0 0 auto;width:1.2rem;height:1.2rem;
   color:#5a4a12}
 .page-shell{max-width:1300px;margin:0 auto;padding:0 1rem;position:relative}
 @media (min-width:1600px){
   .page-shell{max-width:1860px}
   /* On html (the root), not body - everything sized in rem (table
      cell padding, the live-strip header/rows, etc.) scales up
      together instead of just plain body text. Kept modest (105%,
      not a huge jump) - this is a small step up for wide screens, not
      a redesign. */
   html{font-size:105%}
 }
 /* Smallest phones: step the whole page down via the root font size
    (everything is rem-based, so tables/text/strips all shrink together)
    so dense results tables fit better. */
 @media (max-width:420px){
   html{font-size:87.5%}
 }
 /* Best cell (compiled results) keeps Metric/Imperial/Wind stacked on
    every screen size, same as the Field Series attempt cells always
    have (explicit user report/request) - a prior narrow-phone rule used
    to collapse this cell onto one line, which looked inconsistent with
    Field Series and hid the wind reading. */
 table{border-collapse:collapse;width:100%} td,th{padding:.35rem .5rem;
 border-bottom:1px solid var(--border);text-align:left}
 thead th{background:var(--brand);color:#fff}
 /* Index page (schedule list): event-name/Start list/Result links use the
    browser's default link color (blue in light mode, clashing in dark) -
    explicit user request for plain black/page text instead, in both
    modes, matching the rest of the row. */
 #index tbody td a{color:var(--page-text)}
 #index tbody td{color:var(--page-text)}
 /* Grouped schedule tables (Admin's "Create Index Page"): no column wraps
    (explicit user request - Event name/Start list read awkwardly wrapped),
    so the block scrolls horizontally instead on narrow screens. Each
    table's own title lives in place of the "Event name" header (see
    render.py's table_html) with a gap between tables replacing the old
    per-table heading. */
 /* Breathing room under the meet logo/leaderboard ribbon before the
    table or card grid starts (explicit user request). */
 #index{overflow-x:auto;font-size:.85rem;margin-top:1.25rem}
 #index table{margin-bottom:1.5rem}
 #index td,#index th{white-space:nowrap;padding:.15rem .5rem;line-height:1.2}
 /* Fixed row height (explicit user request) - a table row's height is a
    floor, not a cap, in HTML tables, so this holds every row to exactly
    26px as long as nothing inside it (icon, badge, square) is taller;
    the 25px PDF icon and .idx-status-sq/.badge cells all fit under it. */
 #index tbody tr{height:26px}
/* A small meet (a few dual/tri XC races, say - render.py's
   _IDX_FEW_THRESHOLD) reads better bigger than the same table sized for
   a 40+ event championship (explicit user request). Row height/padding
   scale with it so bigger text doesn't clip inside the fixed 26px row. */
 #index.idx-few{font-size:1.25rem}
 #index.idx-few td,#index.idx-few th{padding:.3rem .7rem}
 #index.idx-few tbody tr{height:38px}
/* XC's table option (cardlayout=off) - bigger unconditionally (explicit
   user request: "take up more of the page", every XC table meet, not
   just the small-meet idx-few case above). white-space:nowrap already
   comes from the base #index td,th rule - text grows, never wraps.
   Centered (explicit user follow-up) - the table is shrink-to-fit
   (width:auto below), so margin:auto on the <table> itself centers the
   whole block without changing cell text alignment. */
/* Padding/row-height in em, not rem, deliberately - they need to shrink
   and grow WITH the media-query font-size steps below (rem is fixed to
   the root font-size regardless of #index's own, which would leave
   mobile with tiny text but full-size padding still eating the same
   absolute width - exactly what caused the horizontal scrollbar this
   was built to avoid). */
 #index.idx-xc-table{font-size:16px}
 #index.idx-xc-table td,#index.idx-xc-table th{padding:.12em .55em}
 #index.idx-xc-table tbody tr,#index.idx-xc-table thead tr{height:1.62em}
 #index.idx-xc-table table{margin:0 auto}
/* The fixed-width nth-child column scheme further down (both the
   701px+ and 1024px+ media blocks) assumes outdoor's 8-column layout
   (Day/Start/sq/Name/Round/Result/Status/PDF) - XC has no Round column
   (7 columns), so those widths would misalign, and at this bigger font
   they clip real content behind an ellipsis instead of just "not
   wrapping". !important is deliberate here: those rules are equal
   specificity and come later in the file, so a plain override would
   lose the cascade tie - auto layout lets every column size to its own
   (bigger) content, which is what "no wrapping" actually means. */
 #index.idx-xc-table table{table-layout:auto!important;width:auto!important}
 #index.idx-xc-table th,#index.idx-xc-table td{width:auto!important;
   overflow:visible!important;text-overflow:clip!important}
/* Scale with viewport (explicit user follow-up: mobile/tablet/large
   screens, no scrollbars at any of them) - higher specificity than the
   plain #index font-size steps a few rules down (extra .idx-xc-table
   class), so these always win regardless of source order. Event names
   are the long pole; these steps keep a typical XC event name
   ("Womens 6000 M Championship") inside the viewport at each range
   without needing #index's own overflow-x:auto scrollbar. */
 @media (max-width:480px){ #index.idx-xc-table{font-size:8.8px} }
 @media (min-width:481px) and (max-width:900px){
   #index.idx-xc-table{font-size:13.4px}
 }
 @media (min-width:1600px){ #index.idx-xc-table{font-size:19px} }
/* XC card grid (explicit user request, render.py's card_html) - the
   timer opts in per meet via [index]cardlayout=on. Capped at 4 columns
   wide (explicit user request) via max-width - 4 * 260px cards + 3 *
   1rem gaps - rather than an unbounded auto-fit that would keep adding
   columns on an ultra-wide screen; still narrows to fewer columns, down
   to one, on smaller viewports since auto-fit/minmax still drives that.
   font-size:1rem is a deliberate reset off #index's own .85rem base -
   cards read fine at the browser default, no need to inherit the
   table's smaller size. */
 #index .idx-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
   gap:1rem;font-size:1rem;max-width:calc(4*260px + 3*1rem);margin:0 auto;
   /* #index became a flex column (weather-anchored-to-bottom change) -
      this element is now a flex ITEM of it, and its own `margin:0 auto`
      (for max-width centering) disables flexbox's default stretch the
      same way .page-shell's did (see that rule's comment) - min-width:0
      alone doesn't fix that, since the item never even attempts to
      stretch/shrink to the container's width, it just sizes to its
      grid's own intrinsic (2-column) content width regardless. width:
      100% sidesteps the whole stretch negotiation with an explicit
      value instead: max-width still caps it exactly as before on wide
      screens (auto margins still center the leftover space around an
      explicit width just fine), and on narrow/mobile screens where
      max-width doesn't kick in, it now correctly fills the actual
      (shrunk) container width, letting auto-fit drop to 1 column. */
   width:100%;min-width:0}
 #index .idx-card{background:var(--stripe);border:1px solid var(--border);
   border-radius:.6rem;padding:1rem 1.1rem;display:flex;flex-direction:column;
   gap:.5rem;transition:border-color .15s}
 #index .idx-card:hover{border-color:var(--accent,#2b6cb0)}
 #index .idx-card-when{font-size:.78rem;color:var(--muted);font-weight:600;
   letter-spacing:.02em;text-transform:uppercase}
 #index .idx-card-name{font-size:1.15rem;font-weight:700;line-height:1.3;
   color:var(--page-text);text-decoration:none}
 #index .idx-card-name:hover{color:var(--accent,#2b6cb0)}
 #index .idx-card-foot{display:flex;align-items:center;justify-content:space-between;
   gap:.6rem;margin-top:auto;padding-top:.5rem;border-top:1px solid var(--border)}
 #index .idx-card-foot-left{display:flex;align-items:center;gap:.5rem}
 #index .idx-card-cta{color:var(--accent,#2b6cb0);font-weight:700;font-size:.88rem;
   text-decoration:none;white-space:nowrap}
 #index .idx-card-cta:hover{text-decoration:underline}
 /* Cards' own PDF icon still wants the same 20px sizing as the table's -
   #index .pdf-ico above already covers it, no override needed here. */
 /* Weather sits BELOW the card grid as its own row, not squeezed in as a
   grid item (render.py wraps cards in their own inner .idx-cards div for
   exactly this) - explicit user request for visible space between the
   last card row and the weather line. .idx-weather's own margin-top:1rem
   already provides that gap; bumped here specifically for the card
   context since cards are visually heavier than a table row. */
 #index .idx-cards + .idx-weather{margin-top:1.5rem}
 /* The real Adobe PDF logo (see render.py's _PDF_ICON_SVG - an <img>
    despite the class name, kept from the old inline-SVG days) now shows
    on mobile too (explicit user request, reversing the earlier "PDF"
    text swap) - same 20px size everywhere, since the row height is
    already fixed at 26px above and 20px already fits it without any
    jump, on both desktop and mobile alike. .pdf-ico-text stays in the
    markup (render.py) but is never shown - harmless dead weight rather
    than a template change, in case a future breakpoint needs it back. */
 #index .pdf-ico{width:20px;height:20px;display:block}
 #index .pdf-ico-text{display:none;font-size:.75rem;font-weight:700;
   color:#b00}
 /* Larger meet logo on the Index page (explicit user request) - a
    second, bigger showing of the same logo the nav header carries
    small, right after the Broadcast banner. Hidden on mobile (explicit
    user request) - the header's own small logo is already visible
    there, a second big one just costs scroll space on a phone. */
 .idx-meet-logo{text-align:center;margin:.75rem 0}
 .idx-meet-logo img{height:120px;width:auto;max-width:100%}
 @media (max-width:900px){ .idx-meet-logo{display:none} }
 /* Team Scores leaderboard ribbon (explicit user request) - static
    (no scroll/wrap) place/logo/abbrev/score strip, one row per gender,
    sitting above the Index table and below the Broadcast banner. A
    hard 10-team-per-gender cap (render.py's leaderboard_fragment) is
    what makes "never wraps" safe to promise - font-size only has to
    solve for a fixed cell count. container-type:inline-size makes the
    cqw units below read THIS element's own box width, not the browser
    viewport, so it scales correctly wherever it's embedded. */
 /* Clickable as a whole (explicit user request - was just the W:/M: tag)
    - opens the Team Scores drawer for the full standings/by-event
    breakdown, same navPanel() the nav bar's own Team Scores button
    uses (render.py's leaderboard_fragment puts the onclick/role/
    tabindex on this element). The "next 5" button (.lb-more) stops its
    click from bubbling here, so paging doesn't also pop the drawer. */
 .leaderboard{
   background:var(--panel-bg);border:1px solid var(--border);
   border-radius:.4rem;margin:.75rem 0;overflow:hidden;cursor:pointer;
   container-type:inline-size;container-name:lb;
 }
 body.no-ribbon .leaderboard{display:none}
 .lb-row{display:flex;align-items:center;gap:.5cqw;padding:.35rem 1rem}
 .lb-row+.lb-row{border-top:1px solid var(--border)}
 .lb-tag{
   flex:none;font-weight:800;color:var(--heading);
   font-size:clamp(.62rem,3.4cqw,.86rem);
 }
 .lb-count{
   /* Smaller than the tag/entries (explicit user request) - it's a
      secondary "how far along" figure, not something that needs to
      compete with the scores themselves. Text color is the normal
      readable page text now, not grey (explicit user request) - stays
      "black" in light mode while still flipping correctly in dark mode. */
   flex:none;color:var(--page-text);font-variant-numeric:tabular-nums;
   margin-right:.3rem;font-size:clamp(.45rem,2.1cqw,.66rem);
 }
 /* Desktop/tablet: no wrap, no scroll, ever - each of the (at most 10)
    entries is an equal flex cell, so the row always fills exactly one
    line. Mobile overrides this below to a 5-at-a-time paged strip
    instead (explicit user request - 10 cells never read cleanly on a
    phone no matter how small the font gets). */
 .lb-entries{display:flex;align-items:center;flex:1;min-width:0;overflow:hidden}
 .lb-entry{
   display:flex;align-items:center;gap:.4cqw;white-space:nowrap;
   padding:.1rem 1.1cqw;border-right:1px solid var(--border);
   flex:1;justify-content:center;min-width:0;
 }
 .lb-entry:last-child{border-right:none}
 .lb-place{
   flex:none;width:1.5cqw;min-width:1.05rem;height:1.05rem;border-radius:.2rem;
   /* White/grey (explicit user request) rather than the theme-following
      --stripe/--border pair, so the badge stays a consistent white
      square with a grey outline in both light and dark mode. */
   background:#fff;border:1px solid #9aa0a6;color:var(--muted);
   /* +1pt over the previous size (explicit user request). */
   font-weight:800;font-size:clamp(.58rem,2.8cqw,.74rem);
   display:inline-flex;align-items:center;justify-content:center;
   font-variant-numeric:tabular-nums;
   /* Nudged up-left off the row's shared baseline (explicit user
      request) - a MARGIN offset, not transform: transform paints the
      badge shifted without reserving that space, so at a narrow
      container width the shift became proportionally large enough to
      visually spill into the previous cell's divider on resize
      (explicit user report). Margin actually moves the box, so
      neighboring layout adjusts with it instead of just overlapping. */
   margin:-2px .2rem -2px -2px;
 }
 .lb-entry .team-logo{height:16px;width:16px}
 /* .lb-stack/.lb-line wrap {logo+abbrev} and {score} into two lines in
    the markup so mobile (below) can stack them, but display:contents
    here removes both wrappers from the box model entirely - their
    children join .lb-entry's own flex row directly, so desktop/tablet
    render exactly as if the wrappers didn't exist (flat single line). */
 .lb-stack,.lb-line{display:contents}
 .lb-abbr{font-weight:500;letter-spacing:.01em;font-size:clamp(.56rem,2.9cqw,.82rem)}
 .lb-score{
   font-weight:700;color:var(--heading);font-variant-numeric:tabular-nums;
   font-size:clamp(.56rem,2.9cqw,.82rem);
 }
 /* Mobile: 5 teams at a time in a snap-paged strip, with a "next 5"
    button (explicit user request) instead of forcing all 10 into an
    ever-shrinking single line. Swipe works too (native scroll-snap);
    the button is the discoverable affordance for anyone who doesn't
    think to swipe a non-obviously-scrollable strip. Breakpoint matches
    the Index table's own mobile cutoff (900px, see .idx-day-col)
    rather than the more common 700px, per explicit user request. */
 @media (max-width:900px){
   .lb-row{padding:.3rem .6rem .3rem 1rem}
   .lb-entries{
     overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;
     -webkit-overflow-scrolling:touch;
   }
   .lb-entries::-webkit-scrollbar{display:none}
   /* Divider stays (explicit user request - removing it read as a bug,
      not a deliberate simplification). At a fixed 20% cell width (5 per
      row) the place+logo+abbrev+score content can genuinely be WIDER
      than the cell itself for a 4-letter abbrev like "IAST"/"WASH"/
      "KSST" - that's what was bleeding across the divider into the next
      team's cell (explicit user report). overflow:hidden contains any
      remaining overflow to this cell as a last-resort clip, but the
      real fix is shrinking every piece (logo/place/gaps/padding/font)
      enough that the worst case (a 4-letter abbrev) fits without
      needing to clip at all - measured against the actual ~65px cell
      width at a 400px viewport. */
   /* Left padding minimized (explicit user request) - the place badge
      sits right at the cell's left edge instead of leaving a gutter. */
   .lb-entry{
     flex:0 0 20%;scroll-snap-align:start;padding:.05rem .15rem .05rem .05rem;
     overflow:hidden;gap:.15rem;
   }
   /* One size up from the previous mobile shrink (explicit user
      request). */
   .lb-entry .team-logo{height:14px;width:14px}
   /* Two lines instead of one - logo+abbrev on top, score underneath,
      both still to the right of the shared place badge (explicit user
      request). Overrides the desktop display:contents above so these
      wrappers actually lay out. line-height:1 and no inter-line gap
      keep the stack's total height close to the old single line's, so
      growing the fonts below doesn't grow the row (explicit user
      request - bigger text, same row height). align-items:stretch
      (the flex default) is what lets the score line's own centering
      below actually have the full stack width to center within. */
   .lb-stack{
     display:flex;flex-direction:column;justify-content:center;gap:0;
     line-height:1;
   }
   .lb-line{display:flex;align-items:center;gap:.15rem;line-height:1}
   /* Score centers under the logo+abbrev line above it, rather than
      sharing its left edge (explicit user request). */
   .lb-line-score{justify-content:center}
   /* +1pt over the previous pass (explicit user request). */
   .lb-abbr{font-size:.68rem}
   .lb-score{font-size:.68rem}
   /* One more level up (explicit user request, on top of the earlier
      +1pt/bold pass) - the box grows a touch more too so the bigger
      digit doesn't overflow its own badge. */
   .lb-place{
     min-width:.78rem;height:.78rem;font-weight:800;
     font-size:clamp(.46rem,1.8cqw,.52rem);
   }
   .lb-more{
     flex:none;display:flex;align-items:center;justify-content:center;
     width:1.6rem;height:1.6rem;margin-left:.3rem;border-radius:50%;
     border:1px solid var(--border);background:var(--stripe);
     color:var(--heading);font-size:1rem;line-height:1;cursor:pointer;
   }
 }
 @media (min-width:901px){ .lb-more{display:none} }
 #index .badge{padding:.05rem .35rem}
 /* Status-color square (explicit user request) - a quick-glance dot
    before the event name, no header. Blank (no square at all) for
    Scored/Official/Complete - those need no extra attention. */
 /* Dark grey border on every color (explicit user request) - keeps the
    lighter squares (amber especially) visible against the white row
    background instead of just blending in. */
 #index .idx-status-sq{display:inline-block;width:.6rem;height:.6rem;
   border-radius:2px;border:1px solid #555}
 #index .idx-sq-grey{background:#9aa0a6}
 #index .idx-sq-green{background:#2ea043}
 #index .idx-sq-red{background:#c00}
 #index .idx-sq-blue{background:#1a73e8}
 /* In Progress / On Hold (explicit user request) - same #FFF3C4 as the
    status badge. */
 #index .idx-sq-amber{background:#FFF3C4}
 /* Status-square column - half the normal cell padding (explicit user
    request), the square itself is tiny so the usual padding leaves an
    oversized gutter around it. */
 #index .idx-sq-col{padding-left:.25rem;padding-right:.25rem;
   padding-top:.075rem;padding-bottom:.075rem}
 /* Fixed per-column widths (explicit user request) so every day/type
    table's columns line up with its neighbors instead of each table
    auto-sizing to its own content. Order matches table_html/row_html:
    Day, Start, status square, Event name, Round, Result,
    Status, PDF (the separate Start List column was removed - explicit
    user request, the event page's SUMMARY RESULTS/START LIST tabs make
    it redundant). Event name is the only flexible column (nth-child(4)
    gets no width, so it absorbs whatever space the fixed columns don't
    use); table-layout:fixed makes the declared widths binding instead of
    just a suggestion.
    Desktop/tablet only (min-width:701px, matching the .idx-day-col
    breakpoint below) - on a narrow phone the fixed columns alone add up
    to more than the viewport, squeezing Event Name to nothing, which
    defeats the point (explicit user report). Mobile falls back to plain
    auto layout, same as before this feature. */
 @media (min-width:701px){
   #index table{table-layout:fixed;width:100%}
   #index th:nth-child(1),#index td:nth-child(1){width:5.5rem}
   #index th:nth-child(2),#index td:nth-child(2){width:6rem}
   #index th:nth-child(3),#index td:nth-child(3){width:1.4rem}
   #index th:nth-child(5),#index td:nth-child(5){width:6rem}
   #index th:nth-child(6),#index td:nth-child(6){width:7rem}
   #index th:nth-child(7),#index td:nth-child(7){width:5.5rem}
   #index th:nth-child(8),#index td:nth-child(8){width:2rem}
   /* table-layout:fixed with nowrap content that overflows its column
      just spills instead of expanding it - clip so a long event/round
      name doesn't bleed into the next column visually. */
   #index td{overflow:hidden;text-overflow:ellipsis}
 }
 /* Above 1024px there's real screen to spare - rem-sized columns leave
    all of it piled into the Event Name column alone, which reads as one
    big empty gap rather than a spread-out row (explicit user report).
    Switch every column to a percentage of the table width instead, so
    the extra space is shared out across all of them proportionally.
    Columns: Day, Start, status square, Event name, Round,
    Result, Status, PDF - matches render.py's row_html/
    table_html (the separate Start List column was removed - explicit
    user request, the event page's tabs make it redundant - so this is
    8 columns now, not 9). */
 @media (min-width:1024px){
   #index th:nth-child(1),#index td:nth-child(1){width:9%}
   #index th:nth-child(2),#index td:nth-child(2){width:9%}
   #index th:nth-child(3),#index td:nth-child(3){width:3%}
   #index th:nth-child(4),#index td:nth-child(4){width:28%}
   #index th:nth-child(5),#index td:nth-child(5){width:15%}
   #index th:nth-child(6),#index td:nth-child(6){width:22%}
   #index th:nth-child(7),#index td:nth-child(7){width:10%}
   #index th:nth-child(8),#index td:nth-child(8){width:4%}
 }
 /* Start List/Result/PDF columns pulled tighter together (explicit user
    request) - narrower side padding than the rest of the row, on both
    mobile and desktop. */
 #index .idx-narrow{padding-left:.2rem;padding-right:.2rem}
 /* Font steps for the grouped schedule tables (explicit user request) -
    larger on big screens where there's room to spare, smaller on phones
    where every column already fights for width. Independent of the
    whole-page html{font-size} steps above (1600px/420px) since those move
    every rem-sized thing at once; this only affects #index's own table
    text/padding. */
 @media (min-width:1600px){ #index{font-size:1rem} }
 @media (max-width:900px){ #index{font-size:.8rem} }
 @media (max-width:480px){ #index{font-size:.72rem} }
 /* Side breathing room for the grouped schedule tables on larger screens
    only (explicit user request) - .page-shell's own padding is shared
    with the navbar/footer, so this is scoped to #index instead of
    touching that. */
 @media (min-width:900px){ #index{padding:0 1.5rem} }
 @media (min-width:1600px){ #index{padding:0 2.5rem} }
 @media (max-width:768px){
   /* Hide the redundant Day column on phones to save width - the table's
      own header (was "Event name") already names the day/group. */
   #index .idx-day-col{display:none}
 }
 /* Combined-event standings (decathlon/heptathlon): dense spreadsheet -
    smaller font/columns, and cells top-aligned so each event's mark (line
    1) lines up along the top of the column, with the wind line (2) blank
    for non-wind events. */
 .combined{overflow-x:auto}
 /* Selectors below key off .combined-table (the table's own class) rather
    than the .combined wrapper div, so the Larger View modal's clone - the
    <table> alone, without its wrapper - keeps identical styling. */
 .combined-table{font-size:.72rem;width:100%}
 .combined-table td,.combined-table th{padding:.12rem .3rem;vertical-align:top;
   white-space:nowrap}
 .combined-table td small{font-size:.9em;color:var(--muted)}
 /* Highlighted Total column (body cells) + dashed vertical separators
    between event columns. */
 .combined-table td.col-total{background:var(--tint-yellow)}
 .combined-table .ev-col{border-left:1px dashed rgba(0,0,0,.18)}
 /* Whoever LEADS the competition after each event: that event column's
    cell for the running-place-1 athlete (ties highlight every sharer).
    Two classes out-specify the zebra stripe so it shows on even rows. */
 .combined-table td.ev-col.ev-lead{background:#FDFCEB}
 /* Center everything from the Total column rightward (headers + data). */
 .combined-table th.col-total,.combined-table td.col-total,.combined-table .ev-col{
   text-align:center}
 /* Multi-event Standings: freeze only the Athlete name column, always on
    (laptop and mobile) - same treatment as By-Heat/Splits and the Field
    Series compiled table, per explicit user request. A stable table class
    (not scoped through the .combined wrapper) so the Larger View modal's
    clone - which carries the <table> alone, not its wrapper div - keeps
    the same freeze. */
 .combined-table td.name-col,.combined-table th.name-col{
   position:sticky;left:0;z-index:2}
 .combined-table td.name-col{background:var(--page-bg)}
 .combined-table th.name-col{background:var(--brand);z-index:3}
 /* Zebra striping (the generic tr:nth-child(even) rule below) has lower
    specificity than the opaque name-col background above and would
    otherwise be masked on every other row - match it explicitly here,
    same fix as the By-Heat/Field Series frozen columns. */
 .combined-table tbody tr:nth-child(even) td.name-col{background:var(--stripe)}
 .split-modal-body .combined-table td.name-col{background:var(--panel-bg)}
 .split-modal-body .combined-table tbody tr:nth-child(even) td.name-col{
   background:var(--stripe)}
 .flag-pb,.flag-sb{background:#2b6cb0;color:#fff;font-weight:600;
   padding:.05rem .35rem;border-radius:.25rem;font-size:.85em}
 .flag-record{background:#2ea043;color:#fff;font-weight:600;
   padding:.05rem .35rem;border-radius:.25rem;font-size:.85em}
 /* Status pill palette - the reference table (legacy vs25.9.py's per-code
    <font color>, ported to cloud/render.py's _status_pill_class): text is
    black by default, background is a very light tint of the legacy colour.
    Border on every variant (including badge-none) so a white
    pill still reads as a pill against this same white page background. */
 .badge{padding:.1rem .4rem;border-radius:.3rem;font-size:.8rem;
   color:#000;border:1px solid rgba(0,0,0,.12)}
 /* Larger status badge in the results-page header (next to the event
    name) than the compact index-table badges. */
 h2 .badge{font-size:1rem;padding:.2rem .7rem;border-radius:.4rem;
   vertical-align:middle}
 .badge-grey{background:#eceeef}
 .badge-green{background:#c9ecd4}
 /* A touch darker than the original #fbe4e1 (explicit user request) -
    lightness pulled down without adding saturation, so it reads as a
    darker neutral/dusty tint rather than a more vivid red. */
 .badge-red{background:#ecd0cc}
 .badge-blue{background:#e2ecfa}
 .badge-none{background:#fff}
 /* In Progress / On Hold (explicit user request). */
 .badge-amber{background:#FFF3C4}
 h2 small{color:var(--muted)} h3{margin-top:1.5rem}
 h1,h2,h3,h4{color:var(--heading)}
 /* Day/time line, directly under the event name - larger than the
    default caption text. */
 .when{color:var(--muted);font-size:1.15rem;font-weight:600;margin:.1rem 0 .6rem}
 /* Schedule note (advancement rule / bar progression) from schedule.csv -
    ONE place only now (explicit user request): under the header/when
    line, before Records, on both the event page and the standalone
    start-list page. */
 .ev-note{margin:0 0 .7rem;padding:.4rem .7rem;font-size:.9rem;color:var(--heading);
   background:var(--tint-blue);border-left:3px solid var(--accent-2);
   border-radius:.25rem}
 h2{margin-bottom:.15rem}
 .heats{margin-top:.5rem} .heat{margin-bottom:1rem}
 /* RESULTS / START LIST tab bar (explicit user request) - replaces the
    standalone SUMMARY RESULTS bar whenever the event also has a start
    list, so the two share one navy strip instead of the start list
    sitting as its own disclosure far down the page. Same navy/white
    language as .sec-head's bar, just two buttons instead of one. */
 /* One size down from the previous .9rem/.4rem-.8rem (explicit user
    request) - still comfortably tappable, just less dominant than the
    page's primary actions. flex-wrap stays "wrap" here for desktop/
    tablet, where there's room; the sub-701px tier below switches to a
    non-wrapping scrollable row instead, so narrow phones get the three
    buttons side by side rather than stacked. */
 .sec-tabs{display:flex;flex-wrap:wrap;gap:.15rem;margin:.9rem 0 0}
 .sec-tab{
   background:var(--stripe);color:var(--muted);border:none;cursor:pointer;
   border-radius:.3rem .3rem 0 0;padding:.3rem .6rem;margin:0;
   font-size:.8rem;font-weight:700;letter-spacing:.05em;
   display:inline-flex;align-items:center;gap:.4rem;
 }
 .sec-tab .chev{color:var(--muted-2)}
 .sec-tab.on{background:var(--brand);color:#fff}
 .sec-tab.on .chev{color:#9fb6d6}
 .sec-tab .cnt{font-weight:600;font-size:.72rem;opacity:.8}
 .sec-tab:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
 /* Icon + short caption (Summary / Heat (splits) / Start List) - built
    for every tab but only shown on narrow screens (.tab-icon/.tab-
    label-short below); the desktop bar keeps the existing full ALL-CAPS
    text and hides these. */
 .sec-tab .tab-icon{display:none;width:18px;height:18px}
 .sec-tab .tab-label-short{display:none}
 .tab-body{border-top:.15rem solid var(--brand);padding-top:.5rem;margin-bottom:.5rem}
 /* Section header row: transparent track holding the navy label bar and,
    for By-Heat, the jump-to nav beside it on the same line. The navy sits
    on the BUTTON (not this row) so the coloured bar stops just past its
    text rather than running the full content width (explicit user
    request). */
 .sec-head{display:flex;align-items:center;flex-wrap:wrap;
   gap:.2rem .75rem;margin:.9rem 0 .5rem}
 /* Section label bar (SUMMARY RESULTS / HEAT RESULTS / START LIST): same
    navy + white text as the site header and table headers, so every section
    is announced identically. One shared width - wide enough for the longest
    label plus ~20%, so all three bars line up at the same length
    regardless of their own text. In rem, so it tracks the root font-size
    steps at the 420px/1600px tiers along with the label text itself.
    Scoped under .sec-head (not a bare .sec-toggle) so it outranks
    .disc-toggle, which the same button also carries - that rule sets
    background:none/color:var(--brand) and is declared later in this sheet, so at
    equal specificity it would win and the bar would render transparent. */
 .sec-head .sec-toggle{background:var(--brand);color:#fff;border-radius:.3rem;
   width:13rem;max-width:100%;box-sizing:border-box;
   justify-content:flex-start;padding:.35rem .6rem;margin:0;
   font-size:.8rem;font-weight:700;letter-spacing:.05em}
 .sec-head .sec-toggle .chev{color:#9fb6d6}
 /* Narrow phones: three tabs (or the single-section bar) side by side in
    one row rather than wrapping onto separate lines (explicit user
    request - stacked disclosure buttons ate too much vertical space).
    Rather than scrolling or shrinking the ALL-CAPS text further, each
    tab swaps to a compact icon-over-caption layout (explicit user
    request) - full labels/chevron hide, .tab-icon/.tab-label-short show,
    and the three tabs split the row evenly so nothing overflows. */
 @media (max-width:700px){
   .sec-tabs{flex-wrap:nowrap}
   .sec-tab{flex:1 1 0;min-width:0;flex-direction:column;gap:.15rem;
     padding:.4rem .2rem;font-size:.68rem}
   .sec-tab .chev{display:none}
   .sec-tab .tab-label-full{display:none}
   .sec-tab .tab-icon{display:block}
   .sec-tab .tab-label-short{display:block;white-space:nowrap;
     overflow:hidden;text-overflow:ellipsis;max-width:100%}
   /* The short label already folds the entry count onto its own line
      ("Start List (24)") - the desktop .cnt span would just repeat it. */
   .sec-tab .cnt{display:none}
   .sec-head .sec-toggle{width:auto}
 }
 .heat-jump{padding:.3rem .5rem;font-size:.85rem;border:1px solid #ccc;
   border-radius:.3rem;background:var(--input-bg);max-width:220px;color:var(--page-text)}
 .heat-nav{display:inline-flex;align-items:center;flex-wrap:wrap;
   gap:.3rem;margin-left:.5rem;vertical-align:middle}
 .heat-nav-lbl{font-size:.8rem;color:var(--muted);font-weight:600;margin-right:.1rem}
 .heat-num{min-width:1.9rem;padding:.2rem .45rem;font-size:.85rem;
   font-weight:600;background:var(--tint-blue);border:1px solid var(--border);
   border-radius:.3rem;cursor:pointer;color:var(--heading);line-height:1}
 .heat-num:hover{background:var(--brand);color:#fff;border-color:var(--brand)}
 .heat h4{margin:.6rem 0 .3rem;font-size:.9rem;font-weight:600}
 .heat-results{overflow-x:auto}
 /* By-Heat results laid up to two across (a full results table carries a
    lot of columns, so two reads better than three), smaller table font;
    auto-fit drops to one column as the width shrinks / on mobile. The
    min(480px,100%) floor lets a card shrink below 480px on narrow phones -
    a bare 480px minimum made the cards (and so the whole page) wider than
    the screen, so the page itself panned sideways and the frozen name
    column drifted out of line with it (explicit user report); the card's
    own .heat-results/.heat-splits scrollers are the intended overflow
    path. Same fix on .sl-heats (see its own rule further down). */
 .heats-grid{display:grid;
   grid-template-columns:repeat(auto-fit,minmax(min(480px,100%),1fr));
   gap:.2rem 1.4rem}
 /* Grid items default to min-width:auto, so a wide split table's intrinsic
    width was pushing the whole .heat card past its track on larger
    screens instead of scrolling inside .heat-splits - the card grew and
    everything after it ran underneath (explicit user report). Clamping
    the card to its track lets .heat-splits's own overflow-x:auto do the
    scrolling as intended. */
 .heats-grid>.heat{min-width:0}
 /* A lone heat/section shouldn't stretch edge-to-edge just for being the
    only card in the grid - auto-fit collapses the empty tracks and lets the
    single item fill the whole row. Cap it near the width it would get as
    one card of a multi-heat row instead (explicit user request). Same for a
    single start-list heat/flight, whose cards are narrower. */
 .heats-grid.single{grid-template-columns:minmax(0,720px)}
 .heats-grid .heat-results table,.heats-grid .heat-splits table{
   font-size:.8rem}
 .heats-grid .heat-results td,.heats-grid .heat-results th,
 .heats-grid .heat-splits td,.heats-grid .heat-splits th{padding:.15rem .35rem}
 /* Split table inside a heat card runs a touch smaller still (many split
    columns) and offers a "larger view" link out to the full splits page. */
 .heats-grid .heat-splits table{font-size:.72rem}
 .splits-expand{display:inline-block;margin:0 0 .3rem;font-size:.78rem;
   color:var(--accent);text-decoration:none;font-weight:600}
 .splits-expand:hover{text-decoration:underline}
 /* Breathing room between two back-to-back .splits-expand buttons on the
    same line (Multi-Event Standings' "View Projected Chart" immediately
    followed by "Larger view" - explicit user request, they were flush
    against each other). General sibling (~), not adjacent (+): a hidden
    .chart-src div (the chart image, toggled by openChartModal) sits
    between the two buttons in the markup, which + doesn't see past. */
 .splits-expand~.splits-expand{margin-left:.6rem}
 /* Field series "Larger view" only appears once Display Field Series is
    enabled (the toggle carries .open), with space after that button. */
 .disc-toggle[data-series]+.splits-expand{display:none;margin-left:1.2rem}
 .disc-toggle[data-series].open+.splits-expand{display:inline-block}
 /* Same rule for Display Splits' own "Larger view" - now rides on the same
    line as the toggle button (explicit user request - it used to sit on
    its own line above the split table). */
 .disc-toggle[data-splits]+.splits-expand{display:none;margin-left:1.2rem}
 .disc-toggle[data-splits].open+.splits-expand{display:inline-block}
 /* Standings link (multi-event constituents): keeps the .splits-expand
    affordance look but reads as a chip - a point larger, all caps, on a
    grey field with a white border (explicit user request). Declared after
    .splits-expand so it wins the equal-specificity tie on font-size. */
 .standings-link{font-size:calc(.78rem + 1pt);text-transform:uppercase;
   letter-spacing:.03em;background:var(--stripe);border:1px solid var(--panel-bg);
   border-radius:.3rem;padding:.2rem .55rem}
 /* Field events only: clearance from the Display Field Series control (and
    its "Larger view", when the series is open) that share this row. A
    general-sibling match, not adjacent - "Larger view" sits between the two
    - and it simply doesn't apply on a track constituent, where the
    standings link leads the row with nothing to clear. */
 .disc-toggle[data-series]~.standings-link{margin-left:1.2rem}
 nav a{margin-right:1rem;font-size:.9rem}
 .attempt-col{display:none;text-align:center;white-space:nowrap;
   min-width:3.4rem}
 table.show-series .attempt-col{display:table-cell}
 /* Field results tables size their columns to the data and never wrap,
    per explicit user request (rather than stretching to full width).
    Wrapped in a horizontal scroller so the series view can't overflow
    the page. */
 /* All results tables (running + field) never wrap their cells - so long
    athlete names stay on one line - and scroll horizontally inside their
    own container (only the table scrolls, not the page). */
 .res-table td,.res-table th{white-space:nowrap}
 /* Compiled results table (the one wrapped in .field-scroll, not the
    By-Heat or start-list tables): slightly tighter rows. */
 .field-scroll>.res-table td,.field-scroll>.res-table th{
   padding-top:.08rem;padding-bottom:.08rem;line-height:1.15}
 /* Compiled results mark column (running Time / field Best): center the
    column. Field Best gets a highlight background (explicit user
    request); running Time does not. */
 .field-scroll>.res-table td.res-time,.field-scroll>.res-table th.res-time,
 .field-scroll>.res-table td.fbest,.field-scroll>.res-table th.fbest{
   text-align:center}
 .field-scroll>.res-table td.fbest{background:var(--tint-yellow)}
 .field-scroll{overflow-x:auto;max-width:100%}
 /* Field tables fill the width like the running tables, in BOTH states -
    revealing the series used to drop the table to width:auto, which shrank
    it to its data and left the series view narrower than the plain one it
    replaced (explicit user request that the two match). Row spacing matches
    the running tables (the default cell padding) rather than a tighter
    pack. All rounds still fit when the series genuinely needs more room
    than the container: a table can't render below its min-content width, so
    it overflows into .field-scroll's scroller instead of being squeezed. */
 .field-table{width:100%}
 /* Faint dashed separators between the attempt/height columns. */
 .field-table td.attempt-col,.field-table th.attempt-col{
   border-right:1px dashed rgba(0,0,0,.12)}
 /* Dashed line to the LEFT of the first attempt column (R1) too - it's
    the one that directly follows a non-attempt column. */
 .field-table td:not(.attempt-col)+td.attempt-col,
 .field-table th:not(.attempt-col)+th.attempt-col{
   border-left:1px dashed rgba(0,0,0,.12)}
 /* Attempt columns (R1-R6 marks/imperial/wind) run a touch smaller and
    tighter than the rest of the field table so all six rounds fit across
    the page without horizontal scrolling. */
 .field-table td.attempt-col{font-size:.75rem;padding-left:.2rem;
   padding-right:.2rem}
 .field-table th.attempt-col{padding-left:.2rem;padding-right:.2rem}
 /* Vertical (HJ/PV) field series - progression headers and XXO marks run
    smaller still, so the many height columns pack in. */
 .vert-series td.attempt-col,.vert-series th.attempt-col{font-size:.7rem}
 /* Best column (field events) - not bolded, per request. */
 /* Best mark in the field series (bolded attempt cell) gets the same
    highlight background as the compiled Best column, per explicit
    user request. */
 .field-table td.attempt-col.best-attempt{background:var(--tint-yellow)}
 /* Mobile: JS (stickyFieldCols) pins the first identity columns; they
    need an opaque background (following the zebra striping) so the
    scrolling columns slide under them without losing their stripes. */
 @media (max-width:700px){
   .res-table td.sticky-col{position:sticky;background:var(--page-bg);
     z-index:2}
   .res-table tbody tr:nth-child(even) td.sticky-col{
     background:var(--stripe)}
   .res-table th.sticky-col{position:sticky;background:var(--brand);z-index:3}
   /* Highlighted rows keep their own color on the pinned columns too
      (explicit user report: the bubble row's amber stopped at the
      Athlete column on phones - these opaque sticky backgrounds were
      masking it). Declared after the zebra sticky rule with matching
      specificity so they win the tie; covers the same gap for the
      field-event current/on-deck highlights while at it. */
   .res-table tbody tr.bubble-row td.sticky-col{background:#FFF3C4}
   body.dark .res-table tbody tr.bubble-row td.sticky-col{
     background:#3a341a}
   .res-table tbody tr.current-competitor td.sticky-col{
     background:var(--tint-green)}
   .res-table tbody tr.on-deck td.sticky-col{background:var(--tint-yellow)}
   /* Dense results/field/split tables run smaller and tighter on phones
      so more of Place/Logo/Athlete/Team/Best/splits is visible at once. */
   .res-table,.split-table{font-size:.78rem}
   .res-table td,.res-table th,
   .split-table td,.split-table th{padding:.18rem .3rem}
   .res-table .team-logo,.split-table .team-logo{height:18px;width:18px}
 }
 /* "Q" (auto-qualified) is settled the moment it's assigned - green,
    no background. "q" (next-best across the round) stays black-on-
    amber (matching tr.bubble-row below - the same "still provisional"
    cue, and the same #FFF3C4 the In Progress/On Hold status badges
    wear, explicit user request) until the round is actually final,
    then it switches to the same green/no-background look as "Q". */
 .qual-auto{font-weight:700;color:#2ea043;padding:.1rem .4rem;
   border-radius:.3rem}
 .qual-provisional{font-weight:700;color:#000;background:#FFF3C4;
   padding:.1rem .4rem;border-radius:.3rem}
 /* Zebra striping - every other body row. Same specificity (element +
    one pseudo/class) as the row-highlight rules right below, so those
    still win on a tie since they're declared later in the cascade;
    header rows are untouched since :nth-child counts among siblings
    within their own <thead>, where there's only ever one row. */
 tr:nth-child(even){background:var(--stripe)}
 /* Last provisional qualifier's row: #FFF3C4 to match the "q" cell and
    the In Progress badge (explicit user request) - deliberately NOT
    --tint-yellow anymore, since that variable also paints the Best Mark
    column/on-deck rows which stay at their own #FDFCEB. Dark mode keeps
    the old dark tint (a light amber bg under dark mode's light text
    would be unreadable). */
 tr.bubble-row{background:#FFF3C4}
 body.dark tr.bubble-row{background:#3a341a}
 tr.current-competitor{background:var(--tint-green);box-shadow:inset 3px 0 0 #2ea043}
 tr.on-deck{background:var(--tint-yellow)}
 .wind-illegal{color:#b00;font-weight:600}
 .projected{color:var(--muted-2)}
 .dq-comment{position:relative}
 .dq-icon{cursor:pointer;color:var(--accent);font-style:normal;font-size:1.05em}
 .dq-popover{display:none;position:absolute;bottom:100%;left:0;
   background:#333;color:#fff !important;padding:.4rem .6rem;
   border-radius:.3rem;font-size:.75rem;font-style:normal;
   white-space:normal;width:200px;z-index:15;
   box-shadow:0 2px 8px rgba(0,0,0,.3);margin-bottom:.3rem}
 /* Show on hover (desktop) AND when tapped/clicked open (mobile/touch). */
 .dq-icon:hover .dq-popover,.dq-icon.open .dq-popover{display:block}
 .team-logo{height:24px;width:24px;object-fit:contain;vertical-align:middle}
 .heat-start{color:var(--muted);font-size:.8rem;margin:.2rem 0 0}
 /* Reaction time (block sensor) - muted colour, same font size as the row. */
 td.react{color:var(--muted);font-variant-numeric:tabular-nums}
 /* Team name stacked under the athlete name (no separate Team column) -
    smaller and muted so the athlete name still leads. */
 .ath-team{font-size:.88em;color:var(--muted);font-weight:400}
/* Comp # pill (XC only, explicit user request) - the athlete's
   competitor/bib number, next to Team [Yr] on the same line. Deliberately
   subtle: smaller than the already-small .ath-team text, fixed light
   grey/black regardless of theme - same "self-contained chip, doesn't
   change with the page background" treatment as the status/flag badges. */
 .xc-comp-pill{display:inline-block;font-size:.62em;font-weight:400;
   color:#333;background:#e8e8e8;border:1px solid #888;border-radius:.7rem;
   padding:.03rem .45rem;vertical-align:middle}
 /* Points-back stacked under the Total in the standings table. */
 .pts-back{font-size:.88em;color:var(--muted);font-weight:400}
 /* Relay leg names (reslt*.csv) under the team/relay entry. */
 .relay-legs{font-size:.95em;color:var(--muted);font-weight:400}
 .heat-start .wx{color:var(--accent)}
 /* Current conditions footer at the bottom of the Index page - same .wx
    icon/text treatment as the per-event weather line, once for the whole
    meet instead of repeated under every event. */
/* Bottom-of-page treatment (explicit user request) - clear separation
   from the table/cards above (bigger top margin, a more visible grey
   divider bar) and centered, instead of a left-aligned line sitting
   right under the last row. */
 .idx-weather{margin-top:2.5rem;padding-top:.75rem;border-top:2px solid var(--border);
   color:var(--muted);font-size:.85rem;text-align:center}
 .idx-weather .wx{color:var(--accent)}
 /* Anchor the weather line + its divider to the very bottom of the page
    (explicit user request) - on a short meet (few events) it used to sit
    right under the cards/table with a big empty gap down to the fixed
    site footer; now it's pushed all the way down to sit just above that
    footer instead, via a flex column running from .page-shell through
    #index. Scoped to Index-page shells only (:has(#index) - only
    render_page's Index route ever emits that id) so no other page's
    layout changes. On a long meet where the table/cards already fill the
    viewport, there is no free space for the flex column to give away, so
    margin-top:auto computes to ~0 here and weather just sits right after
    the last row, same as before - the divider (border-top above) still
    reads clearly either way.

    The flex column starts at .page-shell, NOT <body> - body has several
    OTHER direct children besides .page-shell (jump/side panels, modals),
    and more importantly .page-shell's own `margin:0 auto` (its normal
    max-width-centering rule) would otherwise disable flexbox's default
    cross-axis stretch the moment it became a flex ITEM of a flex body:
    per spec, auto margins on a flex item absorb space and OPT OUT of
    stretch, so .page-shell (and everything nested inside it) would
    shrink to its CONTENT's intrinsic width - the two 260px event cards
    side by side - instead of the actual (narrow, mobile) viewport width,
    which is exactly what caused the whole page to overflow sideways and
    the cards to never drop to a single column. Starting the flex column
    at .page-shell itself sidesteps that: nothing above it is a flex
    item, so its own margin:auto keeps behaving exactly as it always did
    (cap at max-width, center within body), and every plain block child
    below it (.page-content, #target, #index - none of which have a
    margin:auto of their own) stretches to fill it correctly. */
 body:has(#index) .page-shell{display:flex;flex-direction:column;
   min-height:calc(100vh - 4rem)}
 @media (max-width:500px){
   /* Matches the fixed footer's own mobile clearance (body{padding-
      bottom:3.6rem} in the 500px breakpoint above) so this stays
      accurate at the width where that padding changes. */
   body:has(#index) .page-shell{min-height:calc(100vh - 4.6rem)}
 }
 body:has(#index) .page-content,body:has(#index) #target,
 body:has(#index) #index{
   display:flex;flex-direction:column;flex:1;
   /* Same min-width:auto shrink issue .idx-cards below has its own fix
      for, but at every level of this nested flex-column chain (each is
      a flex item of its own parent's column) - without min-width:0
      here too, #index (and everything below it) stays as wide as the
      cards grid wants instead of shrinking to the actual viewport. */
   min-width:0}
 /* body:has(#index) #index .idx-weather (not just .page-shell ...
    .idx-weather) - needs the extra #index to out-specificity the
    card-context override above (#index .idx-cards + .idx-weather). */
 body:has(#index) #index .idx-weather{margin-top:auto}
 .heat-splits{margin-top:.5rem}
 /* Scrolls rather than overflowing: the cells below are nowrap, so a long
    athlete/team name can push the table past a narrow screen - keep that
    inside the table's own box instead of scrolling the whole page. */
 /* Compiled report pages (/reports/results, /reports/startlists):
    embedded PDF viewer with a download link above it. 75vh, not a fixed
    px height, so it fills most of any screen; phones that refuse inline
    PDFs still have the link. */
 .pdf-dl{font-weight:600}
 .pdf-frame{width:100%;height:75vh;border:1px solid var(--border);
   border-radius:.4rem}
 .records{margin-bottom:1rem;overflow-x:auto}
 /* Records table runs smaller than the results tables, and (unlike the
    compiled table) sizes to its own content rather than stretching to the
    full content width - it's a short reference table, so spanning as wide
    as the results just strands its columns apart on a large screen. Roomier
    cell padding and centred at every width, per explicit user request.
    Widened on follow-up: the table is content-sized, so that's a min-width
    floor rather than a fixed width - a longer record row still sizes to its
    own content instead of being clamped. The min() keeps the floor from
    forcing overflow on a narrow screen, where a bare min-width would
    otherwise beat max-width. nowrap on the cells is what actually
    guarantees no name ever wraps (explicit user request) - it makes the
    table's min-content width its true one-line width, which the auto width
    then honours; .records scrolls if that exceeds a narrow screen. */
 .records table{font-size:.8rem;width:auto;min-width:min(680px,100%);
   margin:0 auto}
 /* Left-justified instead of centred on large screens (explicit
    follow-up user request, overriding the centred default above which
    stays for mobile/tablet). */
 @media (min-width:1024px){ .records table{margin:0} }
 .records td,.records th{padding:.3rem .9rem;white-space:nowrap}
 .team-limit{display:flex;align-items:center;gap:.5rem;font-size:.9rem;
   font-weight:600;color:var(--page-text);margin:.2rem 0 .7rem}
 .team-limit input[type=range]{flex:1;max-width:200px}
 #team-limit-val{min-width:2.2rem;color:var(--accent)}
 /* Athletes drawer/page: search box, count, report button, compact table.
    .ath-sticky freezes the search/filter/count/report block at the top
    while the table scrolls under it - top offset is set by JS (see
    stickAthleteControls) to the drawer head's actual rendered height,
    since the head is rendered outside this fragment (fetched separately
    by openAthletes) and its height isn't a fixed CSS constant. */
 .ath-sticky{position:sticky;top:0;z-index:2;background:var(--panel-bg);
   padding-top:.3rem;margin-top:-.3rem}
 /* Athletes/Relays tabs (explicit user request for the Relays tab) -
    same look as Team Scores' .score-tabs. Sticky, sitting ABOVE
    .ath-sticky in the stacking order - stickAthleteControls sets both
    elements' top offsets since the drawer's own head height (rendered
    outside this fragment) isn't a fixed CSS constant. */
 .ath-tabs{display:flex;gap:.3rem;margin:0 0 .5rem;position:sticky;z-index:3;
   background:var(--panel-bg);padding-top:.3rem}
 .ath-tabs button{flex:1;padding:.35rem;font-size:.82rem;font-weight:600;
   color:var(--heading);background:var(--tint-blue);border:1px solid var(--border);
   border-radius:0;cursor:pointer}
 .ath-tabs button.on{background:var(--brand);color:#fff;border-color:var(--brand)}
 .ath-pane[hidden]{display:none}
 .ath-search{width:100%;box-sizing:border-box;padding:.4rem .6rem;
   font-size:.95rem;border:1px solid var(--border);border-radius:.3rem;
   margin:.2rem 0 .4rem}
 /* "Exact search" switch (checkbox styled as a slider - mobile-friendly). */
 .ath-exact{display:inline-flex;align-items:center;gap:.4rem;cursor:pointer;
   font-size:.85rem;font-weight:600;color:var(--page-text);margin:.1rem 0 .4rem;
   user-select:none}
 .ath-exact input{position:absolute;opacity:0;width:0;height:0}
 .ath-exact .ath-exact-track{width:34px;height:18px;border-radius:999px;
   background:#bbb;position:relative;transition:background .18s;flex:0 0 auto}
 .ath-exact .ath-exact-track::after{content:'';position:absolute;top:2px;left:2px;
   width:14px;height:14px;border-radius:50%;background:#fff;transition:left .18s}
 .ath-exact input:checked+.ath-exact-track{background:#2b6cb0}
 .ath-exact input:checked+.ath-exact-track::after{left:18px}
 /* Generic "waiting on an async fetch" placeholder (currently just the
    Athletes drawer's ~1.3k-row list, lazy-loaded on first open) - a
    spinning ring + "Loading data...", not bare text, per explicit user
    request. Respects prefers-reduced-motion. */
 .loading{display:flex;align-items:center;gap:.6rem;color:var(--muted);
   font-size:.9rem;padding:1.2rem 1rem}
 .spinner{width:1.1rem;height:1.1rem;flex:0 0 auto;border-radius:50%;
   border:2px solid var(--border);border-top-color:var(--heading);
   animation:spin .7s linear infinite}
 @keyframes spin{to{transform:rotate(360deg)}}
 @media (prefers-reduced-motion:reduce){.spinner{animation:none}}
 .ath-count{font-size:.8rem;color:var(--muted-2);margin-bottom:.3rem}
 .ath-report{background:none;border:1px solid var(--border-strong,#999);
   border-radius:0;font-size:.8rem;padding:.25rem .6rem;cursor:pointer;
   margin-bottom:.5rem}
 #athletes table{font-size:.78rem}
 #athletes td,#athletes th{padding:.15rem .35rem}
 /* No zebra striping in the Athletes drawer (explicit user request) - just
    a plain row divider instead. #athletes beats the generic tr:nth-child
    rule above (id + pseudo vs. bare pseudo). */
 #athletes tr:nth-child(even){background:none}
 #athletes tbody tr{border-bottom:1px solid var(--border)}
 /* Gender its own column now (was inline <small> after the name) - muted
    like the rest of the secondary text in this table. */
 #athletes td.ath-gender-cell{color:var(--muted-2)}
 .record-badge{background:#2ea043;color:#fff;padding:.05rem .35rem;
   border-radius:.25rem;font-size:.7rem;font-weight:600}
 .vert-dot{display:inline-block;width:.7rem;height:.7rem;
   border-radius:50%}
 .vert-in{background:#2ea043}
 .vert-passed{background:#e8b800}
 @media (max-width:768px){
   .records{display:none}
   .records.show-records{display:block}
 }
 /* Disclosure header (Field Series, Splits) - a chevron that rotates as
    the section expands, replacing the old show/hide toggle buttons. */
 .disc-toggle{display:inline-flex;align-items:center;gap:.4rem;background:none;
   border:none;cursor:pointer;font-size:.9rem;font-weight:600;color:var(--heading);
   padding:.3rem 0;margin:.2rem 0 .5rem}
 .disc-toggle .chev{display:inline-block;transition:transform .18s;
   font-size:.65rem;color:var(--muted-2)}
 .disc-toggle.open .chev{transform:rotate(90deg)}
 /* Start list vs results: same navy palette, differentiated by a per-heat
    badge (START LIST / RESULTS), a left-to-right fade on the start-list
    header, and a two-across layout for start-list heats. */
 /* One continuous left-to-right fade across the WHOLE header row (put the
    gradient on the row, cells transparent) instead of each column header
    fading on its own and repeating the band per column. */
 .startlist-table thead tr{
   background:linear-gradient(to right,var(--brand),#3a6ea5)}
 .startlist-table thead th{background:transparent}
 /* Left accent border: solid navy on every results table. Start-list
    tables drop that solid border and instead get a faded (top-to-bottom
    gradient) navy border on their wrapper div - border-image is reliable
    on a div, unlike on a border-collapse table. */
 .res-table,.split-table{border-left:3px solid var(--brand)}
 /* Split tables (per-athlete + Intermediate Leaders) run a smaller row
    height to match their smaller font, with dashed grey vertical lines
    between columns (not before the first). */
 .split-table td,.split-table th{padding:.12rem .35rem}
 .split-table td.lap,.split-table th.lap{border-left:1px dashed rgba(0,0,0,.18)}
 /* A wide split table (many split points, e.g. 3000SC/5000) scrolls within
    its heat card instead of overflowing into the neighbouring column. Only
    the Athlete/Relay name column freezes (.name-col below) - Pl and any
    logo column scroll away with everything else, per explicit user
    request (was previously Pl+Logo+Athlete together). */
 .heat-splits{overflow-x:auto}
 /* Standalone /splits/{key} page (splits_fragment's own wrapper) - the
    same wide split table, but with no heat card around it, so it needs
    its own horizontal scroller; without one a long-race table overflowed
    the page itself on phones. */
 .splits{overflow-x:auto}
 .split-modal-body .split-table td.name-col{background:var(--panel-bg)}
 /* Freeze ONLY the Athlete/Relay name column - By Heat results table and
    the split table (inline heat card, standalone /splits page, and the
    Larger View modal all share this markup) - always on, laptop and
    mobile alike, not gated to an overflow/resize check like the older
    multi-column sticky-col system still used by the compiled table. */
 .heat-results .res-table td.name-col,.heat-results .res-table th.name-col,
 .split-table td.name-col,.split-table th.name-col{
   position:sticky;left:0;z-index:2}
 .heat-results .res-table td.name-col,.split-table td.name-col{
   background:var(--page-bg)}
 .heat-results .res-table th.name-col,.split-table th.name-col{
   background:var(--brand);z-index:3}
 .heat-results .res-table tbody tr:nth-child(even) td.name-col,
 .split-table tbody tr:nth-child(even) td.name-col{
   background:var(--stripe)}
 /* Field Series (Horizontal/Vertical) compiled table - same name-only
    freeze, always on (laptop and mobile), replacing the old mobile-only
    Pl+Logo+Athlete JS freeze for this table specifically. */
 .field-table td.name-col,.field-table th.name-col{
   position:sticky;left:0;z-index:2}
 .field-table td.name-col{background:var(--page-bg)}
 .field-table th.name-col{background:var(--brand);z-index:3}
 .field-table tbody tr:nth-child(even) td.name-col{background:var(--stripe)}
 /* Highlighted rows (bubble/current/on-deck) keep their own color on the
    frozen name column too - declared after the zebra rule with matching
    specificity so it wins the tie, same pattern as the old sticky-col
    system used. */
 .field-table tbody tr.bubble-row td.name-col{background:#FFF3C4}
 body.dark .field-table tbody tr.bubble-row td.name-col{background:#3a341a}
 .field-table tbody tr.current-competitor td.name-col{
   background:var(--tint-green)}
 .field-table tbody tr.on-deck td.name-col{background:var(--tint-yellow)}
 .startlist-table{border-left:0}
 .sl-tw{border-left:3px solid transparent;overflow-x:auto;
   border-image:linear-gradient(to bottom,var(--brand),transparent) 1}
 .heat-tag{background:var(--brand);color:#fff;font-size:.7rem;font-weight:700;
   padding:.1rem .5rem;border-radius:.4rem;letter-spacing:.06em;
   vertical-align:middle;margin-left:.5rem}
 .heat-tag.sl-tag{background:linear-gradient(to right,var(--brand),#3a6ea5)}
 /* Start-list heats sit up to three across, filling the row - auto-fit
    means two heats span the full width (not squeezed into 2 of 3
    columns) and it drops to one column on mobile automatically. */
 .sl-heats{display:grid;
   grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));
   gap:.2rem 1.2rem}
 /* Grid items default to min-width:auto, so a relay heat's unbreakable
    relay-legs text (4 names, comma-separated) was pushing that card
    past its track and visibly bleeding into/over the next one in the
    ~950-1600px range where auto-fit picks 2-3 columns (explicit user
    report) - same root cause and same fix as .heats-grid>.heat above,
    the "Same fix on .sl-heats below" this promised. Clamping the card
    lets .sl-tw's own overflow-x:auto (added alongside this) do the
    scrolling instead. */
 .sl-heats>.sl-heat{min-width:0}
 /* Same width as a single Heat Results table (.heats-grid.single,
    explicit user request - the two should match instead of the start
    list being noticeably narrower). */
 .sl-heats.single{grid-template-columns:minmax(0,720px)}
 .sl-heats h4{margin:.6rem 0 .2rem}
 /* Rank-superscript footnote (render.py's _startlist_body) - sits once
    at the bottom of the whole start list, below every heat/flight card,
    not repeated per card. */
 .sl-rank-note{font-size:.75rem;color:var(--muted-2);
   margin:.5rem 0 0}
 /* SB-wind footnote sits directly under the rank one (explicit user
    request) - no extra gap between two consecutive notes. */
 .sl-rank-note+.sl-rank-note{margin-top:0}
 /* Start-list tables run tighter than results tables - smaller font, less
    cell padding, smaller logos - so all columns (incl. SB/PB) fit when
    three heats sit across the row instead of getting clipped. */
 .startlist-table{font-size:.8rem}
 .startlist-table td,.startlist-table th{padding:.14rem .3rem}
 .startlist-table .team-logo{height:16px;width:16px}
 /* Non-mobile: a bit more breathing room down the sides of the results,
    per-heat/section and start-list tables (explicit user request). Only the
    LEFT/RIGHT cell padding moves - the vertical padding above stays put, so
    rows keep their density and the side-by-side heat cards don't grow
    taller. Declared after the rules it widens (same specificity, so source
    order is what wins); phones keep the tighter padding set further up. */
 @media (min-width:701px){
   .field-scroll>.res-table td,.field-scroll>.res-table th{
     padding-left:.8rem;padding-right:.8rem}
   .heats-grid .heat-results td,.heats-grid .heat-results th{
     padding-left:.6rem;padding-right:.6rem}
   .startlist-table td,.startlist-table th{
     padding-left:.55rem;padding-right:.55rem}
 }
 /* Comp# column: small and grey to keep it compact. */
 .sl-comp{font-size:.68rem;color:var(--muted-2)}
 .sl-mark{font-size:.72rem;color:var(--muted);font-variant-numeric:tabular-nums;
   white-space:nowrap}
 .sl-imp{display:block;font-size:.85em;color:var(--muted-2);font-weight:400}
 /* .has-stats (render.py's _startlist_body, set only when SB/PB actually
    rode along) scopes this to the real SB/PB header cells - without it,
    once those 2 columns are gone (Show SB/PB off), this grabbed whatever
    ended up last instead - Athlete included - and right-aligned it by
    mistake (explicit user report). */
 .startlist-table.has-stats th:nth-last-child(-n+2),
 .startlist-table .sl-mark{text-align:right}
 /* Breathing room on the last column (PB, or Team when no stats) so the
    start-list tables don't run right to the edge. */
 .startlist-table td:last-child,.startlist-table th:last-child{
   padding-right:1rem}
 /* Toggle switch (Records) - a labelled slider. */
 .rec-switch{display:none;align-items:center;gap:.5rem;cursor:pointer;
   font-size:.85rem;font-weight:600;color:var(--page-text);
   margin:.3rem 0 .6rem;user-select:none}
 @media (max-width:768px){ .rec-switch{display:inline-flex} }
 .rec-switch .sw{position:relative;width:38px;height:20px;border-radius:999px;
   background:#bbb;transition:background .18s;flex:0 0 auto}
 .rec-switch.on .sw{background:#2b6cb0}
 .rec-switch .sw::after{content:'';position:absolute;top:2px;left:2px;
   width:16px;height:16px;border-radius:50%;background:#fff;transition:left .18s}
 .rec-switch.on .sw::after{left:20px}
 .live-strip{background:var(--tint-green);border-left:3px solid #2ea043;
   border-radius:.4rem;padding:.6rem .9rem;margin:.5rem 0;
   line-height:1.35;font-size:1.05rem}
 .live-strip-name{font-weight:700}
 .live-strip-name small{font-weight:400;color:var(--muted)}
 .live-strip-detail{font-size:1rem}
 .live-strip-extra{font-size:.95rem;color:var(--amber)}
 .live-strip-extra:empty,.live-strip-detail:empty{display:none}
 /* Running (track) header: EVENT NAME + HEAT, all caps and much larger
    than the body text, with reserved slots for Wind (from CURRLYNX) and
    a future clock feed (blank until that data stream exists). */
 /* nowrap (explicit user request) - the clock must stay on the same
    line as the event name/heat, never drop below them. Event name is
    the one flexible/shrinking item (min-width:0 + ellipsis) so it's
    what gives way if the row is ever too narrow for everything, rather
    than the row wrapping and pushing the clock down. */
 .live-strip-header{display:flex;align-items:baseline;flex-wrap:nowrap;
   gap:0 .9rem;text-transform:uppercase;font-weight:800;font-size:1.7rem;
   border-bottom:2px solid #2ea043;padding-bottom:.35rem;margin-bottom:.5rem}
 /* Event name / Heat sized down (explicit user request). */
 .live-strip-title{flex:1 1 auto;font-size:14px;min-width:0;
   overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
 .live-strip-heat{flex:none;white-space:nowrap;font-weight:600;
   color:var(--accent);font-size:14px}
 .live-strip-wind{flex:none;font-weight:600;color:var(--amber);
   font-size:1.2rem;min-width:1px}
 .live-strip-clock{flex:none;font-weight:600;color:#0f0;font-size:1.2rem;
   min-width:6rem;text-align:center;background:#000;padding:.1rem .5rem;
   border-radius:.2rem}
 @media (max-width:700px){
   .live-strip-clock{font-size:.8rem;min-width:3.4rem;
     padding:.1rem .3rem}
   /* Athlete rows read better a notch smaller on a phone. (Extra
      selector depth so this beats the base .live-strip-row rule that
      appears later in the sheet.) */
   .live-strip .live-strip-row{font-size:.95rem}
   /* Event title + heat number, same one-notch-down treatment. */
   .live-strip .live-strip-title,
   .live-strip .live-strip-heat{font-size:12px}
   /* Mobile: badge stays on ONE line (the two-line stack is a
      wide-screen look) - small enough to fit beside the title. */
   /* Browsers clamp tiny font sizes (~8px floor), so shrink the whole
      badge with a transform instead to get below it. */
   .live-strip-badge{max-width:none;white-space:nowrap;
     font-size:.5rem;padding:.1rem .3rem;
     transform:scale(.85);transform-origin:left center}
 }
 /* STARTLIST/RESULTS sub-header - tells the viewer at a glance whether
    the rows below are the pre-race roster or actual finish results. */
 .live-strip-mode{display:flex;align-items:center;font-size:.85rem;
   font-weight:700;letter-spacing:.08em;color:var(--muted);
   border-bottom:2px solid #2ea043;padding-bottom:.15rem;margin-bottom:.35rem}
 .live-strip-results{display:flex;flex-direction:column;gap:.1rem}
 /* Grid columns so lane/place, name, team, and every time column line
    up row to row - roster and result rows use the same column count
    scheme within their own section (only one section shows at once). */
 .live-strip-row{font-size:1.15rem;padding:.05rem 0;column-gap:.5rem;
   align-items:baseline}
 .live-strip-row span{overflow:hidden;text-overflow:ellipsis;
   white-space:nowrap}
 .live-strip-roster-row{display:grid;grid-template-columns:2.4rem 1fr 7rem;
   border-bottom:1px solid var(--border)}
 .live-strip-roster-row:last-child{border-bottom:none}
 /* Affiliation back in its own column (explicit user follow-up,
    reversing the earlier merged "LAST First - TEAM" cell). Name and Team
    are BOTH flexible (3:2, name favoured) so neither can pin the other
    to zero width when the fixed time columns eat the rest - each just
    ellipsis-truncates via the shared .live-strip-row span rule above.
    Time/split columns sized to the 12px digits ("2:44.98"), not the old
    5rem, so the text columns keep as much room as possible. */
 .live-strip-result-row{display:grid;
   grid-template-columns:2.4rem minmax(0,1.5fr) minmax(0,1fr)
     3.8rem 3.8rem 3.8rem;
   border-bottom:1px solid var(--border)}
 /* No-splits heats (render.py drops the two split spans when no row has
    split data - short sprints, or splits not fed): just Place/Name/Team/
    Time, the name keeps the reclaimed width instead of being ellipsis-
    squeezed against two empty split columns (explicit user report). */
 .live-strip-result-row.no-splits{
   grid-template-columns:2.4rem minmax(0,1.5fr) minmax(0,1fr) 3.8rem}
 /* Small phones: drop the Affiliation column from result rows entirely
    (the name needs the room more) and step the row font down a notch so
    long names shrink-to-fit rather than ellipsis-truncate (explicit user
    request). The roster/start-list row keeps its team column - no time
    columns competing for width there. */
 @media (max-width:480px){
   .live-strip-result-row{
     grid-template-columns:2.4rem minmax(0,1fr) 3.8rem 3.8rem 3.8rem}
   .live-strip-result-row.no-splits{
     grid-template-columns:2.4rem minmax(0,1fr) 3.8rem}
   .live-strip-result-row .live-strip-team{display:none}
   .live-strip-row.live-strip-result-row{font-size:11px}
 }
 /* Time/split columns (4th on) sit flush right so the numbers line up on
    their right edge, clear of the name/team columns. */
 .live-strip-result-row span:nth-child(n+4){text-align:right}
 .live-strip-result-row:last-child{border-bottom:none}
 /* Result AND roster/start-list rows both sized down (explicit user
    request, extending the result-only pass) - two classes so this beats
    the shared .live-strip-row font-size at equal specificity via
    declaration order/selector depth. Athlete name is a plain unstyled
    span, so it inherits this directly; .live-strip-team has its own
    font-size below and needs its own override to match. */
 .live-strip-row.live-strip-result-row,
 .live-strip-row.live-strip-roster-row{font-size:12px}
 /* Field current-competitor card (CURRFIELDLYNX). Sub-header = current
    athlete name+affiliation; Row 1 = Pl/A/mark-metric/mark-imperial
    (+VComponents for vertical); Row 2 = six attempt blocks (best mark
    highlighted); footer = next athlete. The in-progress attempt shows a
    spinner until its mark lands. */
 .live-field{padding:.35rem 0}
 /* Sub-header: athlete name (larger) left-justified, affiliation
    right-justified, underlined. */
 .live-field-sub{display:flex;justify-content:space-between;
   align-items:baseline;gap:.5rem;margin-bottom:.4rem;padding-bottom:.3rem;
   border-bottom:2px solid #2ea043}
 .live-field-sub .lf-name{font-weight:700;font-size:1.4rem}
 .live-field-sub .lf-affil{font-weight:400;color:var(--muted);font-size:1.15rem;
   flex:0 0 auto}
 /* Row 1: four (or five, vertical) blocks, all bordered, on one line. */
 .live-field-row1{display:flex;gap:.4rem;margin-bottom:.4rem;
   flex-wrap:nowrap;overflow-x:auto}
 /* align-items:center (was baseline) - with mixed font sizes in one chip
    (small label + larger mark/value) baseline alignment left the smaller
    text riding high; centering the two vertically reads cleaner (explicit
    user request). */
 .lf-block{background:var(--tint-green);border:1px solid var(--border);border-radius:.3rem;
   padding:.25rem .5rem;display:flex;align-items:center;gap:.3rem;
   min-width:2.5rem;justify-content:center;flex:0 0 auto}
 .lf-lbl{font-size:.75rem;font-weight:700;color:var(--muted-2);text-transform:uppercase}
 .lf-val{font-size:1.15rem;font-weight:700}
 /* Place/Attempt/Wind values run at the same size as their PLACE/
    ATTEMPT/WIND labels (explicit user request). */
 .lf-val-sm{font-size:.75rem}
 /* Field mark block: metric in gold/yellow, imperial in black, both the
    same size. */
 .lf-mark{font-size:1.15rem;font-weight:700;color:var(--amber)}
 .lf-mark .lf-imp{font-weight:700;color:var(--page-text);font-size:1.15rem;margin-left:.4rem}
 /* VComponents (XXX) inline after the imperial in the Row 1 mark block. */
 .lf-mark .lf-vcomp-inline{font-weight:700;color:var(--amber);font-size:1.05rem;
   margin-left:.4rem}
 /* Field "IN PROGRESS" badge stays on a single line (the two-line stack
    is only for the track strip). */
 .lf-badge{max-width:none;white-space:nowrap}
 .lf-vcomp{font-size:1.05rem;font-weight:700;color:var(--amber)}
 /* Row 2: six equal-width attempt blocks on one line, each bordered. */
 .live-field-row2{display:grid;grid-template-columns:repeat(6,1fr);
   gap:.25rem;margin-bottom:.4rem}
 /* min-height/padding up ~10% (explicit user request) - Metric/
    Imperial/Wind are now three stacked lines (horizontal field events)
    instead of two, so the box needs the extra room. */
 .lf-att{background:var(--stripe);border:1px solid var(--border);border-radius:.3rem;
   padding:.33rem 0;text-align:center;font-size:1rem;font-weight:600;
   min-height:1.76rem;overflow:hidden;text-overflow:ellipsis}
 .lf-att.lf-best{background:#e6f4ea;border-color:#2ea043;color:#137333}
 .lf-att.lf-current{background:var(--tint-yellow);border-color:#f0c000}
 /* Horizontal field events: Metric (top), Imperial (middle), Wind if
    present (bottom) - three separate lines now (explicit user request;
    was Metric+Imperial sharing one line with Wind below). Vertical
    (high jump/pole vault) is untouched - .lf-att-l1 there still pairs
    height+imperial on one line, .lf-att-vres is its own second line. */
 .lf-att-l1{display:block;white-space:nowrap}
 .lf-att-mark{font-size:.85rem;font-weight:700}
 .lf-att-imp{font-size:.68rem;font-weight:400;color:var(--page-text)}
 /* Horizontal only: Imperial is now a DIRECT child of .lf-att (a sibling
    of the Metric/Wind lines), so this won't match vertical's version,
    which stays nested one level deeper inside .lf-att-l1 (height+
    imperial sharing one line there is untouched, per explicit user
    scope - only horizontal's layout was asked to change). */
 .lf-att>.lf-att-imp{display:block;margin-top:.05rem}
 /* Vertical only (explicit user request) - Imperial nested inside
    .lf-att-l1 (height+imperial sharing one line) is the vertical
    high-jump/pole-vault case specifically, distinct from horizontal's
    direct-child version above which stays at the base .68rem. */
 .lf-att-l1 .lf-att-imp{font-size:.78rem}
 .lf-att-wind{display:block;font-size:.68rem;font-weight:400;color:var(--amber)}
 /* Vertical: the O/X clearance result under each height. */
 .lf-att-vres{display:block;font-size:.78rem;font-weight:700;color:var(--success);
   letter-spacing:.05em}
 /* Bouncing ball flagging the current (live) attempt in the series -
    inline, on the same line as the O/X result. */
 .lf-att-ball{display:inline-block;margin-left:.3rem;vertical-align:middle}
 /* Any foul ("X") shows red. */
 .lf-foul{color:#c00;font-weight:700}
 .live-field-next{font-weight:700;color:var(--amber);font-size:1.05rem}
 /* In-progress attempt spinner (blinking ball). */
 .lf-spin{display:inline-block;width:.8rem;height:.8rem;border-radius:50%;
   background:var(--accent);animation:lf-blink 1s ease-in-out infinite}
 @keyframes lf-blink{0%,100%{opacity:.25}50%{opacity:1}}
 /* Matches the row's own 12px now (explicit user request) - was a fixed
    1rem regardless of row size. */
 .live-strip-team{color:var(--muted);font-size:12px}
 /* DNS/DQ/FS/ADV/SCR/... status marks in the strip - red and bold so
    they read as a mark, not a time. */
 .live-strip-status{color:#c00;font-weight:700}
 /* Field (.lf-badge): "IN PROGRESS" deliberately wraps to two stacked
    lines (IN over PROGRESS) - the max-width forces the break, per
    explicit request. Track overrides this below (one line instead). */
 /* Amber #FFF3C4 with black text (explicit user request) - the same
    "still provisional" amber the In Progress status badges and bubble
    rows use elsewhere, replacing the old green/white. */
 .live-strip-badge{background:#FFF3C4;color:#000;
   border:1px solid #E0D5AC;font-size:.5rem;
   font-weight:700;padding:.12rem .4rem;border-radius:.4rem;
   text-transform:uppercase;letter-spacing:.03em;flex:0 0 auto;
   max-width:3.2rem;text-align:center;line-height:1.25}
 /* Track only (larger screens - the .lf-badge field variant above is
    untouched): no max-width/nowrap so "IN PROGRESS" fits on a single
    line. Higher specificity than the base rules above (two classes/an
    adjacent-sibling selector vs one class) so it wins regardless of
    source order. (No longer touches .live-strip-title's font-size - that
    would re-inflate it past the 14px set above; sizing is fully owned by
    the base .live-strip-title rule now.) */
 @media (min-width:1025px){
   .live-strip-badge:not(.lf-badge){font-size:calc(.5rem - 1pt);
     max-width:none;white-space:nowrap}
 }
 /* Viewer-controlled hide/show for the inline Live View strip (mobile only
    - see below): the x in the strip header hides it, a small "Show Live
    View" button takes its place to bring it back. State lives on <body> +
    localStorage so it survives both SSE fragment swaps and page loads. */
 .lv-hide-btn{background:transparent;border:1px solid #c00;color:#c00;
   border-radius:.25rem;font-size:1rem;cursor:pointer;flex:0 0 auto;
   line-height:1;padding:.05rem .3rem}
 .lv-hide-btn:hover{background:#c00;color:#fff}
 /* When hidden, the way back in is a fixed edge tab styled like the old
    Jump-to-Event tab (same shape/placement idiom, green so it reads as
    Live View). Mobile only - see the large-screen rule below. */
 .lv-show-tab{display:none;position:fixed;top:83%;right:0;z-index:16;
   background:#2ea043;color:#fff;border:none;
   border-radius:.4rem 0 0 .4rem;padding:.9rem .5rem;cursor:pointer;
   font-size:1rem;font-weight:600;letter-spacing:.05em;
   box-shadow:-2px 0 6px rgba(0,0,0,.25);
   writing-mode:vertical-rl}
 body.lv-hidden .live-strip{display:none}
 body.lv-hidden .lv-show-tab{display:block}
 /* Larger screens: Live View lives ONLY in the drawer (nav bar's More ->
    Live View) - no more inline strip beside/above the compiled results.
    Placed AFTER the lv-hidden rules so it wins the equal-specificity tie
    regardless of the inline hide/show toggle state. Scoped to the ORIGINAL
    strip (a direct child of .live-strip-anchor, in the results content)
    so the cloned copy inside the Live View drawer (.side-panel-body) is
    unaffected. */
 @media (min-width:1025px){
   .live-strip-anchor>.live-strip,.live-strip-anchor>.lv-show-tab{
     display:none}
 }
 /* Lynx "bug" - broadcast-style branding badge shown next to RESULTS on
    the Live View strip once the winner's time is posted. */
 .lynx-badge{background:#55009C;color:#fff;font-size:.7rem;font-weight:700;
   letter-spacing:.06em;padding:.05rem .4rem;border-radius:.35rem;
   vertical-align:middle;margin-left:.4rem}
 sup{color:var(--muted);font-size:.7em}
 a.openStats{color:var(--page-text);text-decoration:none;border-bottom:1px dashed var(--accent)}
 a.openStats:hover{color:var(--accent)}
 #stats-modal{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:20;
   display:flex;align-items:center;justify-content:center}
 #stats-modal[hidden]{display:none}
 .stats-modal-box{background:var(--panel-bg);color:var(--page-text);
   border-radius:.5rem;min-width:260px;
   max-width:90vw;box-shadow:0 4px 20px rgba(0,0,0,.25)}
 .stats-modal-head{display:flex;justify-content:space-between;
   align-items:flex-start;padding:.7rem 1rem;border-bottom:1px solid #eee;
   font-weight:600}
 .stats-modal-head button{border:none;background:none;font-size:1.2rem;
   cursor:pointer;line-height:1}
 /* Athlete name on top (prominent), team name below it (smaller/muted) -
    explicit user request, was one plain "Name TEAM" line. */
 #stats-modal-title{display:flex;flex-direction:column;gap:.1rem}
 .stats-modal-title-name{font-size:1.02rem;font-weight:700}
 .stats-modal-title-team{font-size:.78rem;font-weight:400;
   color:var(--muted)}
 .stats-modal-body{padding:1rem;text-align:center}
 .stats-modal-body img{max-height:38px}
 /* Split "Larger view" modal - a centered box that fits any screen
    (near-full on mobile, comfortable on desktop) with the split table at a
    readable size and its own scroll. */
 #split-modal,#chart-modal,#field-modal,#photo-modal,
 #standings-modal,#about-modal,#privacy-modal,#status-codes-modal{
   position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:21;
   display:flex;align-items:center;justify-content:center;padding:1rem}
 #split-modal[hidden],#chart-modal[hidden],#field-modal[hidden],
 #photo-modal[hidden],#standings-modal[hidden],
 #about-modal[hidden],#privacy-modal[hidden],
 #status-codes-modal[hidden]{display:none}
 #status-codes-modal .split-modal-box{max-width:30rem}
 .status-code-row{display:flex;align-items:baseline;gap:.6rem;
   margin:0 0 .5rem}
 .status-code-row .badge{flex:0 0 auto;min-width:6.5rem;text-align:center}
 .status-code-row span:last-child{font-size:.85rem;color:var(--muted)}
 .about-link{color:#000;text-decoration:underline;
   text-decoration-style:dotted;cursor:pointer}
 /* Pure black is invisible against the dark theme's footer background -
    swap to a light color there so the link stays visible either way. */
 body.dark .about-link{color:#e8e8e8}
 /* The shared modal box defaults to max-width:900px (sized for wide result
    tables) - too wide for a short text blurb, so narrow it here. */
 #about-modal .split-modal-box{max-width:26rem}
 /* Privacy policy modal: same width as About, for visual consistency
    between the footer's two popups (explicit user request) - the ID-
    scoped selector (matching #about-modal's own override below) beats
    .split-modal-box's 900px default on specificity, not source order,
    so it can't be undone by reordering the block. The longer policy
    text scrolls instead, via .split-modal-body's shared overflow:auto/
    90vh cap. */
 #privacy-modal .split-modal-box{max-width:26rem}
 #privacy-modal .split-modal-body h3{margin:1.1rem 0 .4rem;
   font-size:.95rem}
 #privacy-modal .split-modal-body h3:first-child{margin-top:0}
 #privacy-modal .split-modal-body ul{margin:.3rem 0 .85rem;
   padding-left:1.3rem}
 #privacy-modal .split-modal-body li{margin:.15rem 0}
 .about-modal-body{padding:1rem 1.25rem 1.25rem;
   font-size:.92rem;line-height:1.5}
 .about-modal-body a{color:var(--brand)}
 body.dark .about-modal-body a{color:#7ecbff}
 .about-modal-body p{margin:0 0 .85rem}
 .about-modal-body p:last-child{margin-bottom:0}
 .about-copyright{font-size:.8rem;color:var(--muted)}
 /* Chart modal body: fit the projected image to the box, keep its scroll. */
 #chart-modal-body{padding:.5rem;overflow:auto}
 #chart-modal-body img{display:block;max-width:100%;height:auto;border-radius:.25rem}
 /* Photo finish modal: image centered in the body, filename sitting under
    it with a rule above so it reads as a footer per explicit user request,
    without a fourth fetch/DOM slot beyond header/body already shared with
    the other modals. */
 .pf-modal-body{text-align:center}
 .pf-modal-body img{display:block;max-width:100%;height:auto;
   border-radius:.25rem;margin:0 auto}
 .pf-filename{margin-top:.6rem;padding-top:.5rem;border-top:1px solid var(--border);
   font-size:.78rem;color:var(--muted-2);font-family:ui-monospace,monospace}
 .pf-missing,.pf-loading{color:var(--muted);font-size:.9rem;padding:1.5rem 1rem}
 /* By-Heat camera button - same look as "Display Splits" (.disc-toggle)
    rather than a bespoke style, so it reads as one more per-heat
    disclosure affordance, not a different kind of control. */
 .pf-btn{display:inline-flex;align-items:center;gap:.35rem;background:none;
   border:none;cursor:pointer;font-size:.85rem;font-weight:600;color:var(--heading);
   padding:.2rem .4rem}
 .pf-btn:hover{text-decoration:underline}
 .split-modal-box{background:var(--panel-bg);color:var(--page-text);
   border-radius:.5rem;width:100%;max-width:900px;max-height:90vh;
   display:flex;flex-direction:column;box-shadow:0 4px 24px rgba(0,0,0,.35)}
 .split-modal-head{display:flex;justify-content:space-between;
   align-items:center;padding:.6rem 1rem;background:var(--brand);color:#fff;
   border-radius:.5rem .5rem 0 0;font-weight:600}
 .split-modal-head button{border:none;background:none;color:#fff;
   font-size:1.3rem;cursor:pointer;line-height:1}
 .split-modal-body{padding:.5rem;overflow:auto}
 .split-modal-body table{font-size:.78rem;border-left:3px solid var(--brand)}
 .split-modal-body td,.split-modal-body th{padding:.12rem .3rem}
 /* Site header: logo (also a link back to the event index) on the
    left, meet name/location/date center, 6 nav links right - shown on
    every page via the shared _PAGE template. Wraps to a stacked layout
    on narrow/mobile screens rather than squeezing everything onto one
    unreadable line. */
 /* Frozen navbar - stays put at the top of the viewport while the rest
    of the page scrolls underneath it, per explicit user request. Below
    the stats-modal overlay's z-index (20) so the modal still shows
    above it when open. */
 /* top:0 (was .5rem) - explicit user request: pinned flush against the
    viewport's top edge, no sliver of scrolled-past page content visible
    above it. */
 .site-header{display:flex;align-items:center;flex-wrap:wrap;gap:.75rem 1.5rem;
   background:var(--brand);color:#fff;padding:.75rem 1rem;border-radius:.4rem;
   margin-bottom:.75rem;position:sticky;top:0;z-index:15}
 .site-header .meet-logo-link{flex:0 0 auto;line-height:0}
 .site-header .meet-logo{height:48px;width:auto}
 .site-header .meet-info{flex:1 1 0%;min-width:180px}
 .site-header .meet-name{font-size:1.3rem;font-weight:700;line-height:1.2;color:#fff}
 .site-header .meet-sub{color:#c8d3e3;font-size:.85rem}
 /* Settings, pulled out of the button bar into the header's own top row
    (explicit user request - phones especially were spending a whole equal-
    width slot in the bar on a button that's used rarely). flex:1 1 auto on
    .meet-info acts as the spacer that pushes this to the row's right edge.
    Icon+label at every width down to 700px, where the label is dropped and
    the button shrinks to an icon-only circle. */
 .mpb-settings-corner{flex:0 0 auto}
 .mpb-settings-corner.mpb-dd{position:relative}
 .mpb-settings-corner>summary{display:flex;align-items:center;
   justify-content:center;gap:.3rem;list-style:none;cursor:pointer;
   background:rgba(255,255,255,.12);border-radius:0;padding:.4rem .55rem;
   color:#fff;font-size:.8rem;font-weight:600;white-space:nowrap}
 .mpb-settings-corner>summary::-webkit-details-marker{display:none}
 .mpb-settings-corner>summary:hover,
 .mpb-settings-corner[open]>summary{background:rgba(255,255,255,.22)}
 .mpb-settings-corner svg{width:18px;height:18px}
 .mpb-settings-corner .mpb-menu{position:absolute;top:100%;right:0;left:auto;
   margin-top:.3rem;z-index:22;background:var(--brand);
   border:1px solid rgba(255,255,255,.2);border-radius:.35rem;padding:.3rem 0;
   min-width:12.5rem;max-width:92vw;box-shadow:0 6px 18px rgba(0,0,0,.4)}
 @media (max-width:700px){
   .mpb-settings-corner>summary{padding:.42rem;border-radius:0}
   .mpb-settings-corner>summary span{display:none}
 }
 /* Universal nav bar: replaces both the old hamburger-collapsed text nav
    and the fixed right-edge drawer tabs (Events/Athletes/Scores/Live/
    Protest) - a single row of icon+label buttons/dropdowns, shown at every
    screen size, pinned to the bottom of the (sticky) header. Class names
    keep the "mobile"/"mpb" prefix from when this was mobile-only. */
 .mobile-panelbar{display:flex;flex-wrap:nowrap;width:100%;
   margin-top:0;padding-top:.14rem;position:relative;
   border-top:1px solid rgba(255,255,255,.18)}
 /* Every bar item (link, button, dropdown summary) looks the same. The
    font-size here is the PHONE size (bumped 15% from .58rem per explicit
    user request) - larger screens override it in the min-width:701px rule
    below, so this value only ever lands on mobile. */
 .mobile-panelbar>a,.mobile-panelbar>button,
 .mobile-panelbar>details>summary{flex:1 1 0;min-width:0;box-sizing:border-box;
   display:flex;flex-direction:column;align-items:center;justify-content:center;
   gap:.05rem;background:none;border:none;
   border-right:1px solid rgba(255,255,255,.14);color:#fff;font-size:.667rem;
   font-weight:600;line-height:1.05;gap:.12rem;padding:.28rem .1rem;
   cursor:pointer;text-decoration:none;list-style:none;white-space:nowrap}
 .mobile-panelbar>details{flex:1 1 0;min-width:0;display:flex}
 .mobile-panelbar>details:last-child>summary,
 .mobile-panelbar>a:last-child,.mobile-panelbar>button:last-child{
   border-right:none}
 .mobile-panelbar svg{width:18px;height:18px}
 /* Larger screens have plenty of spare width in the header bar - the icons
    stay the same size, but the label text (illegibly small at the phone-
    tuned .58rem) can grow to something actually readable. */
 @media (min-width:701px){
   .mobile-panelbar>a,.mobile-panelbar>button,
   .mobile-panelbar>details>summary{font-size:.8rem;gap:.25rem;
     padding:.4rem .3rem}
 }
 .mobile-panelbar>details>summary::-webkit-details-marker{display:none}
 .mobile-panelbar>*:active,
 .mobile-panelbar>details[open]>summary{background:rgba(255,255,255,.16)}
 body.no-protest .mpb-protest{display:none}
 /* Dropdown popovers drop below the bar. */
 .mpb-dd{position:static}
 .mpb-menu{position:absolute;top:100%;left:.25rem;z-index:22;
   background:var(--brand);border:1px solid rgba(255,255,255,.2);
   border-radius:.35rem;padding:.3rem 0;min-width:12.5rem;max-width:92vw;
   box-shadow:0 6px 18px rgba(0,0,0,.4)}
 .mpb-dd-right .mpb-menu{left:auto;right:.25rem}
 /* Scores is the one bar-anchored dropdown narrow enough to hang its own
    menu directly below itself even on phones (explicit user request) -
    every other mobile dropdown still resolves against .mobile-panelbar per
    the comment below. */
 .mpb-scores{position:relative}
 .mpb-scores .mpb-menu{left:0}
 /* Non-mobile: anchor each dropdown under its OWN button. .mpb-dd is
    position:static above, so the menus resolve against .mobile-panelbar
    (the nearest positioned ancestor) and every one of them opened at the
    bar's far left or far right edge instead of below the button that opened
    it (explicit user request). Making the <details> the containing block
    puts the menu directly under its own summary; the -right variant still
    aligns by its right edge so the last buttons in the bar can't run off
    screen. Declared after the rules it overrides (same specificity, so
    source order decides). Mobile keeps the bar-anchored placement - its
    buttons are too narrow to hang a 12.5rem menu off. */
 @media (min-width:701px){
   .mpb-dd{position:relative}
   .mpb-menu{left:0}
   .mpb-dd-right .mpb-menu{left:auto;right:0}
 }
 /* Menu items: icon + label row. */
 .mpb-mi{display:flex;align-items:center;gap:.6rem;width:100%;
   box-sizing:border-box;background:none;border:none;color:#fff;
   text-decoration:none;padding:.5rem .9rem;font-size:.9rem;font-weight:500;
   cursor:pointer;white-space:nowrap;text-align:left}
 .mpb-mi:hover{background:#16386a}
 .mpb-mi svg{width:18px;height:18px;flex:0 0 auto}
 /* Divider above the custom link (More menu's last item, explicit user
    request) - same idea as Settings' .disp-row-sep, coloured to match this
    dropdown's own navy/white palette instead of Settings' blue-toned one. */
 .mpb-mi-sep{border-top:1px solid rgba(255,255,255,.2);margin-top:.2rem;
   padding-top:.7rem}
 /* GPS Tracker, greyed out when [gps]enabled=off (explicit user request) -
    visible so operators know the feature exists, but inert: dimmed,
    no pointer cursor, no hover highlight. */
 .mpb-mi-disabled{opacity:.4;cursor:default}
 .mpb-mi-disabled:hover{background:none}
 /* Collapse chevron under the bar; rotates to point up when the bar is
    showing (tap to hide) and down when hidden (tap to show) - available at
    every screen size to reclaim vertical space while reading results. */
 .mpb-collapse{display:flex;align-items:center;justify-content:center;
   width:100%;background:rgba(255,255,255,.06);border:none;
   border-top:1px solid rgba(255,255,255,.15);color:#cdd8ea;cursor:pointer;
   padding:.02rem;line-height:0}
 .mpb-collapse svg{width:16px;height:16px;transform:rotate(180deg)}
 body.mpbar-collapsed .mobile-panelbar{display:none}
 body.mpbar-collapsed .mpb-collapse svg{transform:rotate(0deg)}
 /* Desktop-only menu items (Reports, Scores dropdowns, Live View button)
    hidden on mobile - their items instead appear inside More/Settings (see
    .mpb-mobile-only below and .disp-row-live above). */
 @media (max-width:700px){
   .mobile-panelbar>details.mpb-dd-desktop,
   .mobile-panelbar>.mpb-btn-desktop{display:none}
 }
 /* Mobile-only menu items (Reports/Scores rows folded into the More menu)
    hidden on desktop, where they already have their own dropdowns. A plain
    display:block here would clobber .mpb-mi's icon+label flex row, so the
    .mpb-mi combo keeps flex. */
 .mpb-mobile-only{display:none}
 @media (max-width:700px){
   .mpb-mobile-only{display:block}
   .mpb-mi.mpb-mobile-only{display:flex}
 }
 /* Display-settings rows (Settings dropdown, .mpb-menu container). Control
    is a plain checkbox on wide screens; under the mobile breakpoint it
    restyles into a true on/off switch (checkbox with appearance:none + a
    sliding knob). */
 .disp-head{font-size:.68rem;letter-spacing:.06em;text-transform:uppercase;
   color:#8fb2e0;padding:.3rem .9rem .35rem}
 .disp-row{display:flex;align-items:center;justify-content:space-between;
   gap:12px;padding:.4rem .9rem;font-size:.9rem;color:#fff;cursor:pointer;
   white-space:nowrap}
 .disp-row:hover{background:#16386a}
 .disp-row-sep{border-top:1px solid #2b5590;margin-top:.2rem;padding-top:.5rem}
 /* Live View now has its own Ribbon button on wide screens (mpb-btn-desktop
    below) - the Settings row is mobile-only so it isn't offered twice. */
 @media (min-width:701px){ .disp-row-live{display:none} }
 /* Scores/Athletes ribbon swap on phones (explicit user request): the Scores
    dropdown (Team Scores + Projected Scores) shows at every width now; on
    phones there isn't room for it alongside Athletes too, so Athletes moves
    into More there (mpb-btn-desktop on the Athletes button). */
 .disp-row input[type=checkbox]{width:16px;height:16px;accent-color:#3a6ea5;
   cursor:pointer;flex:0 0 auto;margin:0}
 @media (max-width:700px){
   .disp-row input[type=checkbox]{appearance:none;-webkit-appearance:none;
     width:40px;height:22px;border-radius:99px;background:#5b6f92;
     position:relative;transition:background .15s}
   .disp-row input[type=checkbox]::after{content:'';position:absolute;
     top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;
     transition:left .15s}
   .disp-row input[type=checkbox]:checked{background:#3a6ea5}
   .disp-row input[type=checkbox]:checked::after{left:20px}
 }
 /* Viewer display toggles applied via a <body> class (survives SSE
    swaps). Compact trims row height; no-logos hides the team-logo imgs. */
 body.compact td,body.compact th{padding-top:.01rem;padding-bottom:.01rem;
   line-height:1.05}
 body.no-logos img.team-logo{display:none}
/* "Show Comp#" display setting (explicit user request, XC only). */
 body.no-compnum .xc-comp-pill{display:none}
 /* Protest hides with the "Show protest" display setting - the drawer
    itself, since there's no edge tab any more (opened from the nav bar's
    More menu instead, .mpb-protest handles that button's visibility). */
 body.no-protest #protest-panel{display:none}
 /* The mobile-only "Show Live View" edge tab (see .lv-show-tab above)
    shrinks on phones so it doesn't overwhelm a short viewport. */
 @media (max-width:700px){
   .lv-show-tab{font-size:.72rem;padding:.5rem .28rem;letter-spacing:.02em}
 }
 @media (max-width:400px){
   .lv-show-tab{font-size:.62rem;padding:.4rem .22rem}
 }
 /* Protest board cards + live countdown. */
 .protest-head{font-size:.8rem;color:var(--muted);margin:.2rem 0 .6rem}
 .protest-empty{color:var(--muted-2);font-size:.9rem}
 .protest-card{border:1px solid var(--border);border-left:4px solid #b45309;
   border-radius:.3rem;padding:.5rem .7rem;margin:0 0 .6rem}
 .protest-card.protest-closing{border-left-color:#c00;background:var(--tint-red)}
 .protest-card.protest-closed{border-left-color:var(--muted-2);opacity:.7}
 .protest-ev{display:flex;justify-content:space-between;align-items:baseline;
   gap:.5rem;font-weight:600}
 .protest-ev a{color:var(--heading);text-decoration:none}
 .protest-round{font-size:.78rem;color:var(--muted-2);font-weight:400}
 .protest-row{display:flex;justify-content:space-between;align-items:center;
   margin-top:.35rem}
 .protest-clock{font-size:1.15rem;font-weight:700;color:var(--amber);
   font-variant-numeric:tabular-nums}
 .protest-card.protest-closing .protest-clock{color:#c00}
 .protest-card.protest-closed .protest-clock{color:var(--muted-2);font-size:.9rem}
 .protest-since{font-size:.78rem;color:var(--muted-2)}
 .protest-text{font-size:.82rem;color:var(--muted);margin-top:.35rem;
   padding-top:.35rem;border-top:1px solid var(--border)}
 /* Slide-out drawer shared by Athletes and Team Scores (same look as the
    Events jump drawer). */
 .side-panel{position:fixed;top:0;right:0;width:414px;max-width:90vw;
   height:100%;background:var(--panel-bg);z-index:17;
   box-shadow:-4px 0 16px rgba(0,0,0,.3);transition:transform .25s ease;
   overflow-y:auto;transform:translateX(100%)}
 .side-panel.open{transform:translateX(0)}
 .side-panel-head{display:flex;justify-content:space-between;align-items:center;
   background:var(--brand);color:#fff;font-weight:700;padding:.75rem 1rem;
   position:sticky;top:0;z-index:1}
 .side-panel-head button{background:none;border:none;color:#fff;
   font-size:1.3rem;cursor:pointer;line-height:1}
 /* Groups the Close button with any drawer-specific head button (so far
    just GPS Tracker's Expand toggle) as ONE flex item on the right, so
    .side-panel-head's justify-content:space-between still reads as
    "label | controls" with exactly two items, not the label drifting
    into the middle once a second button exists. */
 .side-panel-head-btns{display:flex;align-items:center;gap:.5rem}
 /* GPS Tracker expand/shrink (explicit user request, tablet+ only - the
    drawer is already capped at 90vw on phones, nothing to gain by
    widening it there, see the media query below). Plain icon button
    matching the Close button's own sizing/color; toggleGpsExpand
    (app.js) swaps both the #gps-panel.expanded class and this button's
    own icon/aria-label between expand/shrink. */
 .gps-expand-btn{display:none;font-size:1rem}
 .gps-expand-btn svg{width:1.15rem;height:1.15rem;display:block}
 @media (min-width:768px){
   .gps-expand-btn{display:block}
   /* Expanded state: a big chunk of the viewport instead of the
      standard 414px drawer width - explicit user request, "expand...
      then shrink back to regular size". Transition matches the
      drawer's own open/close slide (.side-panel's transform) so
      widening reads as one continuous motion, not a jump cut. */
   #gps-panel{transition:transform .25s ease,width .25s ease}
   #gps-panel.expanded{width:min(1100px,90vw)}
   #gps-panel.expanded #gps-map{height:75vh}
 }
 .side-panel-body{padding:.5rem 1rem;font-size:.88rem}
 /* Live View drawer is 25% wider than the standard side-panel (414 * 1.25)
    for both Track and Field - a live strip needs more room than a plain
    list. Idle message shown when the event isn't in progress / no event is
    open. Mobile has no drawer at all - the strip is inline/stacked instead
    (liveNavClick toggles it there); belt-and-suspenders hide in case
    anything ever tries to open it below the breakpoint. */
 #live-panel{width:518px}
 @media (max-width:1024px){ #live-panel{display:none} }
 .lv-idle{color:var(--muted);text-align:center;padding:1.5rem 1rem;font-size:.95rem}
 .side-panel-body .live-strip{position:static;width:auto;margin:0;
   max-width:none}
 .side-panel-body h2{font-size:1.1rem;margin:.5rem 0}
 .side-panel-body table{font-size:.88rem}
 .side-panel-body h3{font-size:.95rem}
 /* GPS Tracker drawer (explicit user request, XC-only) - Leaflet needs an
    explicit pixel height on its container to render into; display:none
    until a fix exists (initGpsMap toggles it) so an empty gray box never
    flashes before the first fix arrives. */
 .gps-map-wrap{position:relative}
 #gps-map{display:none;height:60vh;min-height:280px;border-radius:6px}
 /* Start/Finish marker dots (explicit user request) - plain colored
    circles, set via inline style per-marker in app.js's _dotIcon. */
 .gps-dot-icon span{display:block;width:14px;height:14px;border-radius:50%;
   border:2px solid #fff;box-shadow:0 0 2px rgba(0,0,0,.6)}
 /* Right-click-to-copy confirmation (explicit user request) - a small
    toast over the map, same idea as a snackbar; fades in/out rather than
    a native alert() so it doesn't block the map interaction. */
 .gps-copied-toast{position:absolute;left:50%;bottom:1.2rem;
   transform:translateX(-50%) translateY(6px);background:rgba(0,0,0,.8);
   color:#fff;padding:.4rem .8rem;border-radius:4px;font-size:.82rem;
   opacity:0;pointer-events:none;transition:opacity .15s,transform .15s;
   z-index:1000}
 .gps-copied-toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
 /* Elevation + Static/Active readout below the map (explicit user
    request) - hidden until the first fix of this open-session arrives
    (teardownGpsMap re-hides it on close, same fresh-per-open rule the
    path trail follows). Plain rectangular chips, no labels - the value
    alone (explicit user request, dropped the "Elevation:"/text-only
    Static-Active in favor of a compact button look). Color-coded dot on
    the status chip: gray/static, green/moving. */
 .gps-readout{display:flex;justify-content:space-between;align-items:center;
   margin-top:.6rem}
 .gps-chip{display:inline-flex;align-items:center;gap:.35rem;
   background:var(--stripe);border-radius:4px;
   padding:.35rem .7rem;font-size:.85rem;font-weight:600;color:var(--muted)}
 .gps-status-dot{width:.55rem;height:.55rem;border-radius:50%}
 .gps-status-static .gps-status-dot{background:#9e9e9e}
 .gps-status-static{color:#9e9e9e}
 .gps-status-active .gps-status-dot{background:#2e9e44}
 .gps-status-active{color:#2e9e44}
 /* Narrow screens: shrink the meet name/logo to keep the header tidy
    alongside the nav bar underneath it. */
 @media (max-width:1024px){
   .site-header{gap:.2rem 1rem;padding:.55rem 1rem .3rem}
   /* Both bumped ~15% from the 1rem/.72rem they shrank to here - the
      meet name/date were a touch too small to read comfortably on a
      phone (explicit user request). */
   .site-header .meet-name{font-size:1.15rem}
   .site-header .meet-sub{font-size:.83rem}
   .site-header .meet-info{min-width:0}
   .site-header .meet-logo{height:38px}
 }
 /* Footer: venue left, Flash Results credit middle, copyright right -
    same simple 3-part layout on every page. Stacks and centers on
    narrow/mobile screens rather than squeezing 3 columns together. */
 /* Fixed to the bottom of the viewport on every page (content scrolls under
    it); ~half the previous height. body has matching padding-bottom so the
    last rows stay reachable. */
 .site-footer{position:fixed;left:0;right:0;bottom:0;z-index:14;
   display:flex;align-items:center;justify-content:space-between;
   flex-wrap:wrap;gap:.2rem 1.5rem;margin-top:0;padding:.4rem 1.25rem;
   border-top:1px solid var(--border,#e2e2e2);background:var(--page-bg);
   box-shadow:0 -1px 4px rgba(0,0,0,.06);color:var(--muted);
   font-size:.72rem;letter-spacing:.01em}
 /* Larger than mobile: the footer is position:fixed, so it's sized against
    the viewport and ignores .page-shell despite being its child - left/right
    0 made it run edge-to-edge while the navbar stopped at the shell. The
    navbar's outer width is exactly .page-shell's max-width (the shell is
    content-box, so its 1rem side padding sits outside that number), so match
    it here: border-box (the footer's own padding differs from the shell's
    and shrinks again on small screens - this keeps the number below the
    final rendered width regardless), capped at the same max-width, and
    centred with auto margins. Per explicit user request; mobile keeps the
    edge-to-edge bar. */
 @media (min-width:701px){
   .site-footer{box-sizing:border-box;width:calc(100% - 2rem);
     max-width:1300px;margin:0 auto}
 }
 @media (min-width:1600px){
   .site-footer{max-width:1860px}
 }
 .footer-venue{font-weight:600;color:var(--page-text);display:flex;align-items:center;
   gap:.4rem}
 .footer-venue svg{width:.95rem;height:.95rem;flex:0 0 auto;
   color:var(--accent-2);display:block}
 .footer-credit{text-transform:uppercase;letter-spacing:.08em;
   font-size:.68rem;color:var(--muted-2)}
 .footer-credit a{color:var(--heading);text-decoration:none;font-weight:600;
   transition:color .15s ease}
 .footer-credit a:hover{color:var(--accent-2);text-decoration:underline}
 .footer-copyright{color:var(--muted-2);font-variant-numeric:tabular-nums}
 .footer-privacy{color:var(--muted-2);text-decoration:none;margin-left:.5rem;
   padding-left:.5rem;border-left:1px solid var(--muted-2)}
 .footer-privacy:hover{color:var(--accent-2);text-decoration:underline}
 @media (max-width:1024px){
   /* Mobile footer: drop the "Timing and Scoring by" prefix (keep the
      Flash Results Inc. link), and match the copyright's size to it. */
   .footer-credit-prefix{display:none}
   .footer-copyright{font-size:.68rem}
 }
 @media (max-width:500px){
   /* Keep the fixed footer compact (wrap, don't stack into a tall column). */
   .site-footer{justify-content:center;text-align:center;gap:.1rem .8rem;
     font-size:.62rem;padding:.3rem .6rem}
   .footer-venue{justify-content:center}
   body{padding-bottom:3.6rem}
 }
 /* On short viewports (wide-but-short desktop windows, tablet landscape)
    the mobile-only Live View show-tab shrinks/tightens too, independent
    of width. Placed after its width-breakpoint rules so these win the
    equal-specificity tie. */
 @media (max-height:820px){
   .lv-show-tab{font-size:.82rem;padding:.55rem .32rem;letter-spacing:.02em}
 }
 @media (max-height:660px){
   .lv-show-tab{font-size:.7rem;padding:.42rem .26rem}
 }
 /* Same width as the Athletes drawer (.side-panel) - explicit user
    request; these two sit side by side in the nav bar and read as a pair. */
 .event-jump-panel{position:fixed;top:0;right:0;width:414px;
   max-width:90vw;height:100%;background:var(--panel-bg);z-index:17;
   box-shadow:-4px 0 16px rgba(0,0,0,.3);
   transition:transform .25s ease;overflow-y:auto;
   transform:translateX(100%)}
 .event-jump-panel.open{transform:translateX(0)}
 /* Freeze the drawer head + gender filter together at the top while the
    event list scrolls under them (wrapping both avoids a header-height
    magic number for the filter's sticky offset). */
 .event-jump-sticky{position:sticky;top:0;z-index:2;
   background:var(--panel-bg)}
 .event-jump-head{display:flex;justify-content:space-between;
   align-items:center;background:var(--brand);color:#fff;font-weight:700;
   padding:.75rem 1rem}
 .event-jump-head button{background:none;border:none;color:#fff;
   font-size:1.3rem;cursor:pointer;line-height:1}
 .event-jump-panel ul{list-style:none;margin:0;padding:.3rem 0;font-size:.8rem}
 .event-jump-panel li{padding:.3rem 1rem;border-bottom:1px solid var(--border);
   display:flex;justify-content:space-between;align-items:center;gap:.5rem}
 .event-jump-panel li a{color:var(--heading);text-decoration:none;font-weight:600}
 .event-jump-panel li a small{font-weight:400;color:var(--muted);display:block;
   font-size:.9em}
 .event-jump-panel li a:hover{text-decoration:underline}
 /* Mobile only: event name + day/time/round bumped 1pt each, explicit user
    request - the status badge is untouched. small's font-size is set as an
    absolute value here (not left relative via em) so it's +1pt from its own
    original .72rem (.9em of .8rem), not compounded on top of the link's own
    +1pt. */
 @media (max-width:700px){
   .event-jump-panel li a{font-size:calc(.8rem + 1pt)}
   .event-jump-panel li a small{font-size:calc(.72rem + 1pt)}
 }
 /* Gender filter (All / Men / Women) - a small segmented control pinned
    below the drawer head; filters the list client-side. */
 /* Gender radio filters (Events / Team Scores / Athletes) - a compact
    inline row of All/M/W(/Multi) radios. */
 .ev-filter,.score-filter{display:flex;gap:.8rem;font-size:.85rem;
   padding:.4rem 1rem;flex-wrap:wrap}
 .ev-filter{border-bottom:1px solid #ddd;background:var(--panel-bg)}
 .score-filter{padding:.4rem 0 .6rem}
 .ev-filter label,.score-filter label{display:inline-flex;align-items:center;
   gap:.25rem;cursor:pointer}
 /* Athletes: Exact-search switch and All/M/W gender radios share one row. */
 .ath-controls{display:flex;align-items:center;justify-content:space-between;
   gap:1rem;flex-wrap:wrap;margin:.4rem 0 .5rem}
 .ath-gender{display:inline-flex;gap:.7rem;font-size:.85rem}
 .ath-gender label{display:inline-flex;align-items:center;gap:.2rem;
   cursor:pointer}
 /* Summary Scores / By Event tabs. */
 .score-tabs{display:flex;gap:.3rem;margin:.2rem 0 .5rem}
 .score-tabs button{flex:1;padding:.35rem;font-size:.82rem;font-weight:600;
   color:var(--heading);background:var(--tint-blue);border:1px solid var(--border);
   border-radius:.3rem;cursor:pointer}
 .score-tabs button.on{background:var(--brand);color:#fff;border-color:var(--brand)}
 .score-pane[hidden]{display:none}
 .sbe-count{font-size:.72rem;font-weight:400;color:var(--muted-2)}
 /* "Larger view" link - opens the full /scores page in a new tab
    (explicit user request), not an in-page modal like every other
    "Larger view" button. */
 .scores-larger{margin-left:.9rem;vertical-align:middle}
 /* Single instance now sits in the Gender filter row (explicit user
    request - was one per section before), pushed flush right instead
    of just trailing the last radio. */
 .score-filter .scores-larger{margin-left:auto}
 /* Summary Scores: tighter rows. */
 .scores table td,.scores table th{padding-top:.12rem;padding-bottom:.12rem}
 /* Full /scores page (body.scores-page, set by hideScoresLargerOnScoresPage)
    only - the Summary Scores table/columns are sized to fit the 414px
    drawer they normally live in, which reads as too narrow stretched
    across a full page instead (explicit user follow-up). Widen the
    table itself and give its cells more breathing room; By Event's grid
    is already wide (many event columns) and horizontally scrollable, so
    it's left alone - just bumped up slightly in font size to match. */
 /* Summary Scores only (not By Event, explicit user request) - Women/Men
    side by side instead of stacked, now that there's real page width to
    spend. flex-wrap so a narrow browser (or the gender filter leaving
    just one section visible, which sets style.display='' - still a
    valid flex item, not "none") falls back to a single stacked column
    rather than squeezing two skinny ones. Wider cap than the single-
    column rule below it needs, to actually fit two tables side by side. */
 body.scores-page .score-pane[data-pane='summary'] .scores{
   max-width:1400px;margin:0 auto;display:flex;flex-wrap:wrap;
   gap:0 2.5rem;align-items:flex-start}
 body.scores-page .score-pane[data-pane='summary'] .score-section{
   flex:1 1 480px;min-width:0}
 body.scores-page .scores table{width:100%}
 body.scores-page .scores table td,body.scores-page .scores table th{
   padding:.45rem .8rem;font-size:1rem}
 body.scores-page .sbe-table{font-size:.78rem}
 body.scores-page .sbe-table td,body.scores-page .sbe-table th{
   padding:.25rem .55rem}
 body.scores-page .score-tabs{max-width:1400px;margin:.2rem auto .5rem}
 body.scores-page .score-filter,body.scores-page .team-limit{
   max-width:1400px;margin-left:auto;margin-right:auto}
 /* By Event grid: compact, horizontal scroll with Pl/Team/Tot pinned. */
 .sbe-scroll{overflow-x:auto;max-width:100%}
 .sbe-table{width:auto;font-size:.62rem;border-collapse:collapse}
 .sbe-table td,.sbe-table th{padding:.1rem .3rem;white-space:nowrap;
   text-align:center;border-bottom:1px solid var(--border)}
 .sbe-table thead th{background:var(--brand);color:#fff}
 .sbe-table tbody tr:nth-child(even) td{background:var(--stripe)}
 .sbe-pin{position:sticky;background:var(--page-bg);z-index:2;text-align:left}
 .sbe-table tbody tr:nth-child(even) td.sbe-pin{background:var(--stripe)}
 .sbe-table thead th.sbe-pin{background:var(--brand);z-index:3}
 .sbe-table .team-logo{height:16px;width:16px}
 .sbe-p1{left:0;min-width:1.3rem}
 .sbe-plogo{left:1.3rem;min-width:1.4rem;text-align:center}
 .sbe-p2{left:2.7rem;min-width:2.8rem}
 .sbe-p3{left:5.5rem;min-width:1.8rem;
   box-shadow:2px 0 3px -1px rgba(0,0,0,.2)}
 /* Highlight the Total column (body cells, over the zebra stripe). */
 .sbe-table td.sbe-p3,
 .sbe-table tbody tr:nth-child(even) td.sbe-p3{background:var(--tint-yellow)}
 /* Full /scores page on mobile: logos stay visible at every width
    (explicit user follow-up - a prior pass hid this column below 700px,
    which turned out to be the wrong call for this column specifically;
    it's reverted here). Summary Scores' logo defaults to 24px (the
    site-wide .team-logo size) - shrink it to match By Event's own
    16px (.sbe-table .team-logo above) so it doesn't crowd a narrow
    phone screen; By Event's is already 16px unconditionally. */
 @media (max-width:700px){
   body.scores-page .score-logo-col .team-logo{width:16px;height:16px}
 }
 /* By Event needs more room than Summary - widen the Team Scores drawer
    ~25% while that tab is active (JS toggles .wide on #scores-panel). */
 #scores-panel.wide{width:518px}
 /* The event currently being viewed is highlighted in the drawer (marked
    client-side from the URL) so it's easy to see where you are. */
 .event-jump-panel li.current{background:var(--tint-blue);
   border-left:3px solid var(--accent-2);padding-left:calc(1rem - 3px)}
 .event-jump-panel li.current a{color:var(--heading)}
 #idle-overlay{display:none;position:fixed;inset:0;z-index:4000;
   background:rgba(10,15,30,.82)}
 #idle-overlay.show{display:flex;align-items:center;justify-content:center}
 .idle-card{background:#fff;color:#16213a;border-radius:12px;
   padding:1.6rem 2rem;text-align:center;max-width:20rem;
   box-shadow:0 8px 30px rgba(0,0,0,.45)}
 .idle-card h2{font-size:1.15rem;margin:0 0 .4rem}
 .idle-card p{margin:0 0 1rem;color:#4a5568;font-size:.9rem}
 .idle-card button,.idle-bye button{border:0;cursor:pointer;
   border-radius:8px;padding:.6rem 1.4rem;font:inherit;font-weight:700}
 #idle-yes{background:#16213a;color:#fff;margin-right:.6rem}
 #idle-no{background:#e2e8f0;color:#16213a}
 .idle-bye{max-width:22rem;margin:18vh auto;text-align:center;
   font-family:'Roboto',system-ui,sans-serif;color:#16213a}
 .idle-bye button{background:#16213a;color:#fff;margin-top:1rem}

/* ---- Cross-country course table ---------------------------------- */
/* One row per athlete, one column per timing point along the course
   (render.py's xc_course_fragment). Wide by nature - a 5K with four
   splits is six-plus columns - so the table scrolls horizontally inside
   its own wrapper and the athlete/team column stays pinned, rather than
   the page itself ever scrolling sideways. */
 .xc-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:1rem}
/* Narrowed 15% on wide screens (explicit user request - the table was
   stretching edge-to-edge and reading too wide there); left full-width
   below this breakpoint, where the room is needed for horizontal
   scrolling instead. Scoped to `.ev.xc` (the main event page) only -
   `.xc-wrap` is reused inside the Team Scores DRAWER too, and this
   rule was shrinking that already-narrow table by 15% of the viewport
   width regardless of how narrow the drawer itself already was
   (explicit user report: the drawer's table was too cramped). */
 @media (min-width:900px){ .ev.xc .xc-wrap{max-width:85%} }
 .xc-table{border-collapse:collapse;width:100%;font-size:.92rem}
/* One size down from the athlete results table (explicit user request) -
   the Team Scores drawer is narrower and the split cells now carry a
   longer "SCORE (PLACE)" string per column. */
 .xc-table-teams{font-size:.82rem}
/* Detailed Team Scores: each athlete cell stacks Place/Name/Time
   (legacy's Detailed Scores layout), center-justified per explicit user
   request. */
 .xc-table-extended .xc-ext-cell{text-align:center;font-size:.78rem;
   line-height:1.35;white-space:normal;min-width:6rem}
/* Detailed Team Scores section (explicit user request - was an overlay
   modal, now its own disclosure section inline on the Results page,
   see xc_detailed_team_scores_section/render.py). Same compact sizing
   the old modal used (several rounds of user-requested tuning) - the
   table still scrolls both ways inside it (.xc-wrap), so sizing text
   down doesn't clip anything, just changes how it reads at a glance. */
 .xc-dts-tabs{display:flex;flex-wrap:wrap;gap:.3rem;margin:0 0 .6rem}
 .xc-dts-tab{padding:.3rem .7rem;font-size:.8rem;font-weight:600;
   color:var(--heading);background:var(--tint-blue);border:1px solid var(--border);
   border-radius:0;cursor:pointer}
 .xc-dts-tab.on{background:var(--brand);color:#fff;border-color:var(--brand)}
 .xc-dts .xc-table-teams{font-size:.72rem}
 .xc-dts .xc-table th,.xc-dts .xc-table td{padding:.25rem .45rem}
/* Team Name column (explicit user report: "too wide, taking up too much
   space") - the site-wide .xc-col-athlete rule (11-16rem, sized for the
   full results/team tables) is way more than this section needs since
   every other column here is a compact Place/Name/Time stack. */
 .xc-dts .xc-col-athlete{min-width:6rem;max-width:8rem}
/* Narrower still once scrolled to an abbreviation (explicit user
   request - the whole point of abbreviating is to give the scorer
   columns more room, which a still-full-width column would waste). NOT
   transitioned (explicit user report: an animated width on a sticky
   cell momentarily desyncs its painted box from the table's actual
   column grid mid-transition, letting scrolled-under content bleed
   through at the edge for a frame or two) - the swap is instant, same
   as the text swap it rides with. Class toggled by _xcExtSyncTeamNames
   (app.js) in lockstep with the text
   swap itself. */
 .xc-dts .xc-wrap.xc-abbrev .xc-col-athlete{
   min-width:3rem;max-width:3.5rem}
/* Pl/Logo/Team's sticky widths+offsets are fixed rem values sized for the
   FULL-size table (.xc-col-place/.xc-col-logo/.xc-table-teams .xc-col-
   athlete above) - rem doesn't shrink with a local font-size change, so
   without this override the shrunk-font Logo column drifts out from
   under the still full-width sticky Team column while scrolling
   (explicit user report). Scaled to match the text sizing above. */
 .xc-dts .xc-col-place{width:2.2rem}
 .xc-dts .xc-col-logo{width:2rem}
 .xc-dts .xc-table-teams .xc-col-athlete{left:2.2rem}
/* The <img> itself is a fixed 24px site-wide (.team-logo above) -
   shrinking the TD's width alone doesn't shrink an image that big
   inside it, so the cell's real rendered width stays ~24px+padding
   regardless, which is what was actually causing the drift above (the
   td width override alone was not sufficient). Other narrow contexts
   already override this the same way (.sbe-table/.res-table .team-logo). */
 .xc-dts .team-logo{width:20px;height:20px}
 .xc-dts .xc-table-extended .xc-ext-cell{font-size:.68rem;
   line-height:1.3;min-width:5rem}
 .xc-dts .xc-wrap{padding:0 .5rem}
 .xc-table th,.xc-table td{padding:.3rem .55rem;text-align:center;
   white-space:nowrap;border-bottom:1px solid var(--rule,#dde3ec)}
 .xc-table thead th{background:var(--brand,#0f2c52);color:#fff;
   position:sticky;top:0;z-index:2}
 .xc-table th.xc-stick,.xc-table td.xc-stick{position:sticky;z-index:3}
 .xc-table td.xc-stick{background:var(--page-bg,#fff)}
 .xc-table th.xc-stick{background:var(--brand,#0f2c52);z-index:4}
/* Logo isn't sticky (it deliberately slides away under the frozen
   columns, see the "Results table pins TWO columns" note below), but it
   still needs an opaque background of its own (explicit user report:
   scrolled content briefly visible through it as it passes under the
   sticky Place/Team cells) - same page-bg/zebra pairing xc-stick cells
   already get, just without the sticky positioning itself. */
 .xc-table td.xc-col-logo{background:var(--page-bg,#fff)}
/* Was var(--zebra,...) - --zebra was never actually declared anywhere
   (:root/body.dark only ever defined --stripe, the site-wide zebra-
   stripe variable every OTHER table already uses), so this silently
   fell through to its light-mode fallback color unconditionally,
   dark mode included (explicit user report: "shaded rows turn white
   during dark mode" - #f5f7fa against a near-black dark page reads as
   white). --stripe already carries the correct value for both themes. */
 .xc-table tbody tr:nth-child(even) td{background:var(--stripe,#ececec)}
 .xc-table tbody tr:nth-child(even) td.xc-stick{background:var(--stripe,#ececec)}
 /* No zebra stripe on a row with nothing in it yet (explicit user
    report) - an athlete who hasn't finished (Pl/Pts/TmPl all blank) or
    a team not yet at this point at all (not even NTS) - see
    xc_athlete_row_html/xc_team_row_html's xc-row-nodata class. Same
    specificity as the two rules above (one extra class each side), so
    source order alone isn't reliable - !important makes the override
    unconditional rather than depending on stylesheet ordering surviving
    future edits. */
 .xc-table tbody tr.xc-row-nodata td,
 .xc-table tbody tr.xc-row-nodata td.xc-stick{background:none !important}
/* Results table pins TWO columns (Place/Athlete) - Logo, when shown, is
   deliberately NOT pinned (explicit user request: it should slide away
   under the frozen columns as the table scrolls, not sit frozen as a
   third one). Athlete's left offset is always Place's width regardless
   of whether Logo is shown: at scroll 0, Logo (normal flow) sits
   between them same as any other cell; once scrolled past Logo's own
   width, Athlete's sticky offset takes over and Logo has already
   scrolled out from under it - no separate "no logo" offset needed.
   The Team Scores table now pins TWO columns too (Overall Place, then
   Team - logo+name still one combined cell), same left-offset pattern
   as the results table. */
 .xc-col-place{width:2.6rem}
 .xc-col-logo{width:2.4rem}
 .xc-table-athletes .xc-col-place{left:0}
 .xc-table-teams .xc-col-place{left:0}
 .xc-table-athletes .xc-col-athlete{left:2.6rem}
 .xc-table-teams .xc-col-athlete{left:2.6rem}
/* .xc-table th,.xc-table td above sets text-align:center at (0,2,0)
   specificity - a bare .xc-col-athlete rule at (0,1,0) loses to it
   silently. Matching th/td selectors here so left alignment actually
   wins (explicit user report: name/affiliation weren't left-justified
   despite this rule already existing). */
 .xc-table th.xc-col-athlete,.xc-table td.xc-col-athlete{
   text-align:left;min-width:11rem;max-width:16rem;white-space:normal}
/* Vertical rules after Athlete and Pts (explicit user request) - same
   right-edge-rule idea as the timing-point columns below, so the frozen
   Place/Athlete block and the trailing Pts column each read as their
   own strip against their neighbour. */
 .xc-table th.xc-col-athlete,.xc-table td.xc-col-athlete,
 .xc-table th.xc-col-pts,.xc-table td.xc-col-pts{
   border-right:1px solid var(--rule,#dde3ec)}
/* Light vertical rule down the right edge of every timing-point column
   (splits AND Finish, explicit user request) - applies to both header
   and body cells, and to both tables (xc-splitcol/xc-fin/xc-fin-cell are
   shared by the athlete table and the Team Scores table), so a column of
   data reads as its own strip rather than blending into its neighbour. */
 .xc-table th.xc-splitcol,.xc-table td.xc-splitcol,
 .xc-table th.xc-fin,.xc-table td.xc-fin-cell{
   border-right:1px solid var(--rule,#dde3ec)}
/* Was nth-last-child(2), which silently assumed Finish sits exactly one
   td before the row's end - broke the moment Pts/TmPl were added AFTER
   Finish in the athlete table (and never matched the team table, which
   has no trailing columns at all). An explicit class on the Finish
   cell itself (_xc_cell/xc_team_row_html) is correct regardless of
   what, if anything, follows it. */
 .xc-table td.xc-fin-cell{font-weight:600}
/* Each cell is two rows: time+place on top, arrow/gap-to-that-point's-
   leader underneath - stacking so a place of 9 and a time of 5:56.1
   don't run together and read as "95:56.1". */
 .xc-t{font-variant-numeric:tabular-nums}
 .xc-pl{color:var(--muted,#5a6a82);font-weight:400}
 .xc-gap{display:block;color:var(--muted,#5a6a82);font-size:.8em;
   min-height:1.1em}
 .xc-none{background:repeating-linear-gradient(135deg,transparent,
   transparent 4px,rgba(128,128,128,.07) 4px,rgba(128,128,128,.07) 8px)}
 .xc-up{color:#1a7f37;margin-right:.3rem;font-size:.9em}
 .xc-down{color:#b3261e;margin-right:.3rem;font-size:.9em}
/* Team Scores drawer's own arrow (explicit user request) - one size
   down from the athlete table's, matching .xc-table-teams's own
   smaller base font. */
 .xc-team-arrow{font-size:.85em}
 .xc-nts{color:var(--muted,#5a6a82);font-style:italic}
/* Names the rule behind a "*" (explicit user request) - only rendered
   when a tiebreak actually fired somewhere on the current scoreboard. */
 .xc-tiebreak-note{text-align:center;color:var(--muted,#5a6a82);
   font-size:.78rem;font-style:italic;margin:.6rem 0 0}
/* NCAA head-to-head detail, one line per team in a tied group (explicit
   user request) - small, muted, centered under the tiebreak-rule note
   it belongs to; a blank line separates one group from the next when a
   scoreboard has more than one. */
 .xc-tiebreak-detail{text-align:center;color:var(--muted,#5a6a82);
   font-size:.72rem;margin:.3rem 0 0}
/* "Hide split results" (mirrors track's Display Splits button):
   collapses every non-finish timing column down to just Finish, in
   BOTH tables at once - CSS-only, so a live row-patch never has to
   care whether splits are currently shown. */
 .ev.xc.xc-hide-splits .xc-splitcol{display:none}
/* Mixed Gender races (Admin > Edit Index Page flag): All/Men/Women
   filter, same CSS-only/survives-a-row-patch approach as the split
   toggle above - each row already carries its own data-gender. */
 .ev.xc.xc-filter-m .xc-table-athletes tbody tr:not([data-gender="m"]){display:none}
 .ev.xc.xc-filter-f .xc-table-athletes tbody tr:not([data-gender="f"]){display:none}
 .xc-gender-filter{background:var(--brand,#0f2c52);border:1px solid var(--brand,#0f2c52);
   color:#fff;border-radius:.3rem;padding:.15rem .4rem;font-size:.8rem}
/* Was transparent with a white border/white text, on the assumption
   .sec-head itself carries the navy background the RESULTS bar shows -
   it does NOT (.sec-head is a bare flex container; the navy pill is
   .sec-head .sec-toggle's OWN background, scoped to that one button).
   This button sits on the plain page background as .sec-toggle's
   sibling, so it needs its own visible styling - white text on white
   page background was invisible (explicit user report: "button is not
   there at all"). Matches .sec-toggle's own navy pill look instead, so
   the two read as a pair. */
 /* Same background at rest, hover, active and focus (explicit user
   report: it was picking up an "alternate" darker color once clicked/
   focused and staying there) - only the label text should change
   (Hide/Show splits), never the button's own color. */
 .xc-split-toggle,.xc-split-toggle:hover,.xc-split-toggle:active,
 .xc-split-toggle:focus{background:var(--brand,#0f2c52);
   border:1px solid var(--brand,#0f2c52);color:#fff}
/* Square corners (explicit user request, consistent with the rest of
   XC's chrome). RESULTS/EXTENDED TEAM SCORES are sec-tabs now, not a
   sec-head disclosure (see xc_course_fragment) - .ev.xc .sec-tab below
   covers the tab buttons themselves; outdoor's identical-markup SUMMARY
   RESULTS/HEAT RESULTS/START LIST tabs keep their rounded corners since
   that rule is scoped to .ev.xc only. */
 .ev.xc .sec-tab{border-radius:0}
/* Hide-splits/race-clock/gender-filter controls (explicit user request)
   now sit in their own row at the top of the RESULTS tab pane
   (.xc-results-ctrl) rather than inline in a sec-head bar - RESULTS is
   a tab now, always fully rendered when its pane is visible, so there's
   no "section collapsed" state left to hide them for. */
 .xc-results-ctrl{display:flex;flex-wrap:wrap;align-items:center;
   gap:.5rem;margin:0 0 .6rem}
 .xc-split-toggle{border-radius:0;padding:.15rem .6rem;font-size:.8rem;
   cursor:pointer}
 .xc-clock{font-weight:600;font-variant-numeric:tabular-nums}
/* Scroll aids (explicit user request): a "back to top" button once the
   viewer has scrolled any real distance, and a small floating chip
   reminding them which event they're on once the real heading has
   scrolled out of view - both fixed, both toggled purely via the
   `hidden` attribute (app.js's setupXcScrollAids), no layout impact
   when hidden. Bottom corners, not the top - the site header is
   already sticky at the top, so anything else fixed up there would
   fight it for space instead of sitting cleanly alongside it. */
 .xc-to-top{position:fixed;right:1rem;bottom:1.2rem;z-index:60;
   background:var(--brand,#0f2c52);color:#fff;border:none;
   border-radius:1.4rem;padding:.5rem 1rem;font-size:.85rem;
   cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.3)}
 .xc-to-top:hover{background:var(--brand-2,#1a3d6e)}
 /* Semi-transparent at rest (explicit user request) - it's a reminder
    chip, not something that needs full attention while scrolling past
    it, and a solid pill was competing with the results underneath it.
    Full opacity on hover/touch so it's still perfectly readable the
    moment someone actually looks at or taps it. */
 .xc-sticky-name{position:fixed;left:1rem;bottom:1.2rem;z-index:60;
   background:var(--brand,#0f2c52);color:#fff;padding:.4rem .9rem;
   border-radius:1.4rem;font-size:.8rem;font-weight:600;
   box-shadow:0 2px 8px rgba(0,0,0,.3);max-width:60vw;white-space:nowrap;
   overflow:hidden;text-overflow:ellipsis;opacity:.55;
   transition:opacity .15s ease}
 .xc-sticky-name:hover,.xc-sticky-name:focus-within{opacity:1}
 .xc-sticky-name .badge{margin-left:.3rem}
 @media (max-width:600px){
   .xc-sticky-name{max-width:50vw;font-size:.75rem}
   .xc-to-top{padding:.45rem .8rem;font-size:.8rem}
 }
/* Team leaderboard (explicit user request): sits beside the event name/
   status/day-time block above the RESULTS tabs, not inside any one tab,
   so it's visible regardless of which tab the reader's on. Row-reverse
   so it wraps to ABOVE .xc-head-main on narrow screens (source order
   puts the name/status first for screen readers) rather than being
   squeezed into a second column. */
 .xc-head-row{display:flex;flex-wrap:wrap-reverse;justify-content:space-between;
   align-items:flex-start;gap:.75rem}
 .xc-head-main{min-width:0}
/* Two 5-row columns (1-5, 6-10) rather than one tall list (explicit
   user request - top 10 now, was top 5) - smaller text throughout
   (explicit user request) so the whole thing stays compact next to the
   name/status/day-time block it sits beside. */
 .xc-leaderboard{flex:0 0 auto;display:flex;gap:0;background:var(--tint-blue);
   border:1px solid var(--border);border-radius:.3rem;font-size:.7rem}
 .xc-lb-col{padding:.3rem .5rem;min-width:8rem}
 .xc-lb-col+.xc-lb-col{border-left:1px solid var(--border)}
 .xc-lb-row{display:flex;align-items:center;gap:.3rem;padding:.1rem 0}
 .xc-lb-row+.xc-lb-row{border-top:1px solid var(--border)}
 .xc-lb-place{flex:0 0 1.1rem;font-weight:700;color:var(--muted)}
 .xc-lb-row .team-logo{width:14px;height:14px}
 .xc-lb-team{flex:1 1 auto;font-weight:600;white-space:nowrap;
   overflow:hidden;text-overflow:ellipsis}
 .xc-lb-score{flex:0 0 auto;font-variant-numeric:tabular-nums;
   color:var(--muted)}
/* Settings > Display settings toggle (explicit user request) - same
   client-side show/hide-via-body-class pattern as every other row in
   that menu (Team Scores Ribbon, Show logos, etc), not a server round
   trip, so it's instant and the choice sticks via localStorage. */
 body.no-xc-leaderboard .xc-leaderboard{display:none}
/* Floating/pinned once scrolled past the header (explicit user request:
   "always make it appear when the user scrolls") - class toggled by
   setupXcScrollAids (app.js), `top` set inline there to the site
   header's own live height so it docks right under the nav bar rather
   than a hardcoded offset that would drift if that header's height
   ever changes (wraps to two lines on a narrow phone, etc). Same
   element, not a clone - it just leaves its normal spot in .xc-head-row
   and starts painting fixed instead. */
 .xc-leaderboard.xc-lb-floating{position:fixed;right:1rem;z-index:16;
   box-shadow:0 4px 12px rgba(0,0,0,.3);max-width:92vw;overflow-x:auto;
   cursor:pointer;transition:transform .22s ease}
/* Dismissed (explicit user request): slides off past the right edge
   rather than disappearing outright, leaving the "Top 10" nub (below)
   peeking out where it stopped - right:1rem plus its own width/padding
   is comfortably more than 100%, so this fully clears the viewport
   without needing to know the chip's exact rendered width. */
 .xc-leaderboard.xc-lb-floating.xc-lb-dismissed{transform:translateX(120%)}
/* Dismiss nub: same right edge and live top offset as the chip itself
   (both set together in setupXcScrollAids), just a small always-visible
   tab rather than the full leaderboard - clicking it un-dismisses.
   Same slide-in transition as the chip it reveals. */
 .xc-lb-nub{position:fixed;right:0;z-index:16;background:var(--brand,#0f2c52);
   color:#fff;border:none;border-radius:.3rem 0 0 .3rem;padding:.4rem .5rem;
   font-size:.72rem;font-weight:700;letter-spacing:.02em;cursor:pointer;
   box-shadow:0 4px 12px rgba(0,0,0,.3);transition:transform .22s ease}
 .xc-lb-nub[hidden]{display:none}
 @media (max-width:600px){
   .xc-table{font-size:.82rem}
   .xc-table th,.xc-table td{padding:.25rem .35rem}
   .xc-col-athlete{min-width:8rem}
 }
