/* SynhalEES Leaderboard — theme derived from logo: navy #2B3044 + red #C62828 */
:root {
  --navy: #2b3044;
  --navy-deep: #1d2230;
  --navy-card: #262b3c;
  --red: #c62828;
  --red-bright: #e53935;
  /* hover highlight for chart dots - a lighter step of the same red */
  --red-hi: #ff6f60;
  --gold: #f5b301;
  /* gold as *text*. --gold stays a bright fill because it carries dark text on
     top (.demo-badge, .cmp-wins.top); darkening it for the light theme would
     break that pairing, so readable-on-white gold is a separate token. */
  --gold-text: #f5b301;
  /* gold as a *stroke* (Pareto frontier dashes, optimal-set halos). Bright gold
     is 1.85:1 on white, so a 2px line and a dashed line both vanish; this is
     the same hue darkened to stay legible as a non-text graphic (needs 3:1). */
  --gold-stroke: #f5b301;
  --text: #eef0f6;
  /* dimmer body copy (methodology Sinhala). Must be a real token: it was only
     ever given as a #cbd1e1 fallback, so the light theme inherited a
     near-white-on-white value. */
  --text-dim: #cbd1e1;
  --muted: #9aa1b5;
  --border: #3a4157;
  --row-alt: #232838;
  --good: #2e9e5b;
  /* surface gradient for the methodology card. It was a literal dark-navy
     gradient, which survived into light mode and left dark text on a dark card. */
  --card-veil: linear-gradient(135deg, rgba(38, 43, 60, 0.75) 0%, rgba(26, 30, 44, 0.95) 100%);
  --card-shadow: 0 4px 20px rgba(0, 0, 0, 0.22);
  /* soft inner scroll affordance on the frozen model column */
  --col-shadow: 6px 0 10px -6px rgba(0, 0, 0, .5);
  --shadow: 0 10px 30px rgba(0, 0, 0, .35);
  /* scrollbars follow the theme instead of the OS default (a light grey bar
     read as a foreign element on the navy cards) */
  --sb-track: #171b26;
  --sb-thumb: #454d66;
  --sb-thumb-hover: #5b6480;
  /* starred leaderboard row: a gold wash. Bright gold at 8% vanishes on the
     white light-theme card, so the tint is its own token (the light theme
     darkens it and raises the alpha instead of reusing the dark value). */
  --star-row-bg: rgba(245, 179, 1, .16);
}

[data-theme="light"] {
  --navy: #ffffff;
  --navy-deep: #f4f5f9;
  --navy-card: #ffffff;
  --text: #232838;
  --muted: #5b6274;
  --border: #d9dce6;
  --row-alt: #f0f1f7;
  --text-dim: #525a6b;
  /* 4.23:1 with the dark-mode red on white; #d32f2f reaches 4.98:1 (WCAG AA
     for normal-size text) while staying the same brand red hue. */
  --red-bright: #d32f2f;
  --gold-text: #8a6100;
  --gold-stroke: #9a6d00;
  --good: #1e7a45;
  --card-veil: linear-gradient(135deg, #ffffff 0%, #eef0f6 100%);
  --card-shadow: 0 4px 18px rgba(43, 48, 68, .10);
  --col-shadow: 6px 0 10px -6px rgba(43, 48, 68, .16);
  --shadow: 0 10px 26px rgba(43, 48, 68, .10);
  --sb-track: #eceef4;
  --sb-thumb: #c3c8d6;
  --sb-thumb-hover: #a8aebe;
  /* a real amber wash: gold at low alpha washed out to a barely-there beige
     on the white card, so the light theme uses a denser, warmer tint */
  --star-row-bg: rgba(214, 146, 0, .22);
}

* {
  box-sizing: border-box;
}

/* ---------- themed scrollbars ----------
   Two layers, because browsers need both: the standard scrollbar-color applies
   in Firefox and modern Chromium, while the ::-webkit pseudo-elements cover
   older WebKit/Blink builds that ignore it. Colours come from the theme
   variables above, so the light theme gets a light bar automatically. */
html {
  scrollbar-color: var(--sb-thumb) var(--sb-track);
  scrollbar-width: thin;
  /* an anchor jump must not park its target underneath the sticky header */
  scroll-padding-top: 80px;
}

::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}

::-webkit-scrollbar-track {
  background: var(--sb-track);
}

::-webkit-scrollbar-thumb {
  background: var(--sb-thumb);
  border-radius: 6px;
  /* 3px transparent border + background-clip gives the thumb visual padding
     without the box growing, so the rounded pill reads correctly on the track */
  border: 3px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--sb-thumb-hover);
  background-clip: content-box;
}

::-webkit-scrollbar-corner {
  background: var(--sb-track);
}

body {
  margin: 0;
  font-family: "Inter", "Noto Sans Sinhala", system-ui, sans-serif;
  background: var(--navy-deep);
  color: var(--text);
  line-height: 1.55;
  /* iOS Safari inflates text in landscape and on some home screens, which breaks
     every width the layout above was tuned against */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* The page must never scroll sideways. `clip` rather than `hidden`: `hidden`
     would make the body a scroll container and silently kill every
     `position: sticky` cell inside the leaderboard table. */
  overflow-x: clip;
}

/* kill the 300ms tap delay and the blue flash on Android Chrome, both of which
   make the tab rows feel sluggish on a phone */
button,
select,
a,
.tab,
.lb-table tbody tr {
  touch-action: manipulation;
}

button,
select,
input {
  -webkit-tap-highlight-color: transparent;
}

/* One gutter system for the whole page: the sticky header, the main column and
   the footer all use .wrap, so they share the same responsive inline gutter at
   every width (16px on a phone -> 40px on a desktop) instead of each adding
   their own padding on top of it. */
.wrap {
  width: 100%;
  /* 1440 left ~240px of dead space on each side of a 1920 screen.
     1680 keeps the leaderboard readable line lengths while letting wide
     monitors actually use the pixels they have. */
  max-width: 1680px;
  margin-inline: auto;
  padding-inline: clamp(16px, 3.2vw, 40px);
}

/* Wide monitors: give the content real breathing room at the screen edges.
   The 1920px cap keeps rows from stretching uncomfortably wide on an ultrawide
   while the larger gutter stops the layout from sitting flush against the bezel.
   Tablet and phone widths keep the base clamp above. */
@media (min-width: 1200px) {
  .wrap {
    max-width: 1920px;
    padding-inline: clamp(24px, 1vw, 40px);
  }
}

.accent {
  color: var(--red-bright);
}

a {
  color: var(--red-bright);
}

/* header */
.site-header {
  background: var(--navy);
  border-bottom: 3px solid var(--red);
  position: sticky;
  top: 0;
  z-index: 20;
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* padding-block, not the `padding` shorthand: this rule sits AFTER .wrap in
     the source at equal specificity, so a shorthand would reset padding-left
     /right back to 0 and throw the shared gutter away (the header content
     then sat ~40px outside the main column). Longhand keeps the inline
     gutter owned by .wrap alone. */
  /* top inset for notched phones; the plain 10px is the everywhere-else value */
  padding-top: max(10px, env(safe-area-inset-top));
  padding-bottom: 10px;
  min-width: 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  text-decoration: none;
  color: var(--text);
}

/* the mark is 3331x592, so a fixed 44px height makes it ~250px wide and pushes
   the header actions off-screen on a 320px phone. Let it shrink with the
   viewport instead - height stays proportional because both dimensions are
   auto and only a max-width is applied. */
.brand-logo {
  width: auto;
  height: auto;
  min-width: 0;
  max-width: min(50vw, 250px);
  max-height: 44px;
  background: #fff;
  border-radius: 8px;
  padding: 3px 8px;
}

.header-actions {
  display: flex;
  align-items: center;
  flex: none;
  gap: 10px;
}

.demo-badge {
  background: var(--gold);
  color: #1d2230;
  font-weight: 700;
  font-size: .75rem;
  padding: 4px 10px;
  border-radius: 999px;
  white-space: nowrap;
}

.btn {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  padding: 6px 14px;
  border-radius: 8px;
  cursor: pointer;
  font: inherit;
  text-decoration: none;
}

.btn:hover {
  border-color: var(--red-bright);
  color: var(--red-bright);
}

/* hero */
.hero {
  text-align: center;
  padding: 48px 0 28px;
}

.hero h1 {
  /* fluid rather than a fixed size: the Sinhala headline has to fit a 320px
     viewport without pushing the page into a horizontal scroll */
  font-size: clamp(1.4rem, 4.4vw + .6rem, 2.1rem);
  margin: 0 0 10px;
  font-weight: 800;
  overflow-wrap: break-word;
}

/* Desktop: the hero is the page's only vertical break, and a tall monitor has
   room to spare, so open up the space above the headline - but the gap
   before the control bar stays tighter so the filters are reachable sooner.
   The phone/tablet values above stay tight. */
@media (min-width: 1100px) {
  .hero {
    padding: 48px 0 30px;
  }

  .hero h1 {
    margin-bottom: 16px;
  }

  .updated {
    margin-top: 22px;
  }

  .controls {
    margin-bottom: 22px;
  }
}

.tagline {
  color: var(--muted);
  max-width: min(640px, 100%);
  margin: 0 auto;
}

.updated {
  color: var(--muted);
  font-size: .85rem;
  margin-top: 14px;
}

/* controls */
.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-bottom: 16px;
  /* lift the whole bar (incl. the Models dropdown) above the table-card -
     stays below the sticky site-header (z-index: 20) */
  position: relative;
  z-index: 10;
}

.tabs {
  display: flex;
  gap: 6px;
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 3px;
}

.tab {
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-weight: 600;
  padding: 6px 16px;
  border-radius: 8px;
  cursor: pointer;
}

.tab.active {
  background: var(--red);
  color: #fff;
}

.pillar-select,
.search {
  background: var(--navy-card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 12px;
  font: inherit;
}

.pillar-select {
  width: 320px;
  max-width: 100%;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa1b5' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px;
  padding-right: 34px;
}

[data-theme="light"] .pillar-select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b6274' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}

.search {
  flex: 1 1 160px;
  min-width: 140px;
}

/* Leaderboard search: same treatment as the radar modal's compare search - Chrome
   paints its own blue "x" on type=search and blue appears nowhere in this theme,
   so hide it and use the themed clear button instead (.search-clear mirrors
   .modal-compare-clear). */
.search::-webkit-search-cancel-button,
.search::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

/* the clear glyph needs the wrapper to be the positioning context */
.search-field {
  position: relative;
  display: flex;
  flex: 1 1 160px;
  min-width: 140px;
}

/* reserve room on the right so the text never runs under the glyph */
.search-field .search {
  width: 100%;
  padding-right: 34px;
}

.search-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.search-clear[hidden] {
  display: none;
}

.search-clear .icon {
  width: 13px;
  height: 13px;
}

.search-clear:hover {
  background: var(--row-alt);
  color: var(--red-bright);
}

.search-clear:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 1px;
}

/* Every .pillar-select on the page is turned into a themed popover by
   assets/dropdown.js. Chrome renders a native dropdown in OS chrome: it cannot
   be themed against this palette, and with long option lists it flips upward
   and runs off the top of the screen. The popover keeps the dark theme, caps its
   height and scrolls, and flips up only when it genuinely will not fit below.

   The wrapper is injected in front of the <select> and becomes the flex child
   of the control row, so it carries the sizing that used to sit on the select. */
.pillar-dd {
  position: relative;
  display: inline-flex;
  min-width: 0;
  /* z-index on a positioned element only orders it inside its own stacking
     context. .chart-card runs a filled fadeUp transform animation, which makes
     it a stacking context, so a z-index on the menu alone cannot lift the
     popover past the chart canvas. Raising the WRAPPER does. */
  z-index: 1;
}

.pillar-dd.is-open {
  z-index: 60;
}

/* Only the leaderboard control row wants the filter to absorb spare width. Every
   other row (chart controls, compare picker) keeps its content-driven width,
   which is what the raw <select> did before it was wrapped. */
.controls>.pillar-dd {
  flex: 1 1 220px;
}

/* The native control stays in the DOM (its options and .value are the single
   source of truth, and every existing change listener reads them) - it is just
   taken out of the layout and the accessibility tree, since the trigger button
   carries the accessible name and the listbox role. */
.pillar-dd-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.pillar-dd-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 0;
  text-align: left;
  background: var(--navy-card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 12px;
  font: inherit;
  cursor: pointer;
}

/* the label is the long part; truncate it rather than widening the row */
.pillar-dd-btn > span:first-child {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pillar-dd-chevron {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--muted);
}

.pillar-dd-btn:hover {
  border-color: var(--red-bright);
}

.pillar-dd-btn:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--navy), 0 0 0 3px rgba(229, 57, 53, .28);
}

/* Opens downward and is capped + scrollable so it can never run past the bottom
   of the page. Flipped open with .open-up when there is not enough room below
   (decided in the JS). */
.pillar-dd-menu {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 6px);
  z-index: 30;
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  padding: 6px;
}

.pillar-dd-menu.open-up {
  top: auto;
  bottom: calc(100% + 6px);
}

.pillar-dd-menu[hidden] {
  display: none;
}

.pillar-dd-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  color: var(--text);
  font: inherit;
  font-size: .85rem;
  padding: 7px 9px;
  border-radius: 8px;
  cursor: pointer;
}

.pillar-dd-opt:hover,
.pillar-dd-opt:focus-visible {
  background: var(--row-alt);
  outline: none;
}

.pillar-dd-opt[aria-selected="true"] {
  background: var(--row-alt);
  color: var(--red-bright);
  font-weight: 700;
}

.pillar-dd-num {
  flex: none;
  color: var(--muted);
  font-size: .78rem;
  font-weight: 800;
  min-width: 18px;
}

.pillar-dd-opt[aria-selected="true"] .pillar-dd-num {
  color: var(--red-bright);
}
/* Leaderboard filter row: the two remaining native selects size to their content,
   while .pillar-dd above takes whatever room is left over so the row reflows
   rather than overflowing. */

.controls>.type-select,
.controls>.provider-select {
  flex: 0 1 auto;
}

/* search + Models filter + PNG export travel as one group, so the export icon
   can never be orphaned alone on a row of its own at any viewport width. */
.controls-end {
  display: flex;
  flex: 1 1 340px;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* table */
.table-card {
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 14px;
  /* iOS needs this for momentum inside a horizontal scroller; harmless elsewhere */
  -webkit-overflow-scrolling: touch;
  /* the leaderboard table gets a min-width on narrow screens, so the card
     scrolls horizontally (the same pattern .compare-wrap already uses)
     instead of the page scrolling sideways */
  overflow-x: auto;
  box-shadow: var(--shadow);
}

.lb-table {
  width: 100%;
  border-collapse: collapse;
  font-size: .95rem;
}

.lb-table th,
.lb-table td {
  padding: 12px 14px;
  text-align: left;
}

/* Set by app.js once the leaderboard holds more than 10 models. Past that point
   the table gets a width FLOOR, not a ceiling: columns never squeeze below a
   steady readable width, and .table-card (overflow-x: auto) scrolls sideways
   whenever the viewport is too narrow to show that width.

   This used to be `max-width`, which could only ever make the table narrower
   than its card - so on wide screens nothing overflowed, no scrollbar appeared,
   and the table just left a large empty gap on the right. A min-width keeps
   the table filling the card when there is room and only scrolls when there
   genuinely is not. */
.lb-table.is-capped {
  min-width: 1180px;
}

/* 11+ rows: freeze the card height at ~10 rows + header and scroll the rest vertically inside the card (top ~10 rows stay visible). */
.table-card:has(.lb-table.is-capped) {
  max-height: 680px;
  overflow: auto;
}

.lb-table.is-capped thead th {
  position: sticky;
  top: 0;
  z-index: 5;
}

/* Auto table layout sizes each column from its widest cell. The moment a value
   like "236.1K" or "2026-09-26" wraps onto a second line, that column resolves to a
   different width on every row and the values stop lining up under their
   headers. Keeping cells on one line costs nothing here: .table-card already
   scrolls horizontally when the table outgrows the viewport. */
.lb-table td {
  white-space: nowrap;
}

/* The model name is the one cell allowed to wrap - it carries the score bar
   underneath, and long names grow the column instead of overflowing it. */
.lb-table td:nth-child(2) {
  white-space: normal;
  min-width: 190px;
  max-width: 320px;
}

.lb-table th {
  background: var(--navy);
  color: var(--muted);
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}

/* Centre every column except the model name. The model column (2) stays
   left-aligned because it is the row identity - a name reads from a fixed
   left edge far better than floating mid-column. The rank badge (1) IS
   centred, so the # header sits on the same line as the medal circles.
   Centring the rest also removes the header/value mismatch the inline sort
   arrow used to cause: centred text is symmetric, so the arrow no longer
   pushes the label off the data's centre line. */
.lb-table th,
.lb-table td {
  text-align: center;
}

.lb-table th:nth-child(2),
.lb-table td:nth-child(2),
.lb-table th:nth-child(3) {
  text-align: left;
}

.lb-table tbody tr {
  border-top: 1px solid var(--border);
  cursor: pointer;
}

.lb-table tbody tr:nth-child(even) {
  background: var(--row-alt);
}

.lb-table tbody tr:hover {
  outline: 2px solid var(--red);
  outline-offset: -2px;
}

.medal {
  font-size: 1.1rem;
}

.model-name {
  font-weight: 700;
  /* Clamp to 2 lines: a long name ("Gemini 3.1 Flash lite Preview") otherwise
     wraps to 3+ lines and makes one row twice as tall as its neighbours.
     app.js puts the full name in `title`, so nothing is actually lost. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.lb-table td.provider {
  color: var(--muted);
  white-space: nowrap;
  text-align: left;
}

/* model license type badge */
.type-badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: .72rem;
  font-weight: 700;
  white-space: nowrap;
  letter-spacing: .02em;
  border: 1px solid transparent;
}

.type-badge.open {
  color: #2ecc71;
  background: rgba(46, 204, 113, .14);
  border-color: rgba(46, 204, 113, .35);
}

.type-badge.proprietary {
  color: #8ab4f8;
  background: rgba(100, 149, 237, .14);
  border-color: rgba(100, 149, 237, .35);
}

/* Those two badge colours are pastels tuned for the navy card; on white they
   land at 2.10:1 and 2.11:1. Darken the text (and the tint behind it) for the
   light theme only - 5.4:1 and 6.3:1. */
[data-theme="light"] .type-badge.open {
  color: #0f7a45;
  background: rgba(15, 122, 69, .10);
  border-color: rgba(15, 122, 69, .28);
}

[data-theme="light"] .type-badge.proprietary {
  color: #1a5fb4;
  background: rgba(26, 95, 180, .10);
  border-color: rgba(26, 95, 180, .28);
}

.type-select {
  width: auto;
  min-width: 140px;
}

/* provider brand logo (white chip keeps black logos visible on dark theme) */
[data-theme="light"] .prov-logo {
  border: 1px solid var(--border);
}

.prov-logo {
  width: 20px;
  height: 20px;
  vertical-align: -5px;
  margin-right: 8px;
  border-radius: 6px;
  background: #fff;
  padding: 2.5px;
  box-sizing: border-box;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .25);
}

/* fallback chip for providers without a logo in assets/logos/ */
.prov-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  vertical-align: -5px;
  margin-right: 8px;
  border-radius: 6px;
  background: var(--red);
  color: #fff;
  font-size: .68rem;
  font-weight: 800;
}

.score-pill {
  display: inline-block;
  min-width: 58px;
  text-align: center;
  font-weight: 700;
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--navy);
  border: 1px solid var(--border);
}

.score-pill.top {
  background: var(--red);
  border-color: var(--red);
  color: #fff;
}

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

.scorebar {
  height: 5px;
  border-radius: 3px;
  background: var(--border);
  margin-top: 5px;
  overflow: hidden;
}

.scorebar>span {
  display: block;
  height: 100%;
  transform-origin: left;
  background: linear-gradient(90deg, var(--red), var(--red-bright));
}

.empty {
  padding: 30px;
  text-align: center;
  color: var(--muted);
}

/* pillar grid */
.pillars {
  margin: 40px 0 60px;
}

.pillars h2 {
  font-size: 1.3rem;
  margin-bottom: 4px;
}

.pillars-sub {
  color: var(--muted);
  font-size: .86rem;
  margin: 0 0 18px;
}

.pillar-grid {
  display: grid;
  /* min() stops a 320px track from forcing a 320px phone to scroll sideways */
  grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: clamp(12px, 2.2vw, 16px);
}

.pillar-card {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 13px;
  background: linear-gradient(165deg, var(--navy-card) 0%, var(--row-alt) 130%);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px 18px 15px;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform .22s ease, border-color .22s ease, box-shadow .22s ease;
}

.pillar-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg, var(--red), var(--red-bright) 55%, transparent);
  opacity: .45;
  transition: opacity .22s ease;
}

.pillar-card:hover {
  transform: translateY(-3px);
  border-color: var(--red);
  box-shadow: var(--shadow);
}

.pillar-card:hover::before {
  opacity: 1;
}

.pillar-card:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 2px;
}

/* card header: icon badge + bilingual title + ghost pillar number */
.pc-top {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.pc-icon {
  flex: none;
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background: rgba(198, 40, 40, .14);
  border: 1px solid rgba(198, 40, 40, .32);
}

.pc-icon .icon {
  width: 20px;
  height: 20px;
  color: var(--red-bright);
}

[data-theme="light"] .pc-icon {
  background: rgba(198, 40, 40, .08);
}

.pc-titles {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.pc-title {
  font-weight: 700;
  font-size: .95rem;
  line-height: 1.32;
}

.pc-si {
  color: var(--muted);
  font-size: .79rem;
}

.pc-num {
  margin-left: auto;
  flex: none;
  font-size: 1.55rem;
  font-weight: 800;
  line-height: 1;
  color: var(--border);
  user-select: none;
}

/* champion footer: provider logo + crown + name, gradient score pill right */
.pc-champ {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  border-top: 1px solid var(--border);
  padding-top: 11px;
}

.pc-champ .who {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-weight: 700;
  font-size: .88rem;
}

.pc-champ .who .prov-logo,
.pc-champ .who .prov-chip {
  width: 18px;
  height: 18px;
  margin-right: 0;
  vertical-align: 0;
  flex: none;
}

.pc-champ .who .icon {
  width: 14px;
  height: 14px;
  color: var(--gold-text);
  flex: none;
}

.pc-champ .who-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pc-val {
  flex: none;
  font-weight: 800;
  font-size: .92rem;
  color: #fff;
  font-variant-numeric: tabular-nums;
  padding: 3px 11px;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--red), var(--red-bright));
  box-shadow: 0 2px 8px rgba(198, 40, 40, .35);
}

/* score meter (fills during the first-load cascade) */
.pc-bar {
  display: block;
  height: 4px;
  border-radius: 3px;
  background: var(--border);
  overflow: hidden;
}

.pc-bar>span {
  display: block;
  height: 100%;
  border-radius: 3px;
  transform-origin: left;
  background: linear-gradient(90deg, var(--red), var(--red-bright));
}

@keyframes barGrow {
  from {
    transform: scaleX(0);
  }

  to {
    transform: scaleX(1);
  }
}

#pillar-grid.grid-boot .pc-bar>span {
  animation: barGrow .9s .3s cubic-bezier(.2, .8, .3, 1) both;
}

/* leaderboard score bars fill in once on first load (rows-boot = bootAnim only) */
#lb-body.rows-boot .scorebar>span {
  animation: barGrow .9s .25s cubic-bezier(.2, .8, .3, 1) both;
}

/* methodology yardstick card */
.methodology-card {
  display: flex;
  align-items: flex-start;
  gap: 18px;
  background: var(--card-veil);
  border: 1px solid var(--border);
  border-left: 4px solid var(--red-bright);
  border-radius: 14px;
  padding: 22px 26px;
  margin: 44px 0 24px;
  box-shadow: var(--card-shadow);
}

.methodology-icon {
  flex-shrink: 0;
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(229, 57, 53, 0.12);
  border: 1px solid rgba(229, 57, 53, 0.28);
  border-radius: 10px;
  color: var(--red-bright);
}

.methodology-icon .icon {
  width: 22px;
  height: 22px;
}

.methodology-content {
  flex: 1;
}

.methodology-content h3 {
  font-size: 1.08rem;
  font-weight: 700;
  margin: 0 0 8px;
  color: var(--text);
  letter-spacing: -0.01em;
}

.methodology-si {
  color: var(--text-dim);
  font-size: .93rem;
  line-height: 1.65;
  margin: 0 0 8px;
}

.methodology-si strong {
  color: var(--text);
  font-weight: 600;
}

.methodology-en {
  color: var(--muted);
  font-size: .85rem;
  line-height: 1.55;
  margin: 0;
}

/* modal */
.modal {
  position: fixed;
  inset: 0;
  background: rgba(10, 12, 20, .72);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 20px;
}

.modal[hidden] {
  display: none;
}

.modal-card {
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 16px;
  /* Top padding moved onto the title below: the toolbar has to sit flush in
     the scroll content so it can stick, and sticky cannot reach into padding. */
  padding: 0 24px 24px;
  max-width: 600px;
  width: 100%;
  position: relative;
  box-shadow: var(--shadow);
  /* Compare mode adds a searchable list plus a 15-row head-to-head table, so
     the card outgrows a short viewport. Cap it and scroll the card itself:
     without this the centred flex clips BOTH ends and the title and the
     strongest-pillars block become unreachable. */
  max-height: calc(100vh - 40px);
  max-height: calc(100dvh - 40px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* Sticky header: the model name on the left, close / compare / export on the
   right. Sticking keeps the controls reachable while the card body scrolls -
   the close button is the primary way out, so losing it to a scroll would be
   a regression - and it also keeps the card labelled at every scroll depth.

   This is a real row rather than the zero-height strip it used to be, with the
   buttons floating above the content. The card body scrolls, so a transparent
   header lets rows slide *under* the buttons and collide with the scores and
   delta chips printed beside them; an opaque header is what stops that. */
.modal-toolbar {
  position: sticky;
  top: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 10px 0;
  background: var(--navy-card);
  border-bottom: 1px solid var(--border);
}

/* close + compare + export reuse the .btn outline/hover (same look as the
   All / compare buttons); here we only square them off and line them up along
   the card's right edge. */
.modal-close,
.modal-compare-btn,
.modal-export {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  color: var(--muted);
}

/* same red accent the Models filter button wears while a filter is live */
.modal-compare-btn.on {
  color: var(--red-bright);
  border-color: var(--red-bright);
}

/* nothing to compare against (a single-model leaderboard) */
.modal-compare-btn:disabled {
  opacity: .4;
  cursor: not-allowed;
}

.modal-compare-btn:disabled:hover {
  color: var(--muted);
  border-color: var(--border);
}

.modal-close .icon,
.modal-compare-btn .icon,
.modal-export .icon {
  width: 18px;
  height: 18px;
  vertical-align: 0;
}

/* The model name sits in the sticky header, left of the buttons, so the card
   you are reading is always labelled - even halfway down a scrolled list of
   pillars. flex:1 pushes the three buttons to the right edge; min-width:0 is
   what lets the ellipsis actually engage on a long name. */
.modal-toolbar > #modal-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.2;
}

.modal-sub {
  color: var(--muted);
  margin-top: 10px;
}

#radar {
  width: 100%;
  height: auto;
}

.modal-best {
  margin-top: 8px;
  font-size: .88rem;
  color: var(--muted);
}

.modal-best strong {
  color: var(--text);
}

/* footer */
.site-footer {
  border-top: 1px solid var(--border);
  padding: 22px 0;
  color: var(--muted);
  font-size: .85rem;
  text-align: center;
}

/* Phones: trim the table to the columns that actually earn their width.
   Provider (3) and license type (4) are reference metadata - both stay in the
   DOM and in the PNG export, they are just not worth a column on a phone.
   The run date (9) is deliberately KEPT: it is the one remaining column that
   tells you how fresh a result is, and now that the rank + model columns are
   frozen and the rest scrolls, there is room to reach it by swiping. */
@media (max-width: 640px) {

  .lb-table th:nth-child(4),
  .lb-table td:nth-child(4),
  .lb-table th:nth-child(3),
  .lb-table td:nth-child(3) {
    display: none;
  }
}

/* inline SVG icons (Lucide-style stroke icons - replaces emojis) */
.icon {
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.15em;
  flex: none;
}

#theme-toggle .icon {
  width: 18px;
  height: 18px;
  vertical-align: -3px;
}

.demo-badge .icon {
  width: 15px;
  height: 15px;
  vertical-align: -2px;
  margin-right: 2px;
}

#modal-best .icon {
  width: 15px;
  height: 15px;
  margin-right: 3px;
  color: var(--red-bright);
}

/* sort arrows in table headers */
.th-label {
  vertical-align: middle;
}

.sort-arrow {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  margin-left: 5px;
  color: var(--muted);
  opacity: .35;
  transform: rotate(0deg);
  transition: transform .35s cubic-bezier(.34, 1.56, .64, 1), opacity .25s ease, color .25s ease;
}

.lb-table th:hover .sort-arrow {
  opacity: .7;
  color: var(--red-bright);
}

.lb-table th.sorted-asc .sort-arrow,
.lb-table th.sorted-desc .sort-arrow {
  opacity: 1;
  color: var(--red-bright);
  animation: arrowPop .35s cubic-bezier(.34, 1.56, .64, 1) both;
}

.lb-table th.sorted-desc .sort-arrow {
  transform: rotate(180deg);
}

@keyframes arrowPop {
  0% {
    scale: .4;
  }

  60% {
    scale: 1.25;
  }

  100% {
    scale: 1;
  }
}

/* tbody sweep when the sort changes */
#lb-body.rows-sort tr {
  animation: fadeUp .3s ease both;
}

/* rank badges (gold / silver / bronze - replaces medal emojis) */
.rank-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  font-weight: 800;
  font-size: .82rem;
}

.rank-badge.r1 {
  background: linear-gradient(135deg, #d4a017, #f5c542);
  color: #3a2f00;
}

.rank-badge.r2 {
  background: linear-gradient(135deg, #8a93a6, #c3cad6);
  color: #2b3044;
}

.rank-badge.r3 {
  background: linear-gradient(135deg, #a05a2c, #c97e4a);
  color: #fff;
}

#modality-tabs .tab .icon {
  width: 15px;
  height: 15px;
  vertical-align: -2px;
  margin-right: 4px;
}

.pillars h2 {
  display: flex;
  align-items: center;
  gap: 8px;
}

.pillars h2 .icon {
  width: 20px;
  height: 20px;
  color: var(--red-bright);
}

.site-footer .icon {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
}


.btn .icon {
  width: 17px;
  height: 17px;
  vertical-align: middle;
}


/* ---------- entrance & draw animations ---------- */
@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(14px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes slideDown {
  from {
    opacity: 0;
    transform: translateY(-14px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

@keyframes popIn {
  from {
    opacity: 0;
    transform: scale(.94) translateY(12px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* page unfolds on open - staggered sections */
.site-header {
  animation: slideDown .45s ease both;
}

.hero {
  animation: fadeUp .55s .12s ease both;
}

.controls {
  animation: fadeUp .55s .24s ease both;
}

.table-card {
  animation: fadeUp .55s .34s ease both;
}

.pillars {
  animation: fadeUp .55s .44s ease both;
}

.site-footer {
  animation: fadeUp .55s .52s ease both;
}

/* leaderboard rows + pillar cards cascade in (first load only, via JS class) */
#lb-body.rows-boot tr {
  animation: fadeUp .45s ease both;
}

#pillar-grid.grid-boot .pillar-card {
  animation: fadeUp .5s ease both;
}

/* modal open (replays every time [hidden] is removed) */
.modal {
  animation: fadeIn .2s ease both;
}

.modal-card {
  animation: popIn .3s cubic-bezier(.2, .9, .3, 1.15) both;
}

/* respect reduced-motion settings */
@media (prefers-reduced-motion: reduce) {
  /* the sweep is decoration, so it is dropped outright rather than shortened */
  .modal-diff .diff-row.sweep .dbar i.a,
  .modal-diff .diff-row.sweep .dbar i.b,
  .modal-diff .diff-row.sweep .dbar::after {
    animation: none !important;
    transform: none !important;
  }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
  }
}

/* radar vertex tooltip */
.radar-tip {
  position: absolute;
  transform: translate(-50%, -100%);
  pointer-events: none;
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 12px;
  font-size: .82rem;
  color: var(--text);
  text-align: center;
  box-shadow: var(--shadow);
  z-index: 6;
  white-space: nowrap;
}

.radar-tip strong {
  display: block;
  font-size: .85rem;
}

.radar-tip span {
  color: var(--red-bright);
  font-weight: 700;
}

/* gold tooltip for strongest-pillar nodes */
.radar-tip.gold {
  border-color: var(--gold-text);
}

.radar-tip.gold span {
  color: var(--gold-text);
}

.radar-tip em {
  display: block;
  font-style: normal;
  font-size: .68rem;
  font-weight: 700;
  color: var(--gold-text);
  letter-spacing: .06em;
  text-transform: uppercase;
}

/* ---------- model comparison chart (chart.js) ---------- */
.chart-card {
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 18px 8px;
  margin: 26px 0 8px;
  box-shadow: var(--shadow);
  animation: fadeUp .55s .4s ease both;
}

.chart-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
}

.chart-head h2 {
  font-size: 1.15rem;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

.chart-head h2 .icon {
  width: 20px;
  height: 20px;
  color: var(--red-bright);
}

/* bar <-> Pareto analytics switch, tab-style like the metric tabs;
   both tabs always render -> identical footprint in both views.
   It is the first item in .chart-controls so on a wide screen it sits right
   beside the Text/Vision/Audio tabs instead of hugging the card title. */
.chart-controls>.view-tabs {
  flex: none;
}

.chart-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: stretch;
}

/* chart control rows share one control height so tab pills, selects,
   the +Select models button and the export icon all line up edge-to-edge.
   The pill groups are double-layered (container padding + button padding),
   so their inner paddings are trimmed to land on the same box height.
   Desktop only: at <=640px the 44px tap-target block below takes over. */
@media (min-width: 641px) {
.chart-head,
.chart-controls,
.pareto-bar {
  --chart-ctl-h: 36px;
}
.chart-controls>.tabs,
.chart-controls>.chart-select,
.chart-controls>.pillar-dd,
.chart-controls>.chart-count-wrap,
.chart-controls>.chart-pick-wrap,
.chart-controls>.cmp-export-btn,
.pareto-bar>.chart-select,
.pareto-bar>.pillar-dd {
  height: var(--chart-ctl-h);
  min-height: var(--chart-ctl-h);
}
/* dropdown.js wraps the chart <select>s in .pillar-dd, so the visible button
   is one level down - it must fill the wrapper and use the compact chart font */
.chart-controls>.pillar-dd .pillar-dd-btn {
  height: 100%;
  min-height: 0;
  display: inline-flex;
  align-items: center;
  font-size: .85rem;
}
.chart-controls>.tabs {
  display: inline-flex;
  align-items: stretch;
}
.chart-controls>.tabs .tab {
  display: inline-flex;
  align-items: center;
  padding-top: 4px;
  padding-bottom: 4px;
  min-height: 0;
}
.chart-controls>.chart-select,
.chart-controls .chart-pick,
.pareto-bar>.chart-select,
.pareto-bar .pillar-dd-btn {
  height: 100%;
  min-height: 0;
  display: inline-flex;
  align-items: center;
}
.chart-controls>.chart-count-wrap {
  display: inline-flex;
  align-items: center;
}
.chart-controls>.chart-count-wrap .chart-select {
  height: 100%;
  min-height: 0;
}
/* Top 25 lives one level deeper (wrap > .pillar-dd > button), so the height
   must land on the inner wrapper and its visible button, not the hidden select */
.chart-controls .chart-count-wrap .pillar-dd {
  height: 100%;
  min-height: 0;
}
.chart-controls .chart-count-wrap .pillar-dd .pillar-dd-btn {
  height: 100%;
  min-height: 0;
  display: inline-flex;
  align-items: center;
  font-size: .85rem;
}
}

.chart-tabs {
  padding: 3px;
}

.chart-tabs .tab {
  padding: 5px 12px;
  font-size: .85rem;
}

/* Pareto-optimal badge inside the scatter tooltip */
.chart-tip .ct-opt {
  color: var(--gold);
  font-weight: 700;
}

/* Measure toolbar (Score/Cost/Latency/Tokens for bars, x-axis for Pareto):
   a plain controls row between the (view-independent) header and the
   canvas, so toggling views never reflows the header row */
.pareto-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  animation: fadeUp .28s ease both;
  position: relative;
  z-index: 1;
}

.pareto-bar[hidden] {
  display: none;
}

/* .pareto-bar runs its OWN filled fadeUp animation, so it is a stacking
   context of its own: everything inside it (incl. the Score/Cost popover with
   its z-index 30/60 menu) paints atomically with the bar row. The chart canvas
   lives in .chart-wrap (position: relative, z-index: auto), which is a LATER
   sibling painted after this in-flow row - so while the measure dropdown was
   open the bars drew straight over the menu. Raising the whole bar above the
   canvas while a menu inside it is open is the same trick .pillar-dd documents
   for .chart-card; :has keeps the resting paint order untouched. */
.pareto-bar:has(.pillar-dd.is-open) {
  position: relative;
  z-index: 60;
}

.pareto-bar .chart-select {
  font-size: .82rem;
  padding-top: 6px;
  padding-bottom: 6px;
}

.pareto-hint {
  margin-left: auto;
  font-size: .76rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 900px) {
  .pareto-hint {
    display: none;
  }
}

/* smooth cross-fade when swapping bar <-> Pareto views */
#chart {
  transition: opacity .16s ease;
}

#chart.chart-swap {
  opacity: 0;
}

.chart-select {
  width: auto;
  /* override .pillar-select fixed 320px so controls fit one line */
  padding: 7px 10px;
  padding-right: 32px;
  /* keep room for the .pillar-select chevron (padding shorthand above would otherwise reset it) */
  font-size: .85rem;
}

.chart-count-wrap {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.chart-count {
  color: var(--muted);
  font-size: .85rem;
  white-space: nowrap;
}

.chart-pick-wrap {
  position: relative;
}

.chart-pick {
  font-size: .85rem;
  padding: 7px 12px;
}

.chart-picker {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 30;
  /* never wider than the viewport: a 260px panel anchored to a trigger that
     sits near the left edge would otherwise run off-screen */
  width: min(260px, calc(100vw - 32px));
  max-height: 330px;
  display: flex;
  flex-direction: column;
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow);
  overflow: hidden;
}

.chart-picker[hidden] {
  display: none;
}

.pk-head {
  display: flex;
  justify-content: space-between;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.pk-head button {
  background: none;
  border: 0;
  color: var(--red-bright);
  cursor: pointer;
  font-size: .8rem;
  font-weight: 700;
  padding: 0;
}

.pk-head button:hover {
  text-decoration: underline;
}

.pk-list {
  overflow-y: auto;
  padding: 6px;
  display: block;
}

.pk-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px;
  border-radius: 8px;
  font-size: .85rem;
  cursor: pointer;
}

.pk-item:hover {
  background: var(--row-alt);
}

.pk-item input {
  accent-color: var(--red);
  flex: none;
}

.pk-item img {
  width: 16px;
  height: 16px;
  object-fit: contain;
  flex: none;
}

/* chart picker rows: checkbox + logo + name grow, then a star button that
   highlights that model in the chart (the old "No highlight" dropdown).
   The label is the clickable name area; the star is a sibling so clicking it
   can never toggle the checkbox through label forwarding. */
.pk-item-main {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  cursor: pointer;
}

.pk-star {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--muted);
  cursor: pointer;
  /* dimmed at rest so the star never competes with the checkbox + name;
     comes to full strength on hover/focus (and stays there while active) */
  opacity: .45;
  transition: opacity .15s ease;
}

.pk-star svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

.pk-star:hover,
.pk-star:focus-visible {
  background: var(--row-alt);
  color: var(--gold-text);
  opacity: 1;
}

.pk-star:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 1px;
}

/* active = this model is highlighted in the chart (always full strength) */
.pk-star[aria-pressed="true"] {
  color: var(--gold-text);
  opacity: 1;
}

.pk-star[aria-pressed="true"] svg {
  fill: currentColor;
}

/* All / None quick actions inside the Models filter header */
.pk-head-actions {
  display: inline-flex;
  gap: 14px;
}

/* search box inside the Models filter dropdown */
.pk-search-wrap {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.pk-search {
  width: 100%;
  box-sizing: border-box;
  background: var(--navy);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 10px;
  font: inherit;
  font-size: .85rem;
}

/* leaderboard Models filter button matches the search/select controls */
.controls .chart-pick {
  background-color: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  flex: none;
  padding: 9px 34px 9px 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239aa1b5' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 15px;
}

[data-theme="light"] .controls .chart-pick {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235b6274' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}

.controls .chart-pick:hover {
  border-color: var(--red-bright);
  color: var(--red-bright);
}

/* leaderboard table PNG export button matches control sizing */
#lb-export {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  /* same height as the search/select/Models controls */
  flex: none;
  padding: 0 13px;
  border-radius: 10px;
}

#lb-export .icon {
  width: 17px;
  height: 17px;
}

/* leaderboard Models button gets a red accent while a filter is active */
.chart-pick.on {
  color: var(--red-bright);
  border-color: var(--red-bright);
}

/* .chart-stage is the positioning context; .chart-wrap inside it is the
   horizontal scroller. Keeping them separate is what lets the watermark sit
   OUTSIDE the scroller, so it stays put while the bars slide under it. */
.chart-stage {
  position: relative;
}

.chart-wrap {
  position: relative;
}

/* Brand watermark - DOM, not painted into the canvas, precisely so it cannot
   scroll away. Hidden by default; chart.js reveals it only while the chart is
   actually scrollable (the canvas draws its own copy when it is not, so the
   two can never overlap). */
.chart-wm {
  display: none;
  position: absolute;
  top: 6px;
  right: 0;
  z-index: 2;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  pointer-events: none;
  white-space: nowrap;
}

.chart-wrap.is-scrollable + .chart-wm {
  display: inline-flex;
}

.chart-wm-mark {
  width: 14px;
  height: 14px;
  object-fit: contain;
}

.chart-wm-name {
  color: var(--text);
}

/* Past ~10 models the chart canvas is wider than its wrapper (chart.js floors
   the per-bar slot at 68px instead of crushing every bar), so the wrapper
   becomes a horizontal scroller - the same pattern .table-card and
   .compare-wrap use. Toggled by JS only when the canvas actually overflows, so
   a short chart never grows a scrollbar. */
.chart-wrap.is-scrollable {
  overflow-x: auto;
  /* iOS needs this for momentum inside a horizontal scroller; harmless elsewhere */
  -webkit-overflow-scrolling: touch;
  /* the rotated bar labels hang below the plot, so keep them off the card edge */
  padding-bottom: 4px;
}

#chart {
  display: block;
  width: 100%;
}

.chart-tip {
  position: absolute;
  z-index: 6;
  pointer-events: none;
  background: var(--navy);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 12px;
  font-size: .82rem;
  box-shadow: var(--shadow);
  min-width: 170px;
}

.chart-tip strong {
  display: block;
  margin-bottom: 2px;
}

.chart-tip .ct-sub {
  color: var(--muted);
  font-size: .76rem;
  margin-bottom: 6px;
}

.chart-tip .ct-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  color: var(--muted);
}

.chart-tip .ct-row span:last-child {
  font-weight: 700;
  color: var(--text);
}

.chart-tip .ct-row.on span {
  color: var(--red-bright);
}

@media (max-width: 720px) {
  /* Dissolve the control row so its children join the header's own flex line.
     Without this the header stacks (title on row 1, controls on row 2) and the
     Bars/Pareto switch - which lives in the control row - can never share the
     title's line. With `display: contents` the switch becomes a direct flex item
     of .chart-head, so it sits beside the title and the rest of the controls
     wrap underneath as before. */
  .chart-head {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    row-gap: 10px;
  }

  .chart-controls {
    display: contents;
  }

  /* the title takes the slack, the switch keeps its intrinsic size */
  .chart-head h2 {
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: wrap;
    row-gap: 8px;
  }

  /* pushed to the far right of the title's line, as on the wide layout */
  .chart-controls>.view-tabs {
    order: 0;
    flex: 0 0 auto;
    margin-left: auto;
  }

  /* the remaining controls (Text/Vision/Audio, the selects, the picker) wrap
     onto the following lines at their natural sizes; the max-width: 640px
     block below still gives them their own row layout. */

  /* the scatter needs vertical room for its labels on a narrow plot */
  .chart-card .chart-wrap {
    margin-inline: -4px;
  }
}

.pk-item img {
  width: 18px;
  height: 18px;
  box-sizing: border-box;
}

[data-theme="dark"] .pk-item img,
[data-theme]:not([data-theme="light"]) .pk-item img {
  background: #ffffff;
  border-radius: 4px;
  padding: 1px;
}

/* ---------- head-to-head comparison table ---------- */
.compare-card {
  background: var(--navy-card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px 18px 14px;
  margin: 26px 0 8px;
  box-shadow: var(--shadow);
  animation: fadeUp .55s .5s ease both;
}

.compare-head {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
}

.compare-head h2 {
  font-size: 1.15rem;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}

.compare-head h2 .icon {
  width: 20px;
  height: 20px;
  color: var(--red-bright);
}

.compare-controls {
  display: flex;
  gap: 10px;
  align-items: center;
}

.compare-count {
  color: var(--muted);
  font-size: .8rem;
  font-weight: 600;
}

.compare-count.flash {
  animation: cmpFlash .55s ease;
}

@keyframes cmpFlash {

  0%,
  100% {
    color: var(--muted);
  }

  50% {
    color: var(--red-bright);
  }
}

.compare-sub {
  color: var(--muted);
  font-size: .88rem;
  margin: 8px 0 12px;
}

/* icon-only export button + hover tooltip */
.cmp-export-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.cmp-export-btn .icon {
  width: 18px;
  height: 18px;
}

.cmp-tip {
  position: relative;
}

.cmp-tip::after {
  content: attr(data-tip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(3px);
  background: var(--navy-card);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 5px 10px;
  border-radius: 7px;
  font-size: .72rem;
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .15s ease, transform .15s ease;
  box-shadow: var(--shadow);
  z-index: 30;
}

.cmp-tip:hover::after,
.cmp-tip:focus-visible::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* The three export buttons sit at the right edge of their row, so their tooltip
   grows leftwards. A centred bubble on a right-edge button sticks past the
   viewport, and because it is only transparent (not display:none) it still
   widens the page into a sideways scroll. */
#lb-export::after,
.cmp-export-btn::after {
  left: auto;
  right: 0;
  transform: translateY(3px);
}

#lb-export:hover::after,
#lb-export:focus-visible::after,
.cmp-export-btn:hover::after,
.cmp-export-btn:focus-visible::after {
  transform: translateY(0);
}

/* no hover on a touch screen, so the bubble is dead weight there */
@media (hover: none) {
  .cmp-tip::after {
    display: none;
  }
}

.compare-wrap {
  overflow-x: auto;
  /* iOS needs this for momentum inside a horizontal scroller; harmless elsewhere */
  -webkit-overflow-scrolling: touch;
  margin: 0 -18px;
  padding: 0 18px 6px;
}

.compare-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 780px;
}

.compare-table th,
.compare-table td {
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
  text-align: center;
  font-size: .92rem;
}

.compare-table thead th {
  border-bottom: 2px solid var(--red);
  vertical-align: bottom;
  padding-bottom: 12px;
}

.cmp-rowhead {
  text-align: left !important;
  position: sticky;
  left: 0;
  background: var(--navy-card);
  z-index: 2;
  min-width: 230px;
  max-width: 300px;
  font-weight: 600;
}

/* .compare-wrap is a horizontal scroller that pulls itself 18px out of the card
   (margin: 0 -18px) and pads the content back in by the same 18px. That padding
   sits INSIDE the scrollport, but a sticky left:0 cell pins to the CONTENT edge,
   so the 18px strip to its left was never covered by the frozen column: the table
   scrolled underneath it and the featured column's red inset border bled down the
   left edge of the Head-to-Head table. This pseudo-element is a plain opaque plate
   in the frozen cell's own colour spanning exactly that gutter. It travels with the
   cell, so nothing can show in the strip at any scroll offset, and it needs no
   layout change (no shift, no width change), so the pinned column keeps the exact
   size and text inset it already had. The scrollport clips it at the card edge. */
.cmp-rowhead::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -18px;
  width: 18px;
  background: var(--navy-card);
}

.cmp-metriccount {
  color: var(--muted);
  font-size: .75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .07em;
}

.cmp-ico {
  display: inline-flex;
  vertical-align: -3px;
  margin-right: 8px;
}

.cmp-ico .icon {
  width: 16px;
  height: 16px;
  color: var(--red-bright);
}

.cmp-rtext {
  display: inline-block;
  vertical-align: middle;
}

/* clamp long labels to 2 lines so one row cannot blow up the table height */
.cmp-rlabel {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  font-weight: 700;
}

.cmp-rsub {
  display: block;
  font-weight: 400;
  font-size: .72rem;
  color: var(--muted);
}

.cmp-model-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}

/* providerLogo() returns ready-made HTML (.prov-logo img / .prov-chip fallback); size it for the compare table header vertical layout */
.cmp-model-box .prov-logo {
  width: 22px;
  height: 22px;
  margin-right: 0;
  vertical-align: 0;
  object-fit: contain;
  flex: none;
}

.cmp-model-box .prov-chip {
  margin-right: 0;
}

.cmp-name {
  font-weight: 800;
  font-size: .95rem;
  /* Same 2-line clamp as the leaderboard model column, so one long model name
     cannot stretch the whole head-to-head header on a phone. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
  width: 100%;
  min-width: 0;
}

.cmp-provider {
  font-weight: 400;
  font-size: .72rem;
  color: var(--muted);
}

.cmp-wins {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: .68rem;
  font-weight: 700;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 9px;
  margin-top: 3px;
}

.cmp-wins .icon {
  width: 11px;
  height: 11px;
}

.cmp-wins.top {
  color: #1d2230;
  background: var(--gold);
  border-color: var(--gold);
}

.cmp-wins.top .icon {
  color: #1d2230;
}

.cmp-group td {
  background: var(--row-alt);
  text-align: left !important;
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
  /* the inline inset lives on the sticky label below, not here: padding on
     the cell sits OUTSIDE the pinned box, so the label would jump inward the
     moment pinning starts and would not line up with the pinned row labels */
  padding: 6px 0;
}

/* the band label is pinned like .cmp-rowhead, so it needs the same gutter plate,
   in the band stripe colour -- otherwise the band rows are the one place the
   scrolled table still shows through beside the frozen column */
.cmp-group .cmp-glabel::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -18px;
  width: 18px;
  background: var(--row-alt);
}

/* The MODALITIES / PILLARS bands are one colspan cell, so panning the table
   sideways used to slide them out of view while every other row label stayed
   pinned: .cmp-rowhead never applies to them, and sticking the CELL cannot
   work either -- its containing block is the row, which is exactly as wide as
   the cell, so sticky has no room to shift it. The label is wrapped in a span
   whose containing block is the wide cell content, so it can slide and pin its
   left edge to the scrollport. It pins at left: 0 and carries the indent as
   its own padding, exactly like .cmp-rowhead, so it stays flush with the
   pinned row labels whichever box the engine measures the inset against. The
   opaque background keeps scrolled columns from showing through it. */
.cmp-group .cmp-glabel {
  position: sticky;
  left: 0;
  display: inline-block;
  z-index: 2;
  /* opaque, so scrolled columns never show through the pinned label */
  background: var(--row-alt);
  /* the indent rides on the pinned box (same trick .cmp-rowhead uses), so the
     label stays flush with the pinned row labels at every scroll offset */
  padding-left: 14px;
  padding-right: 10px;
}

.cmp-hero td {
  font-weight: 800;
  font-size: 1rem;
}

.compare-table td.cmp-null {
  color: var(--muted);
  font-weight: 400;
}

.cmp-best {
  background: rgba(198, 40, 40, .24);
  font-weight: 800;
}

[data-theme="light"] .cmp-best {
  background: rgba(198, 40, 40, .13);
}

/* featured mode: soften rivals' best-cell highlight (visual emphasis only — scores unchanged) */
.compare-table.has-featured td.cmp-best:not(.cmp-featured) {
  background: rgba(198, 40, 40, .13);
  font-weight: 600;
}

[data-theme="light"] .compare-table.has-featured td.cmp-best:not(.cmp-featured) {
  background: rgba(198, 40, 40, .05);
}

td.cmp-featured {
  box-shadow: inset 1px 0 0 var(--red-bright), inset -1px 0 0 var(--red-bright);
}

thead th.cmp-featured {
  box-shadow: inset 1px 0 0 var(--red-bright), inset -1px 0 0 var(--red-bright), inset 0 3px 0 var(--red-bright);
}

.compare-table tbody tr:last-child td.cmp-featured {
  box-shadow: inset 1px 0 0 var(--red-bright), inset -1px 0 0 var(--red-bright), inset 0 -2px 0 var(--red-bright);
}

.pk-item input:disabled {
  opacity: .35;
  cursor: not-allowed;
}

.pk-item:has(input:disabled) {
  opacity: .55;
}

/* ...except the starred compare row: pinned to the first column, so its
   disabled checkbox is a lock and the row keeps full strength */
#compare-picker-list .pk-item:has(input[data-featured="1"]) {
  opacity: 1;
}

#compare-picker-list .pk-item input[data-featured="1"] {
  opacity: 1;
}

@media (max-width: 720px) {
  .compare-head {
    flex-direction: column;
    align-items: stretch;
  }

  .compare-controls {
    justify-content: flex-start;
  }
}

/* ---------- responsive layout ----------
   One consolidated block for the narrow end of the range. The page itself must
   never scroll sideways and every control must stay a comfortable tap target
   from 320px upwards; wide tables scroll inside their own card, never the page. */

@media (max-width: 1100px) {

  /* the filter row can no longer hold every control on one line - let the
     group wrap to a full-width second row instead of squeezing the controls */
  .controls {
    row-gap: 10px;
  }
}

@media (max-width: 760px) {

  /* tighter card padding gives the data more room */
  .chart-card,
  .compare-card {
    padding: 14px 14px 10px;
  }

  .compare-wrap {
    margin: 0 -14px;
    padding: 0 14px 6px;
  }

  .pillars {
    margin: 32px 0 44px;
  }

  .methodology-card {
    gap: 12px;
    padding: 18px 18px 16px;
    margin: 32px 0 20px;
  }

  /* icon-only PNG export buttons hug the right edge of their row instead of
     floating alone at the left when the row wraps */
  .chart-controls .cmp-export-btn,
  .compare-controls .cmp-export-btn,
  #lb-export {
    margin-left: auto;
  }

  /* the leaderboard table scrolls inside its card rather than crushing columns */
  .lb-table {
    min-width: 620px;
  }

  /* Head-to-Head on a phone: the sticky metric column only holds a short label
     (e.g. "Sinhala Grammar") plus a tiny icon, so 230-300px was mostly wasted -
     it pushed every model column so narrow that the model names had to wrap
     hard. Give the label what it needs and the names the rest. */
  .cmp-rowhead {
    min-width: 132px;
    max-width: 160px;
  }

  /* .compare-table td drops to a 10px inline inset here, so the band label has
     to follow it or it would sit out of line with the pinned row labels */
  .compare-table .cmp-group td {
    padding: 8px 0;
  }

  .compare-table .cmp-group .cmp-glabel {
    padding-left: 10px;
  }

  /* .compare-wrap drops to a 14px inset on a phone, so the gutter plates match */
  .cmp-rowhead::before,
  .cmp-group .cmp-glabel::before {
    left: -14px;
    width: 14px;
  }

  .compare-table th,
  .compare-table td {
    padding: 8px 10px;
  }

  .compare-table {
    min-width: 560px;
  }

  .lb-table th,
  .lb-table td {
    padding: 10px 12px;
  }
}

@media (max-width: 640px) {

  /* comfortable tap targets (>= 44px) + consistent row heights: one height token so tabs, dropdowns, search, Models + export all render at 44px */
  .controls {
    --ctl-h: 44px;
  }
  .controls>.tabs {
    height: var(--ctl-h);
    min-height: var(--ctl-h);
    padding: 4px;
    align-items: stretch;
  }
  .controls>.tabs .tab {
    padding: 6px 12px;
    min-height: 0;
    height: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1.2;
  }
  .pillar-select,
  .search,
  .pillar-dd-btn,
  .controls .chart-pick,
  .chart-pick,
  .chart-select {
    height: var(--ctl-h);
    min-height: var(--ctl-h);
  }
  .pillar-dd-btn,
  .controls .chart-pick {
    display: inline-flex;
    align-items: center;
  }
  .controls>.pillar-dd,
  .controls-end .chart-pick-wrap {
    height: var(--ctl-h);
  }
  .controls>.pillar-dd .pillar-dd-btn,
  .controls-end .chart-pick-wrap .chart-pick {
    height: 100%;
    min-height: 0;
  }
  .controls-end {
    align-items: stretch;
  }
  .controls-end .search-field {
    height: var(--ctl-h);
  }
  .controls-end .search-field .search {
    height: 100%;
    min-height: 0;
  }
  #lb-export {
    height: var(--ctl-h);
    min-height: var(--ctl-h);
    align-self: auto;
  }

  .chart-pick,
  .chart-select {
    padding-top: 6px;
    padding-bottom: 6px;
  }

  /* Leaderboard controls pack into three rows instead of five, so no single
     control is stretched across the full width of a phone (which made the
     selects look oversized):
       row 1  Text / Vision / Audio
        row 2  All 15 Pillars  +  All orgs  +  All types  (3-way)
       row 3  search  +  Models  +  export      (search flexes, icons fixed) */
  .controls>.tabs {
    flex: 1 1 100%;
  }

  .controls>.tabs .tab {
    flex: 1 1 auto;
  }

  /* the pillar listbox and the two selects share one row; each gives up the gaps
     between them. .pillar-dd is the flex child now (it replaced the <select>). */
  .controls>.pillar-dd,
  .controls>.type-select,
  .controls>.provider-select {
    flex: 1 1 calc(33.33% - 8px);
    width: auto;
    max-width: none;
    font-size: .82rem;
  }

  .controls-end {
    flex: 1 1 100%;
    flex-wrap: nowrap;
  }

  /* search absorbs the leftover width and is allowed to shrink below its own
     basis so the Models + export buttons keep their tap targets intact */
  .controls-end .search-field {
    flex: 1 1 60px;
    min-width: 0;
  }

  /* the input inside it must be allowed to shrink too, or the wrapper just
     squeezes the text instead of the box */
  .controls-end .search-field .search {
    min-width: 0;
  }

  /* chart controls pack like the leaderboard filter row instead of each select
     stretching across the full width of the card:
       row 1  Text / Vision / Audio
       row 2  organization (full width; the highlight moved into the picker star)
       row 3  top-N + count, then Select models + export */
  /* .chart-controls is `display: contents` at this width, so its own row-gap is
     inert - the row spacing now comes from .chart-head in the 720px block. */

  /* #chart-tabs, not .chart-tabs: the Bars/Pareto switch carries the same class
     but is a 2-tab control that must stay on one line with the row below it */
  .chart-controls>#chart-tabs {
    flex: 1 1 100%;
  }

  .chart-controls>#chart-tabs .tab {
    flex: 1 1 auto;
  }

  .chart-controls>.chart-select {
    flex: 1 1 calc(50% - 5px);
    min-width: 0;
    height: var(--ctl-h);
  }
  /* only the organization filter sits at this level now (featured moved into
     the picker star), so it takes the whole row instead of half of it */
  .chart-controls>.pillar-dd {
    flex: 1 1 100%;
    width: auto;
    max-width: none;
    min-width: 0;
    height: var(--ctl-h);
  }
  .chart-controls>.pillar-dd .pillar-dd-btn {
    height: 100%;
    min-height: 0;
    font-size: .85rem;
  }

  .chart-count-wrap {
    flex: 1 1 calc(50% - 5px);
    min-width: 0;
    height: var(--ctl-h);
  }
  .chart-count-wrap .chart-select {
    min-width: 0;
  }
  .chart-count-wrap>.pillar-dd {
    flex: 1 1 auto;
    min-width: 0;
    height: 100%;
  }
  .chart-count-wrap .pillar-dd .pillar-dd-btn {
    height: 100%;
    min-height: 0;
    font-size: .85rem;
  }

  .chart-pick-wrap {
    flex: 1 1 auto;
  }

  .chart-pick-wrap .chart-pick {
    width: 100%;
  }

  .pareto-bar {
    row-gap: 8px;
  }

  .pareto-bar>.chart-select,
  .pareto-bar>.pillar-dd {
    flex: 1 1 calc(50% - 5px);
    width: auto;
    max-width: none;
    min-width: 0;
    height: var(--ctl-h);
  }
  .pareto-bar>.pillar-dd .pillar-dd-btn {
    height: 100%;
    min-height: 0;
    font-size: .85rem;
  }

  /* "+ Select models" opens directly under its own button rather than as the
     bottom sheet above. Same reasoning as #compare-picker below: an anchored
     panel reads as the menu for the control that was just tapped, while a sheet
     pinned to the bottom of the screen is detached from it and buries the chart
     it is filtering. Left+right anchor to the trigger so a narrow phone cannot
     push the panel off either edge. */
  /* same footprint as the leaderboard model filter (#model-filter below), so
     both model pickers read as one menu on a phone: 300px wide, right-anchored
     under the trigger, up to 62dvh tall. The old trigger-width box squeezed the
     count + All/None header into a wrapped 2-line stub. */
  #chart-picker {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    bottom: auto;
    left: auto;
    width: min(300px, calc(100vw - 32px));
    max-height: min(62vh, 380px);
    max-height: min(62dvh, 380px);
    z-index: 60;
  }

  /* keep the header on one line at the 300px width */
  #chart-picker .pk-head {
    flex-wrap: nowrap;
  }

  #chart-picker .compare-count {
    white-space: nowrap;
  }

  /* the grab-bar is a bottom-sheet affordance, not an anchored dropdown's */
  #chart-picker::before {
    display: none;
  }

  /* flipped upward when the list will not fit below the trigger, set by
     chart.js alongside its open/close handling */
  #chart-picker.open-up {
    top: auto;
    bottom: calc(100% + 6px);
  }


  /* The leaderboard Models filter anchors directly under its trigger button
     just like #chart-picker and #compare-picker. */
  #model-filter {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    bottom: auto;
    left: auto;
    width: min(300px, calc(100vw - 32px));
    max-height: min(62vh, 380px);
    max-height: min(62dvh, 380px);
    z-index: 60;
  }

  #model-filter::before {
    display: none;
  }

  /* The Head-to-Head picker opens directly under its own button rather than
     as the bottom sheet above. On this card the trigger sits right on top of
     the subtitle, so an anchored panel reads as the menu for the button that
     was just tapped; a sheet pinned to the far bottom of the screen is
     detached from it. Left+right anchor to the full-width trigger, which is
     what stops the panel running off a narrow phone's edge. */
  #compare-picker {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    bottom: auto;
    left: 0;
    width: auto;
    max-height: 48vh;
    max-height: min(48dvh, 320px);
    z-index: 60;
  }

  /* the grab-bar is a bottom-sheet affordance, not an anchored dropdown's */
  #compare-picker::before {
    display: none;
  }

  /* Visible columns are rank + model (frozen) and score, cost, tokens, latency,
     date (scrolling). The min-width has to cover all of them, otherwise the
     date is reachable only by squeezing the last column past the card edge. */
  .lb-table {
    min-width: 616px;
  }

  /* Freeze the rank + model columns. Without this the model name scrolls out of
     view and a horizontally scrolled row cannot be identified at all, because
     the number and the name live in the same row. Everything to the right
     (score, cost, tokens, latency, date) scrolls as one block.
     Both frozen cells need an OPAQUE background - the row stripe is a
     transparent odd row, so a transparent sticky cell would let the scrolled
     columns show straight through it. */
  .lb-table th:nth-child(1),
  .lb-table td:nth-child(1) {
    position: sticky;
    left: 0;
    z-index: 3;
    width: 44px;
    min-width: 44px;
    padding-inline: 8px;
    background: var(--navy);
  }

  .lb-table th:nth-child(2),
  .lb-table td:nth-child(2) {
    position: sticky;
    left: 44px;
    z-index: 3;
    /* Narrower than the 200px desktop width: the name is clamped to 2 lines
       (see .model-name) so a short column no longer buys extra wrapping. */
    width: 156px;
    min-width: 156px;
    max-width: 156px;
    background: var(--navy);
  }

  /* the frozen body cells take the row stripe, matching their row exactly */
  .lb-table tbody td:nth-child(1),
  .lb-table tbody td:nth-child(2) {
    background: var(--navy-card);
  }

  .lb-table tbody tr:nth-child(even) td:nth-child(1),
  .lb-table tbody tr:nth-child(even) td:nth-child(2) {
    background: var(--row-alt);
  }

  /* hairline + soft shadow on the frozen edge so it reads as pinned, not broken */
  .lb-table th:nth-child(2),
  .lb-table td:nth-child(2) {
    box-shadow: 1px 0 0 var(--border), var(--col-shadow);
  }

  /* frozen HEADERS stay above the horizontally scrolling headers. When the table
     is capped every thead th is sticky (top) with z-index 5, which painted TEXT
     SCORE / COST over the pinned # + MODEL headers (all were z-index 3). 7 wins
     over the scrolling headers; body cells stay at 3 so the header row still
     covers them on vertical scroll. top: 0 keeps the vertical freeze working. */
  .lb-table thead th:nth-child(1),
  .lb-table thead th:nth-child(2) {
    top: 0;
    z-index: 7;
  }

  /* keep the hover outline from being clipped by the frozen columns */
  .lb-table tbody tr:hover td:nth-child(1),
  .lb-table tbody tr:hover td:nth-child(2) {
    background: var(--navy-card);
  }

  .header-inner {
    padding-block: 8px;
  }

  .site-footer {
    padding: 18px 0;
  }

  .modal {
    padding: 12px;
  }

  .modal-card {
    /* matches the base rule; .modal is padded 12px here, hence -24px */
    padding: 0 16px 18px;
    max-height: calc(100vh - 24px);
    max-height: calc(100dvh - 24px);
  }
}

/* ---------- phone polish (360px class) ----------
   Everything here is scoped to the phone range, so the desktop layout that was
   tuned at 1200px+ is untouched. */

@media (max-width: 640px) {

  /* the sticky header eats a lot of a short phone screen; the logo and the two
     icon buttons are the only things in it, so the block shrinks to them */
  .brand-logo {
    max-height: 36px;
  }

  /* the chart tooltip is a hover affordance, but a tap-drag on a phone fires
     mousemove too, so it still has to fit the narrow card */
  .chart-tip {
    min-width: 0;
    max-width: calc(100vw - 32px);
    font-size: .8rem;
  }

  /* A phone has no hover, so a pressed row needs a visible cue (the desktop
     hover tint never fires on touch). Matches the desktop hover fill so the two
     read as the same interaction. */
  .lb-table tbody tr:active td {
    background: var(--row-alt);
  }

  .lb-table tbody tr:active td:nth-child(1),
  .lb-table tbody tr:active td:nth-child(2) {
    background: var(--row-alt);
  }

  /* 30px square is a mouse target, not a finger one */
  .modal-close,
  .modal-compare-btn,
  .modal-export {
    width: 44px;
    height: 44px;
  }

  .modal-close .icon,
  .modal-compare-btn .icon,
  .modal-export .icon {
    width: 20px;
    height: 20px;
  }

  /* picker rows: 5px padding gives a ~30px hit area, below the 44px minimum */
  .pk-item {
    padding: 10px 8px;
  }

  /* finger-sized star, still inside the 44px+ row */
  .pk-star {
    width: 44px;
    height: 44px;
  }

  .pk-star svg {
    width: 19px;
    height: 19px;
  }

  /* Grab-bar for the bottom-sheet presentation. #chart-picker and #compare-picker
     cancel it with their own ::before rules, because they anchor to a button and
     are not dismissible sheets - a drag handle would be misleading there. */
  .chart-picker::before {
    content: "";
    display: block;
    width: 36px;
    height: 4px;
    border-radius: 999px;
    background: var(--border);
    margin: 8px auto 4px;
    flex: none;
  }

  .chart-picker .pk-head {
    padding-top: 6px;
  }
}

@media (max-width: 420px) {

  /* The comparison bar is one labelled button plus an icon button, which does fit
     2-up at this width - but the labelled one should take the slack so the pair
     fills the row evenly instead of leaving a gap. The icon button is excluded:
     it is icon-only, and letting it flex would stretch it across the card. */
  .compare-controls .chart-pick-wrap {
    flex: 1 1 auto;
  }

  .compare-controls .chart-pick-wrap .chart-pick {
    width: 100%;
  }

  .compare-controls .cmp-export-btn {
    flex: none;
  }
}

/* very narrow phones: trim the last few pixels of chrome */
@media (max-width: 380px) {
  .tab {
    padding: 10px 9px;
    font-size: .92rem;
  }

  .brand-logo {
    max-width: 42vw;
  }
}

/* ---------- notched phones (last word on the safe-area insets) ----------
   These live at the end on purpose: .header-inner and .site-footer both use the
   `padding` shorthand further up, which would reset padding-top / padding-bottom
   back to 0 if these landed before them. viewport-fit=cover in the viewport meta
   lets the page paint into the cutout area, so the header and footer have to
   pad themselves clear of it. max() keeps the normal padding on every device
   without a safe area. */
.site-footer {
  padding-bottom: max(22px, env(safe-area-inset-bottom));
}

@media (max-width: 640px) {
  .site-footer {
    padding-bottom: max(18px, env(safe-area-inset-bottom));
  }
}

/* ---------- model comparison (modal) ----------
   The second model's radar overlay uses its own token pair so the light theme
   gets a legible blue instead of inheriting the dark-theme step. */
:root {
  --cmp-2: #38bdf8;
  --cmp-2-text: #7dd3fc;
  --cmp-2-hi: #bae6fd;
}

[data-theme="light"] {
  --cmp-2: #0b6fc4;
  --cmp-2-text: #0a5fa8;
  --cmp-2-hi: #38bdf8;
}

/* Collapsed until the toolbar button opens it, and laid out as a column: a
   search box above the select, because a long leaderboard makes scrolling a
   native dropdown a poor way to find one rival. The label and the match count
   share a head row, the same shape as .pk-head in the picker sheets. */
.modal-compare {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 10px 0 6px;
}

.modal-compare[hidden] {
  display: none;
}

.modal-compare-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.modal-compare-head label {
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

.modal-compare-count {
  font-size: .75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.modal-compare-search {
  width: 100%;
  box-sizing: border-box;
}

.modal-compare-search:focus {
  outline: none;
  border-color: var(--red-bright);
}

/* An input is always :focus-visible once focused, so the border change above
   already carries the state. A second 2px outline stacked into a thick red halo,
   so focus is signalled with a soft ring hugging the same border instead. */
.modal-compare-search:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--navy), 0 0 0 3px rgba(229, 57, 53, .28);
}

/* The results sit in a visible, scrollable list. .pk-list supplies the
   overflow and padding so the control matches the picker sheets elsewhere in
   the page; the cap keeps a long leaderboard from pushing the radar out of
   the card. */
.modal-compare-field {
  position: relative;
}

/* The results are a popover anchored to the field, not an in-flow block: they
   overlay the radar instead of pushing it down, and they dismiss on the first
   click anywhere outside, the way every other dropdown here already behaves. */
.modal-compare-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 20;
  max-height: 210px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--navy-card);
  box-shadow: var(--shadow);
}

.modal-compare-list[hidden] {
  display: none;
}

/* Chrome paints its own blue "x" for type=search, and blue appears nowhere in
   this theme. Hide it and use the themed clear button below instead. */
.modal-compare-search::-webkit-search-cancel-button,
.modal-compare-search::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

.modal-compare-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.modal-compare-clear[hidden] {
  display: none;
}

.modal-compare-clear .icon {
  width: 13px;
  height: 13px;
}

.modal-compare-clear:hover {
  background: var(--row-alt);
  color: var(--red-bright);
}

.modal-compare-clear:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: 1px;
}

/* The chevron and the clear glyph occupy the same corner, so exactly one of
   them is shown at a time. */
.modal-compare-field.has-value .modal-compare-chevron {
  display: none;
}

.modal-compare-chevron {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  color: var(--muted);
  /* decorative only: a click on it must still reach the input beneath */
  pointer-events: none;
}

.modal-compare-chevron .icon {
  width: 15px;
  height: 15px;
}

.modal-compare-search {
  padding-right: 34px;
}

.cmp-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  box-sizing: border-box;
  padding: 6px 9px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: .85rem;
  text-align: left;
  cursor: pointer;
}

.cmp-row:hover {
  background: var(--row-alt);
}

.cmp-row:focus-visible {
  outline: 2px solid var(--red-bright);
  outline-offset: -2px;
}

/* the active rival, matching the red overlay drawn on the radar */
.cmp-row.on {
  background: rgba(229, 57, 53, .14);
  color: var(--red-bright);
  font-weight: 700;
}

/* "None" is the resting state, not a pick. It gets its own neutral selected
   look, because the red accent must mean one thing only: this model is the
   one overlaid on the radar. A red "None" reads as an error/warning state. */
.cmp-row-none {
  color: var(--muted);
  font-style: italic;
}

.cmp-row-none.on {
  background: var(--row-alt);
  color: var(--text);
  font-style: normal;
  font-weight: 600;
}

.cmp-row-logo {
  display: inline-flex;
  flex: none;
}

.cmp-row-logo img {
  width: 16px;
  height: 16px;
  object-fit: contain;
}

[data-theme="dark"] .cmp-row-logo img,
[data-theme]:not([data-theme="light"]) .cmp-row-logo img {
  background: #ffffff;
  border-radius: 4px;
  padding: 1px;
}

.cmp-row-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cmp-row-score {
  flex: none;
  font-size: .78rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.cmp-row.on .cmp-row-score {
  color: var(--red-bright);
}

.cmp-empty {
  padding: 14px 10px;
  font-size: .85rem;
  color: var(--muted);
  text-align: center;
}

.radar-legend {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-size: .78rem;
  color: var(--muted);
  margin-bottom: 4px;
}

.radar-legend[hidden] {
  display: none;
}

.radar-legend span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 48%;
}

.radar-legend i {
  width: 12px;
  height: 3px;
  border-radius: 2px;
  background: var(--red-bright);
  flex: none;
}

.radar-legend i.c2 {
  background: var(--cmp-2);
}

.radar-legend span.c2 {
  color: var(--cmp-2-text);
}

.modal-diff {
  margin-top: 12px;
  border-top: 1px solid var(--border);
  padding-top: 10px;
  font-size: .85rem;
}

.modal-diff[hidden] {
  display: none;
}

.modal-diff .diff-head {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 6px;
}

.modal-diff .diff-head em {
  font-style: normal;
  color: var(--muted);
  font-weight: 600;
  font-size: .78rem;
}

.modal-diff .diff-row {
  /* stagger index for the one-shot bar sweep, set inline by renderCompareUI */
  --i: 0;
  padding: 5px 0;
  color: var(--muted);
}

/* title + delta on the top line, the tug-of-war bar on the one below */
.modal-diff .diff-row .dtop {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.modal-diff .diff-row .dn {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.modal-diff .diff-row .dbot {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
}

.modal-diff .diff-row .d1,
.modal-diff .diff-row .d2 {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  min-width: 40px;
  font-size: .8rem;
}

.modal-diff .diff-row .d1 {
  color: var(--red-bright);
  text-align: right;
}

.modal-diff .diff-row .d2 {
  color: var(--cmp-2-text);
  text-align: left;
}

/* The bar: each side fills inward from its own edge toward the centre, so the
   two colours meet head-on and the longer fill wins the pillar. The tick is the
   line they are fighting over. */
.modal-diff .dbar {
  position: relative;
  flex: 1;
  height: 8px;
  min-width: 60px;
  border-radius: 999px;
  background: rgba(154, 161, 181, .16);
  overflow: hidden;
}

.modal-diff .dbar::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--navy-card);
  opacity: .85;
}

.modal-diff .dbar i {
  position: absolute;
  top: 0;
  bottom: 0;
  display: block;
}

.modal-diff .dbar i.a {
  left: 0;
  border-radius: 999px 0 0 999px;
  background: var(--red-bright);
}

.modal-diff .dbar i.b {
  right: 0;
  border-radius: 0 999px 999px 0;
  background: var(--cmp-2);
}

/* One-shot reveal: both fills grow out of their own edge and meet at the
   centre tick, so the row reads as a tug-of-war being decided rather than two
   numbers appearing. scaleX is used instead of animating width, which would
   relayout every row on every frame; each side grows from its own origin, and
   the winner simply arrives first because its final length is longer. */
@keyframes cmp-sweep-a {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

@keyframes cmp-sweep-b {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* Each row animates on its own when it scrolls into view (see playBarSweep), so
   the class is on the row rather than the list. Rows revealed in the same
   moment still cascade a little, via --i, so a batch arriving together reads
   as one gesture instead of a flat snap. The stagger is capped in JS, because
   with a per-row trigger an uncapped index would leave a late row waiting on a
   delay it can never see. */
.modal-diff .diff-row.sweep .dbar i.a,
.modal-diff .diff-row.sweep .dbar i.b {
  transform: scaleX(0);
  animation-duration: 520ms;
  animation-timing-function: cubic-bezier(.22, .68, .32, 1);
  animation-fill-mode: both;
  animation-delay: calc(var(--i, 0) * 35ms);
}

.modal-diff .diff-row.sweep .dbar i.a {
  transform-origin: left center;
  animation-name: cmp-sweep-a;
}

.modal-diff .diff-row.sweep .dbar i.b {
  transform-origin: right center;
  animation-name: cmp-sweep-b;
}

/* The centre tick draws in just ahead of the fills, which is what makes the two
   colours look like they are meeting on it rather than crossing it. */
@keyframes cmp-tick-in {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}

.modal-diff .diff-row.sweep .dbar::after {
  transform-origin: center;
  animation: cmp-tick-in 220ms ease-out both;
  animation-delay: calc(var(--i, 0) * 35ms);
}

/* the side that took the pillar glows a little, so a scan down the list picks
   out the wins without reading a single number */
.modal-diff .diff-row.win .dbar i.a,
.modal-diff .diff-row.lose .dbar i.b {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .25) inset;
}

.modal-diff .diff-row .dd {
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  min-width: 52px;
  text-align: right;
  padding: 1px 6px;
  border-radius: 999px;
  font-size: .78rem;
  background: rgba(154, 161, 181, .14);
  color: var(--muted);
}

.modal-diff .diff-row.win .dd {
  background: rgba(46, 158, 91, .18);
  color: var(--good);
}

.modal-diff .diff-row.lose .dd {
  background: rgba(198, 40, 40, .14);
  color: var(--red-bright);
}

/* ---------- starred (highlighted) leaderboard row ----------
   Appended at the end of the file on purpose: these equal-specificity rules
   must beat the zebra stripes, the hover fill and the mobile frozen-column
   fills declared further up. The gold mark is the same star the Models
   filter / chart picker / compare picker use (.pk-star). */
.lb-table tbody tr.lb-starred td {
  background: var(--star-row-bg);
}

.lb-table tbody tr.lb-starred td:nth-child(1),
.lb-table tbody tr.lb-starred td:nth-child(2) {
  background: var(--star-row-bg);
}

.lb-table tbody tr.lb-starred td:first-child {
  box-shadow: inset 4px 0 0 var(--gold-text);
}
