/* One stylesheet, no framework, no build step.
 *
 * A financial data site is tables. Everything here serves reading a table:
 * tabular figures that line up, right-aligned numbers, a horizontally scrollable
 * wrapper so a wide table never makes the page scroll sideways, and enough
 * contrast to read a column of grey numbers.
 *
 * Cache-busted by templates.py's css_version (md5 of this file), which matters
 * as soon as anything caches it. */

/* --- the typefaces -------------------------------------------------------- */

/* IBM Plex, served from our own origin. Three files, 76KB, latin only.
 *
 * **Self-hosted rather than from Google Fonts**, for the reason htmx is
 * vendored: a stylesheet from `fonts.googleapis.com` tells a third party the IP
 * and User-Agent of every visitor to every page, and puts a second host in the
 * critical path of the first paint. These are two requests to the host that is
 * already serving the page.
 *
 * **The sans is one variable file covering 400-700.** Google serves Plex Sans
 * as a variable font with a `wght` axis, so four weights are one 46KB download
 * instead of four of them; `font-weight: 100 700` is what tells the browser it
 * can interpolate rather than synthesising a fake bold. Plex Mono has no
 * variable cut, hence two files for the two weights the site actually uses.
 *
 * **Latin only, and `unicode-range` says so.** Company names and filing prose
 * out of EDGAR are ASCII; the glyphs the UI draws that are outside this range
 * anyway - the ↺ on a filtered heading, the ▾ on a picker, the ✓ in a pill -
 * come from the fallback stack, exactly as they did before there was a webfont
 * here. Declaring the range is what keeps the browser from downloading these
 * files to render a character they do not contain.
 *
 * `font-display: swap`: the page renders immediately in the system stack and
 * swaps when the file lands. A table of figures that is invisible for 200ms is
 * worse than one that reflows once. */
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-sans-var-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/ibm-plex-mono-600-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  --bg: #ffffff;
  --surface: #f7f8fa;
  --border: #e3e6ea;
  /* The rules *inside* the screener table, lighter than --border on purpose:
   * see `table.data.dense th + th`. */
  --grid: #eef0f3;
  --text: #14181f;
  --muted: #626b78;
  --accent: #12507e;
  --pos: #17703f;
  --neg: #a02020;
  --radius: 6px;
  /* The larger of the two, for anything that reads as a surface rather than as
   * a control: a table's box, a panel, a card. Two radii and not one, because a
   * 6px corner on a 900px-wide table box is a corner you cannot see, and a 10px
   * one on a 22px input is a lozenge. */
  --radius-lg: 10px;
  /* The system stacks stay behind Plex as the fallback: the page has to be
   * readable before 76KB of font arrives, and identical if it never does. */
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #101318;
    --surface: #171b22;
    /* Lifted a shade from the first version of these. A border at #262c35 on a
     * #101318 ground is about 1.3:1, which is a line you can only see where it
     * is long - so a table read as a grid and a card read as nothing at all. */
    --border: #2d343f;
    --grid: #232a33;
    --text: #e7eaee;
    --muted: #98a1ae;
    --accent: #6fb3e8;
    --pos: #4cba7c;
    --neg: #e07a7a;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }

/* The gap under the sticky header, and it is owned here rather than by the
 * first heading of each page. Every page used to start with an `h1` whose
 * `margin-top` was the only thing between it and the header - so a page whose
 * first element was a hero, a company header or a `.prose` article started at
 * a different height from the one beside it, and the company page started
 * about 20px too close to the bar above it. One rule, every page, and `h1`
 * has no top margin (below) so the two cannot add up. */
main.wrap { padding-top: 32px; padding-bottom: 24px; }

/* The screener gets the window, and the header and footer come with it: 1180px
 * is the right measure for prose and about half the right measure for a table
 * of thirty columns of figures, and a full-width table under a 1180px header
 * reads as a page that has come apart. Capped rather than unbounded because a
 * table stretched across a 34-inch monitor puts the ticker and the last column
 * a head-turn apart. */
body.page-screener .wrap { max-width: 1760px; padding: 0 20px; }

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* No top margin on an `h1`: `main.wrap` owns the space under the header, and a
 * margin here as well is that gap twice on every page that opens with one. */
h1 { font-size: 1.7rem; line-height: 1.2; letter-spacing: -0.02em; margin: 0 0 0.9rem; }
h2 { font-size: 1.15rem; line-height: 1.3; letter-spacing: -0.01em; margin: 2.2rem 0 0.9rem; }
h3 { font-size: 1rem; margin: 0; }

/* The as-of clause that sits inside a heading - "FY2025 figures against the
 * last close". It is set small and muted, and every other property was
 * inherited from the heading, so it also came out bold: a whole sentence in
 * small bold grey, which reads as a second heading rather than as a caption on
 * the first. */
h1 .asof, h2 .asof, h3 .asof { font-weight: 400; letter-spacing: normal; }

/* --- header --------------------------------------------------------------- */

.site-header {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 20;
}
.site-header .wrap {
  display: flex;
  align-items: center;
  gap: 12px 28px;
  min-height: 60px;
  padding-top: 8px;
  padding-bottom: 8px;
  flex-wrap: wrap;
}
.logo { font-weight: 700; letter-spacing: -0.02em; color: var(--text); }
.site-header nav { display: flex; gap: 22px; }
.site-header nav a { color: var(--muted); }
.site-header nav a:hover { color: var(--text); }
.site-header nav a.active { color: var(--text); font-weight: 600; }

.nav-search { margin-left: auto; position: relative; }
.nav-search input { min-width: 260px; }

input, select, button {
  font: inherit;
  padding: 7px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
}
/* One focus ring, on everything, and it is the same ring the pills and the
 * summary handles draw for themselves. Without it a keyboard reader gets
 * whatever the browser's default is over our own backgrounds - which in Chrome
 * is a dark ring that is invisible in dark mode. */
input:focus-visible, select:focus-visible, button:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
input:hover:not(:disabled), select:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
button {
  background: var(--accent);
  color: #fff;
  border-color: transparent;
  cursor: pointer;
  font-weight: 600;
  transition: background-color 90ms ease;
}
button:hover { background: color-mix(in srgb, var(--accent) 88%, var(--text)); }
.btn-secondary {
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--muted);
}
/* The class was written for a link - "Reset all" - where there is no background
 * to undo. On a <button> the rule above it has already set the accent fill and
 * white text, and `.btn-secondary` overrides only the colour: muted grey on
 * accent blue, which is a button you cannot read. The background and the weight
 * have to come back with it. */
button.btn-secondary {
  background: var(--bg);
  border-color: var(--border);
  font-weight: 500;
}
button.btn-secondary:hover, button.btn-secondary:focus-visible {
  border-color: var(--accent);
  color: var(--text);
}

/* --- typeahead ------------------------------------------------------------ */

.suggestions {
  position: absolute;
  z-index: 30;
  top: 100%;
  left: 0;
  right: 0;
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
  overflow: hidden;
}
.suggestions a {
  display: grid;
  grid-template-columns: 74px 1fr auto;
  gap: 12px;
  padding: 9px 14px;
  color: var(--text);
}
/* A rule between rows rather than only a hover: five names in a 400px box with
 * nothing between them is one paragraph until the pointer lands on it. */
.suggestions li + li a { border-top: 1px solid var(--grid); }
.suggestions a:hover { background: var(--surface); text-decoration: none; }
.suggestions .ticker { font-family: var(--mono); font-weight: 600; }
.suggestions .cap { color: var(--muted); font-family: var(--mono); font-size: 0.85em; }

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

/* Wide content scrolls inside its own box. The page body must never scroll
 * sideways - a 20-column screener on a phone otherwise makes the whole layout
 * draggable. */
.table-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); }
/* A table straight under the page's own heading - the search results - needs the
 * room an intervening paragraph would otherwise have given it. */
h1 + .table-scroll, h1 + .empty { margin-top: 20px; }

table.data { width: 100%; border-collapse: collapse; font-size: 0.92rem; }
table.data th, table.data td { padding: 9px 14px; border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; }
table.data thead th { background: var(--surface); font-weight: 600; position: sticky; top: 0; }
/* The header row is the strongest line in the table, and the row rules under it
 * the lightest: two weights, so the eye reads the heading as a boundary and the
 * rows as a grid rather than everything as the same fence. */
table.data:not(.dense) thead th { border-bottom: 2px solid var(--border); }
/* Only the small tables get the lighter row rules. A six-row table three across
 * on the front page is read one row at a time and full-strength rules there are
 * a fence around every line; a statement table is eleven columns by forty rows
 * and the rules are the thing carrying the eye across it. */
table.data.compact tbody th, table.data.compact tbody td { border-bottom-color: var(--grid); }

/* --- the grid inside a full-size table ------------------------------------ */

/* A rule between every pair of columns, for the same reason the screener has
 * them: ten fiscal years of right-aligned figures with only whitespace between
 * them is a grid the reader has to hold together themselves, and the eye slides
 * one column left on the way back from FY2016 - so FY2019's revenue gets read
 * as FY2018's. `--grid` is the lighter tint, so the outer box and the header
 * still read as the stronger lines.
 *
 * **The line between the labels and the figures is the heavy one.** It is the
 * only column boundary that separates two different kinds of thing rather than
 * two of the same, and it is the one a reader uses to find their place: the row
 * name on one side, forty numbers on the other. `:not(.num) + .num` is that
 * boundary wherever it falls - after "Fiscal year" in a statement's header,
 * after the row label in its body, after the company name in the peers table -
 * without any table having to say which column number it is. */
table.data:not(.dense):not(.compact) th + th,
table.data:not(.dense):not(.compact) td + td { border-left: 1px solid var(--grid); }
table.data:not(.dense):not(.compact) :not(.num) + .num { border-left: 2px solid var(--border); }
table.data tbody tr:hover { background: var(--surface); }
/* The last row's rule and the wrapper's own border are two lines a pixel apart,
 * which is the one place a bordered table looks unfinished. Only inside the
 * wrapper: a table with no box around it needs that last rule to close it. */
.table-scroll table.data tbody tr:last-child > th,
.table-scroll table.data tbody tr:last-child > td { border-bottom: 0; }
table.data td.name { white-space: normal; min-width: 220px; }
table.data .ticker a { font-family: var(--mono); font-weight: 600; }

/* The screener's own table: up to thirty columns by two hundred rows, where the
 * padding that makes a six-row table on the front page comfortable is a page of
 * scrolling. The name is clipped rather than wrapped for the same reason - one
 * long name wrapping to three lines sets the height of its whole row - and the
 * cell carries the full name as a `title` so nothing is actually lost. */
/* Every measurement that decides how wide a column is, in one place, because a
 * column's width is not one number: it is the cell padding, the size of the
 * figures, the two filter boxes under the heading and the heading's own label,
 * and changing one without the others moves nothing. Two 50px boxes and a gap
 * set the floor under every numeric column on this table, so those are the
 * pixels that matter and they are named here rather than buried in the filter
 * rules two hundred lines down.
 *
 * The reader picks between three sets of them with `?density=` - see
 * `screener_columns.parse_density` and `.d-narrow` / `.d-wide` below. Custom
 * properties rather than a wall of overrides: the density class only has to win
 * the cascade for the *variable*, which it does by having one more class than
 * the rule that sets the default, and every rule that uses one is written
 * once. */
table.data.dense {
  --cell-x: 9px;
  --cell-y: 6px;
  --cell-font: 0.86rem;
  --filter-font: 0.78rem;
  --filter-pad: 2px 5px;
  --filter-gap: 2px;
  /* Wide enough for "$999.9B" typed as 999.9B and no wider. */
  --box-w: 50px;
  --text-w: 118px;
  --select-w: 128px;
  --search-w: 160px;
  --name-min: 210px;
  --name-max: 320px;
  /* Headings do not wrap unless a density says they may - see `.d-narrow`. */
  --label-wrap: nowrap;
  --label-max: none;
}
table.data.dense th, table.data.dense td { padding: var(--cell-y) var(--cell-x); font-size: var(--cell-font); }
table.data.dense td.name { white-space: nowrap; min-width: var(--name-min); max-width: var(--name-max); overflow: hidden; text-overflow: ellipsis; }

/* Narrow: everything in a notch, and the heading label allowed to wrap.
 *
 * The wrap is the one that actually buys width. "Free cash flow yield" on one
 * line is 140px of column whatever the boxes below it do, and a heading is the
 * one thing here that can be two lines without costing a row: they are all the
 * same height already, because the tools strip and the filter block make them
 * so. Capped rather than free, since `white-space: normal` on its own changes
 * nothing - the table sizes to its content and scrolls, so a label only wraps
 * where something tells it where to stop. */
table.data.dense.d-narrow {
  --cell-x: 6px;
  --cell-y: 4px;
  --cell-font: 0.8rem;
  --filter-font: 0.74rem;
  --filter-pad: 1px 4px;
  --box-w: 44px;
  --text-w: 96px;
  --select-w: 104px;
  --search-w: 124px;
  --name-min: 150px;
  --name-max: 230px;
  --label-wrap: normal;
  --label-max: 96px;
}

/* Wide: the figures given room, for a reader with four columns on the table
 * rather than twenty. The slack column takes what is left over either way, so
 * this is not "spread six columns across 1700px" - that is what the slack is
 * there to prevent. */
table.data.dense.d-wide {
  --cell-x: 13px;
  --cell-y: 8px;
  --cell-font: 0.9rem;
  --filter-font: 0.8rem;
  --filter-pad: 3px 6px;
  --box-w: 58px;
  --text-w: 140px;
  --select-w: 150px;
  --search-w: 190px;
  --name-min: 240px;
  --name-max: 380px;
}

/* A rule between every pair of columns, and it earns its ink here in a way it
 * would not on the six-column table on the front page.
 *
 * Twenty columns of right-aligned figures with only whitespace between them is
 * a grid a reader has to hold together themselves: the eye slides one column
 * left on the way back from the far end of a row, and a P/E gets read as a P/B.
 * The horizontal rules were already doing that job for rows. `--grid` is a
 * lighter tint than `--border` so the outer box and the header still read as
 * the stronger lines - a full-strength rule in both directions turns the table
 * into graph paper and is harder to read than none at all.
 *
 * The last real column gets none, because the slack cell to its right is not a
 * column and a rule against its left edge draws a boundary around nothing. */
table.data.dense th + th, table.data.dense td + td { border-left: 1px solid var(--grid); }
table.data.dense th.pad, table.data.dense td.pad { border-left: 0; }
/* The header row sits above the body's grid rather than in it. */
table.data.dense thead th { border-bottom: 2px solid var(--border); }

/* The ticker stays put while the figures scroll under it. Reading across twenty
 * columns with the ticker off the left edge is reading a row of numbers that
 * belongs to nobody. The background is explicit on every state because a sticky
 * cell is painted over the cells sliding beneath it - transparent would show
 * both at once - and the header's corner cell needs the higher z-index because
 * it is sticky in both directions at once. */
table.data.dense td.ticker, table.data.dense th.ticker {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
  box-shadow: 1px 0 0 var(--border);
}
table.data.dense thead th.ticker { top: 0; z-index: 3; background: var(--surface); }
/* See the template: this is the cell that eats the leftover width. */
table.data.dense .pad { width: 100%; padding: 0; }

/* The + at the end of the header row.
 *
 * Adding a column used to mean knowing that a picker existed, finding it above
 * the table, and opening it - three steps away from the place a reader is
 * looking when the thought occurs, which is the right-hand end of the headings
 * they already have. So the slack cell carries a +, next to the last figure on
 * the table, and it opens the figure picker on the panel above.
 *
 * It is a real <a href="#column-picker">, so with scripting off it still takes
 * the reader to the picker (see `.picker:target`), and it is a link rather than
 * a button because it is a jump to somewhere on this page and nothing else -
 * a <button> here would submit the form it is not in and mean nothing without
 * JavaScript. Dashed until hovered: it is an empty slot inviting a column, not
 * a control with a state.
 *
 * The cell it sits in is the one that eats the leftover width, so on a
 * six-column table the + is a hand's width from the last heading rather than at
 * the far right of the screen: `text-align: left` is doing that. */
table.data.dense th.pad { padding: 2px 8px; text-align: left; }
/* On the line of the headings rather than at the top of the cell: the header is
 * three rows tall now - the column's tools, its label, its filters - and a +
 * floating level with the tools reads as one of them. */
table.data.dense th.pad .add-col { margin-top: 16px; }
.add-col {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 1px dashed var(--muted);
  border-radius: 50%;
  background: var(--bg);
  color: var(--muted);
}
.add-col:hover, .add-col:focus-visible {
  border-style: solid;
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--bg));
  color: var(--accent);
  text-decoration: none;
}

/* The + itself, drawn rather than typed - shared with the pickers' handles.
 *
 * A "+" character is not centred in its own line box: it sits on the font's
 * math axis, which is above the middle, so flex-centring the text still leaves
 * the glyph visibly high inside a circle - and how high depends on which font
 * of the stack the reader actually has. Two gradient bars are centred on the
 * box by construction and cannot drift. `currentColor` is what keeps them
 * following the hover and open states set on the element around them. */
.plus-mark {
  display: block;
  width: 9px;
  height: 9px;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 100% no-repeat;
}
table.data.dense tbody tr:hover td.ticker { background: var(--surface); }

/* Numbers right-aligned and tabular, so digits line up down a column. Without
 * font-variant-numeric a proportional "1" is narrower than a "0" and every
 * column is visibly ragged. */
.num { text-align: right !important; font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* Chosen columns that are words rather than figures - sector, industry, state.
 * A SIC description in a right-aligned monospace column is unreadable, and it
 * is a column the screener now lets a reader add. */
table.data td.text { white-space: normal; max-width: 260px; }
.pos { color: var(--pos); }
.neg { color: var(--neg); }

/* The label wraps only where a density says it may, and only up to the width
 * that density sets - `--label-wrap` and `--label-max`, both `nowrap`/`none`
 * everywhere else. It is a flex item in `.col-head`, so it is blockified and
 * `max-width` applies to it; `th` sets `white-space: nowrap` for the figures
 * and this is the one thing in the cell that is allowed out of it. */
th .sort { color: inherit; white-space: var(--label-wrap, nowrap); max-width: var(--label-max, none); }
th.num .sort { text-align: right; }
th .sort.current { color: var(--accent); }

/* The arrow beside a sorted heading's label. State, not a control of its own -
 * it is inside the heading's link, and it carries the accent the label does. */
th .sort-mark { margin-left: 5px; color: var(--accent); }

.result-count { color: var(--muted); font-size: 0.9rem; }
/* The count on the left, the top pager on the right, one line. `baseline` and
 * not `center`: the count is text and the pager is a row of boxes, and centring
 * them against each other leaves the sentence sitting visibly low. Wraps to two
 * lines on a narrow screen, where the pager then starts at the left edge. */
/* The bottom margin is not decoration: the pager's boxes would otherwise sit
 * directly on the table's top border and read as part of the header row. */
.result-bar { display: flex; align-items: baseline; justify-content: space-between;
              gap: 12px 16px; flex-wrap: wrap; margin-bottom: 16px; }
.result-bar .result-count { margin: 0; }
.result-bar .pagination { margin: 0; }
.empty {
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  color: var(--muted);
  max-width: 80ch;
}

/* No matches, said inside the table rather than instead of it. The filters are
 * in the headings, so replacing the table with a paragraph takes away the box
 * the reader would empty to get their rows back. Left-aligned and left-sticky:
 * the cell spans a scroller that can be 3000px wide, and a centred sentence in
 * that is a sentence off the side of the screen. */
table.data.dense tr.empty-row td {
  padding: 26px 12px;
  color: var(--muted);
  text-align: left;
  white-space: normal;
  border-left: 0;
}
table.data.dense tr.empty-row .empty-note { position: sticky; left: 0; max-width: 60ch; }
table.data.dense tbody tr.empty-row:hover td { background: transparent; }

/* --- pagination ----------------------------------------------------------- */

.pagination { display: flex; gap: 7px; margin: 18px 0; flex-wrap: wrap; }
.pagination a, .pagination .current {
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.9rem;
}
.pagination a:hover { border-color: var(--accent); text-decoration: none; }
.pagination .current { background: var(--accent); color: #fff; border-color: transparent; }
/* The bottom pager owns its line, so it centres under the table. The top one is
 * a flex item in `.result-bar` and stays where that puts it. */
.pagination-bottom { justify-content: center; margin-top: 22px; }

/* --- screener filters ----------------------------------------------------- */

.page-head h1 { margin-bottom: 0.5rem; }

/* Shown on a narrow screen only - see the template for why it is not an apology
 * for the layout. `display: none` here and block in the media query at the
 * bottom of this file, so the default is the quiet one. */
.wide-hint { display: none; }

/* The form is the whole screener now - the toolbar, the picker *and* the
 * results table, because the filters live in the table's own header. So it has
 * no box of its own: what used to be a bordered panel is `.filter-top`, one
 * line of chrome above the table, and everything else the reader touches is on
 * the table itself.
 *
 * That is what the filter panel was always trying to buy. Every rule in the old
 * one that looked like penny-pinching - 68px inputs, groups packed across
 * instead of down, a sentence moved behind an info icon - existed because a
 * panel that pushes the results off the bottom of the screen makes a reader
 * filter blind: they type a bound, scroll down to see what it did, scroll back
 * up to change it. Filters in the header cost no vertical space at all: the
 * heading was already there. */
.filters { margin-bottom: 18px; }

/* One line of chrome: the picker, the row count, Apply and Reset. `position:
 * relative` and the z-index are what the picker's sheet is hung on and what
 * puts it over the table's sticky header. */
.filter-top {
  position: relative;
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  padding: 11px 14px;
  margin-bottom: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
/* Apply and Reset pushed to the far end, so they are in the same place whatever
 * else is on the line. */
.filter-top .filter-actions { margin-left: auto; }
.filter-actions { display: flex; gap: 8px; align-items: center; }
.filter-top > * { min-width: 0; }

/* The rows-per-page box is the one control still shaped like the old panel's:
 * a fieldset with a legend, because it belongs to the page rather than to any
 * column. */
.filters fieldset.slicer { position: relative; border: 1px solid var(--border); border-radius: var(--radius); padding: 2px 8px 6px; margin: 0; background: var(--bg); }
.filters fieldset.slicer legend { font-size: 0.74rem; color: var(--muted); padding: 0 4px; white-space: nowrap; }
.filters fieldset.slicer select { padding: 3px 6px; font-size: 0.85rem; }

/* --- the filters, in the table's header ----------------------------------- */

/* A heading is three things stacked: the tools that move or remove the column,
 * the label that sorts it, and the controls that bound it. In that order down
 * the cell, because the label is the thing a reader is looking for and it stays
 * on the line their eye is already on - the tools sit above it in a strip that
 * is empty until they want it, and the boxes below, next to the figures they
 * filter. */
table.data.dense thead th { vertical-align: top; }
.col-head { display: flex; flex-direction: column; gap: 4px; }
/* A numeric column's heading is right-aligned over its figures, so everything
 * in it goes to the right-hand edge too. */
th.num .col-head { align-items: flex-end; }
th.num .col-filter { justify-content: flex-end; }

/* The move arrows and the ×.
 *
 * The strip is always there and always the same height, so a heading does not
 * change size when the pointer crosses it and the header row stays one rule
 * across the table. It is transparent until the heading is hovered or something
 * inside it has focus: three controls on each of thirty headings, all at full
 * strength, is a header made mostly of chrome, and the arrows are worth
 * exactly nothing until somebody wants to move that column.
 *
 * `:focus-within` is not garnish either - it is what makes them usable from a
 * keyboard, where there is no pointer to reveal anything. */
.col-tools {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  width: 100%;
  height: 18px;
}
/* The fade is on the arrows, not on the strip: the strip also carries the info
 * icon, which is not chrome - it is the one thing in a heading that answers a
 * question, and a reader cannot hover something they cannot see. */
.col-tools .tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 3px;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1;
  font-weight: 700;
  opacity: 0;
  transition: opacity 90ms ease;
}
th:hover .col-tools .tool, th:focus-within .col-tools .tool { opacity: 1; }
.col-tools .tool:hover, .col-tools .tool:focus-visible {
  background: color-mix(in srgb, var(--accent) 14%, var(--bg));
  color: var(--accent);
  text-decoration: none;
}
.col-tools .tool.drop:hover, .col-tools .tool.drop:focus-visible {
  background: color-mix(in srgb, var(--neg) 14%, var(--bg));
  color: var(--neg);
}

/* The reset, and it is the exception to the fade above: the arrows are worth
 * nothing until somebody wants to move a column, but this one is only rendered
 * when a bound is in force under that heading - and a filter that has emptied
 * the table is exactly when a reader needs to see the way out of it. Carrying
 * the accent for the same reason a filtering select does: it is state as much
 * as it is a control. */
.col-tools .tool.reset { opacity: 1; color: var(--accent); font-weight: 400; font-size: 0.95rem; }

/* Everything a heading has to explain, at its top-right corner.
 *
 * `position: relative` here because the bubble is placed against this box, and
 * it hangs to the **left**: the icon is at the right-hand edge of a column that
 * may itself be the right-hand edge of the table, and `.table-scroll` clips -
 * `overflow-x: auto` makes the other axis `auto` too, so a bubble that runs off
 * the column runs into a scrollbar rather than over the next column.
 *
 * A tooltip is also why a hovered heading is lifted above the ones after it. A
 * sticky `th` is a stacking context of its own, so the bubble - z-index and all
 * - is painted inside its own column, and the next column, being later in the
 * document, paints its background straight over it. The bubble was there the
 * whole time; half of it was behind the neighbour. */
.col-info { position: relative; display: inline-flex; align-items: center; margin-left: 3px; }
table.data thead th:hover, table.data thead th:focus-within { z-index: 3; }
.col-info .tip { left: auto; right: 0; }

/* The controls under one heading. A column with none renders an empty block, so
 * every heading is the same height and the header row still reads as a rule.
 *
 * `position: relative` on each control so an info bubble is placed against the
 * control rather than against the 13px icon - centred on the icon, a 230px
 * bubble puts half of itself off the edge of the screen for the first and last
 * columns of the table. */
.col-filter { display: flex; flex-wrap: wrap; gap: var(--filter-gap, 2px); min-height: 22px; }
.col-filter .slicer { position: relative; display: flex; align-items: center; gap: var(--filter-gap, 2px); }
.col-filter input, .col-filter select {
  padding: var(--filter-pad, 2px 5px);
  border-radius: 4px;
  font-size: var(--filter-font, 0.78rem);
  font-weight: 400;
  font-family: var(--sans);
}
/* Two of these plus the gap are the floor under every numeric column on the
 * table, so every pixel here is a pixel of every row - which is why the number
 * is a variable the reader can change rather than a constant. See the density
 * block at the top of the table rules for what each width has to fit. */
/* `flex: none`, because these are flex items in a box as wide as the column
 * above them: a heading narrower than two boxes and a gap shrank them below
 * `--box-w` instead of letting the column widen, so the same pair of boxes was
 * a different size under "P/E" than under "Market cap".
 *
 * And the spinner is inside the box. A `type="number"` control carries Chrome's
 * spin buttons within its own width - about 13px of it - so at `--box-w` the
 * placeholder "max" came out clipped to "ma", while the scaled box beside it,
 * which is `type="text"` and has no spinner (see `_controls.html` for why),
 * read fine at exactly the same width. The allowance goes on the boxes that
 * have a spinner rather than into `--box-w`, which is the floor under every
 * numeric column on the table including the ones that never needed it. */
.col-filter .slicer.range input { flex: none; width: var(--box-w, 50px); }
.col-filter .slicer.range input[type="number"] { width: calc(var(--box-w, 50px) + 14px); }
.col-filter .slicer.text input { width: var(--text-w, 118px); }
.col-filter .slicer.choice select { max-width: var(--select-w, 128px); }
/* The search box under Company is the one filter a reader comes to the page
 * looking for, so it is not squeezed to the width of a heading. */
th[data-col="name"] .col-filter .slicer.text input { width: var(--search-w, 160px); }
.col-filter input::placeholder { color: var(--muted); opacity: 0.7; }
/* A select with nothing chosen is showing its label as a placeholder; one with
 * a value chosen is a filter in force and says so, like the chip above the
 * table. */
.col-filter select { color: var(--muted); }
.col-filter .slicer.on select { color: var(--text); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, var(--bg)); }
.col-filter select option { color: var(--text); }
.col-filter input:not(:placeholder-shown) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 7%, var(--bg));
}

/* A value the browser thinks is wrong - a percentage that is not a whole one, a
 * `1.2.3B` in an amount box. The form carries `novalidate`, so the browser no
 * longer *refuses* one of these: it would have refused by silently halting
 * every request htmx makes from this form, from any box, until the offending
 * character was found. This is the refusal made visible instead - the box says
 * so where the reader is typing, the query still runs, and the parser drops
 * what it cannot read. Last, so it wins over the "in force" colouring above:
 * being wrong is the more useful thing to say about a box than being full. */
.col-filter input:invalid {
  border-color: var(--neg);
  background: color-mix(in srgb, var(--neg) 8%, var(--bg));
}

/* The picker, and its handle is a button.
 *
 * It used to be a <summary> that looked like what it is - a triangle and a line
 * of grey text - and the thing a reader most needs to do on this page, choosing
 * which figures the table shows, was the thing on it that did not look
 * clickable. There were two of these, side by side, until the filters moved
 * into the headings and the second list stopped existing. So the handle is
 * sized, bordered and filled like the buttons beside it, carries a + because
 * adding is what a reader comes to it for, and states its count in a badge.
 *
 * It is still a <details>/<summary>: the marker is hidden and a chevron of our
 * own is rotated in its place, which keeps the open/shut behaviour, the
 * keyboard handling and the scripting-off path exactly as they were. `list-
 * style: none` is what hides the marker in Firefox and current Chrome; the
 * ::-webkit- rule is for Safari, which still draws its own.
 *
 * Shut or open, the two handles sit side by side on one line and stay there.
 * The panel is taken out of flow and hung under both of them: as a full-width
 * flex item it pushed the other handle onto a line of its own, which moved it
 * out from under the cursor that had just opened this one, and it shoved the
 * results table down by 400px of pills every time somebody looked at the list.
 * `position: relative` here is what the panel is positioned against, and the
 * z-index is what puts it over the table's sticky header (z-index 3). */
.pickers { position: relative; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.pickers > .picker { flex: 0 0 auto; }
.filters .picker > summary {
  display: inline-flex;
  cursor: pointer;
  align-items: center;
  gap: 8px;
  padding: 6px 14px 6px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text);
  font-size: 0.85rem;
  font-weight: 600;
  margin-bottom: 0;
  list-style: none;
}
.filters .picker > summary::-webkit-details-marker { display: none; }
.filters .picker > summary:hover { border-color: var(--accent); color: var(--text); }
.filters .picker > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Open, the handle reads as pressed - and it is the only thing that moves,
 * because the panel it opened is out of flow. */
.filters .picker[open] > summary {
  background: color-mix(in srgb, var(--accent) 10%, var(--bg));
  border-color: var(--accent);
}

/* The panel: a sheet hung under both handles, left-aligned with them.
 *
 * Narrower than the filter panel it hangs off. The screener's wrap is 1760px,
 * and a sheet that wide spreads nine groups of pills over a couple of lines
 * each with metres of gap between them - the eye crosses the whole window to
 * read one group. Capped, it reads as a menu rather than as a second page and
 * the pills stay packed. `right: auto` is what lets the width win.
 *
 * Capped and scrolled rather than as tall as it likes. The figures picker is
 * ninety pills over nine groups, which is most of a screen on its own; as a
 * sheet over the table it would otherwise cover the results entirely and put
 * its own Apply button below the fold. */
.picker-panel {
  position: absolute;
  left: 0;
  right: auto;
  width: min(100%, 1040px);
  top: calc(100% + 8px);
  max-height: min(60vh, 620px);
  overflow-y: auto;
  padding: 16px 18px 14px;
  border: 1px solid var(--accent);
  border-radius: var(--radius-lg);
  background: var(--bg);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.18);
}
.picker-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--bg);
  transition: transform 120ms ease;
}
/* Open, it is not an invitation to add any more - it is the way back out. */
.filters .picker[open] .picker-plus { transform: rotate(45deg); }
.picker-plus .plus-mark { width: 8px; height: 8px; }
.picker-chev { display: inline-flex; color: var(--muted); font-size: 0.7rem; line-height: 1; transition: transform 120ms ease; }
.filters .picker[open] .picker-chev { transform: rotate(180deg); }
.picker .count {
  font-weight: 500;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 7px;
}
.filters .picker[open] .count { background: var(--bg); }
/* Arriving here from the + on the table without JavaScript lands on a shut
 * picker, and a page that has silently jumped is a page that looks unchanged.
 * The ring says which control was meant; the reader clicks it. With scripting
 * on, screener.js opens it and this is never seen. */
.picker:target > summary { outline: 2px solid var(--accent); outline-offset: 2px; }
.picker-note { color: var(--muted); font-size: 0.8rem; margin: 0 0 14px; max-width: 78ch; }
.picker-actions { margin: 10px 0 2px; }
.picker-group { margin-bottom: 18px; }
.picker-group:last-of-type { margin-bottom: 6px; }
/* `position: relative` so the heading's info bubble is anchored to the heading
 * and not to the 13px icon - the same reason `fieldset.slicer` has it. */
.picker-group h3 { position: relative; font-size: 0.8rem; color: var(--text); font-weight: 700; letter-spacing: 0.01em; margin-bottom: 8px; }
/* The caption that separates the two kinds of control under one heading: a
 * short list of answers we wrote, and a pair of number boxes. See the template
 * for why that distinction is a caption rather than a heading of its own. */
.picker-kind { color: var(--muted); font-size: 0.74rem; margin: 6px 0 4px; }

/* Pills, not a grid of checkboxes.
 *
 * The old picker was a 13px box with a word beside it, ninety times. Two
 * hundred targets that size is a page you aim at rather than use: the label was
 * clickable but did not look it, nothing about a ticked row read as "chosen" at
 * a glance, and the one-pixel row gap meant the box above was a near miss. So
 * each entry is a pill the size of a button, the whole of it clickable, and a
 * chosen one is filled rather than merely ticked.
 *
 * It is still a real checkbox inside a real label - see the template. What
 * changed is the size of the target and how obvious the state is; the form,
 * the keyboard behaviour and the scripting-off path are exactly as they were. */
.picker-boxes { display: flex; flex-wrap: wrap; gap: 7px; }
.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 13px 6px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text);
  font-size: 0.84rem;
  line-height: 1.25;
  cursor: pointer;
  user-select: none;
}
.pill:hover { border-color: var(--accent); }
/* The tick. `appearance: none` rather than a hidden input with a drawn
 * substitute, so it is still the checkbox itself that is focused, tabbed to and
 * toggled with Space - and so the pill degrades to a visible, obviously
 * three-state control in a browser without `:has()` below. */
.pill input {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
  margin: 0;
  padding: 0;
  border: 1.5px solid var(--muted);
  border-radius: 4px;
  background: var(--bg);
}
.pill input:checked { background: var(--accent); border-color: var(--accent); }
.pill input:checked::after {
  content: "✓";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
}
.pill input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The whole pill picks up the state, so a chosen figure is legible from across
 * the panel rather than from the tick alone. Progressive: without `:has()` the
 * tick above still says which are chosen. */
.pill:has(input:checked) {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--bg));
}

/* --- saved screens -------------------------------------------------------- */

/* Two switches, and neither one is "JavaScript is on".
 *
 * `.copy-link` needs scripting and a clipboard; `.screens-picker` needs
 * `localStorage` to have accepted a write and given it back, which is a
 * different question - a browser in a locked-down mode has the object and
 * throws on use. screens.js adds each class only once its half is really
 * working, so the default state is the honest one: neither control on the page
 * at all, rather than a Save button that quietly loses what it is given. Same
 * shape as `html.hx-live` above, and for the same reason.
 *
 * `display: none` on the picker rather than `hidden` because it is a flex item
 * in `.pickers`, sitting beside the column picker's handle when it is there. */
.screens-picker { display: none; }
html.has-screens .screens-picker { display: block; }
.copy-link { display: none; }
html.js-live .copy-link { display: inline-flex; }

/* The figures picker is ninety pills and wants a metre of sheet; this one is a
 * short list of names and a box to type one into, and at 1040px it would be a
 * mostly empty pane over the table. */
.screens-picker .picker-panel { width: min(92vw, 420px); }

/* The glyph in the handle, where the column picker has its +. Not a + itself:
 * adding a column and keeping a screen are different gestures, and a second
 * round + beside the first is one control that looks repeated. */
.picker-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 19px;
  height: 19px;
  border-radius: 50%;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.62rem;
}
.filters .picker[open] .picker-mark { background: var(--bg); }

.screens-list { list-style: none; margin: 0 0 10px; padding: 0; }
.screens-empty { color: var(--muted); font-size: 0.82rem; padding: 4px 0 2px; }

/* A row is a link and a delete, and the link is the whole width of the row: a
 * saved screen is a thing you go to, and a twelve-character name in a 400px
 * panel with the target only around the letters is a menu that feels broken. */
.screen {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 2px 4px 6px;
  border-radius: var(--radius);
}
.screen:hover { background: var(--surface); }
.screen-load {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.88rem;
}
/* Which of them is on the table, recomputed from the address bar after every
 * swap - see screens.js. Without it, a reader who has since moved a column and
 * changed a bound cannot tell whether they are looking at their own saved
 * screen or at something they have drifted away from. */
.screen-current {
  flex: none;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0 7px;
}
.screen-drop {
  flex: none;
  padding: 0 6px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.2;
  cursor: pointer;
}
.screen-drop:hover, .screen-drop:focus-visible { color: var(--neg); }

.screens-save { display: flex; gap: 6px; align-items: center; }
.screens-save input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
}
.screens-save input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.screens-save button { flex: none; font-size: 0.85rem; padding: 5px 10px; }

/* Empty until something has happened, and taking up no room until then: a
 * panel that reserves a line for a message it is not showing has a gap in it
 * that reads as a rendering fault. */
.screens-note { color: var(--muted); font-size: 0.78rem; margin: 8px 0 0; }
.screens-note:empty { margin: 0; }

/* --- applying without an Apply button ------------------------------------- */

/* Two classes, one switch, and the switch is *htmx being live* rather than
 * scripting being on. `html.hx-live` is added by screener.js from htmx's own
 * load event; see the comment there for why the usual "JavaScript is enabled"
 * test is the wrong one. Default state is the one that works with nothing: the
 * Apply buttons visible, the live affordances hidden. */
.hx-only { display: none; }
html.hx-live .no-hx { display: none; }
html.hx-live .hx-only { display: inline-flex; }

.live-note { color: var(--muted); font-size: 0.78rem; }

/* The results are stale for the length of one request, and saying so beats
 * either a spinner (which moves the table down and then back up) or nothing at
 * all (which on a slow connection reads as a click that did not register).
 * `hx-indicator="#results"` on the form is what puts the class here.
 * `pointer-events: none` so a second click during the request cannot sort a
 * table that is about to be replaced - and, now that the filters are in the
 * table's own header, so a keystroke during the request cannot land in a box
 * that is about to be swapped out from under it. */
/* **The rows dim, not the header.** The filters are in the header now, and
 * dimming the box somebody is typing a bound into - or making it inert for the
 * length of the request that bound just started - is a control that fights the
 * reader. So the fading is on what is actually stale: the rows, the count and
 * the chips. */
#results.htmx-request tbody,
#results.htmx-request .result-count,
#results.htmx-request .chips { opacity: 0.45; }
#results.htmx-request tbody { pointer-events: none; }
#results tbody, #results .result-count, #results .chips { transition: opacity 120ms ease; }

/* --- info icon ------------------------------------------------------------ */

/* A sentence that only matters while somebody is typing into one particular box
 * - what a K/M/B/T suffix means, that a percentage box takes 15 for 15% - used to
 * be printed beside the box or above the whole band. Forty of those is a panel
 * made mostly of explanation, so each one is behind an icon now.
 *
 * **Hover and focus, and the text is really in the DOM.** `opacity` rather than
 * `display: none` keeps the sentence in the accessibility tree, so a screen
 * reader reads it where a `title` attribute would be skipped; `tabindex` on the
 * icon puts it in reach of a keyboard; `pointer-events: none` on the bubble
 * keeps it from swallowing a click on whatever is underneath. */
.info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-left: 4px;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg);
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  vertical-align: middle;
  cursor: help;
}
.info:hover, .info:focus { background: var(--accent); border-color: transparent; color: #fff; outline: none; }
.info .tip {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 40;
  width: max-content;
  max-width: 230px;
  padding: 6px 9px;
  border-radius: var(--radius);
  background: var(--text);
  color: var(--bg);
  font: 400 0.75rem/1.4 var(--sans);
  font-style: normal;
  text-align: left;
  white-space: normal;
  opacity: 0;
  pointer-events: none;
  transition: opacity 90ms ease;
}
.info:hover .tip, .info:focus .tip { opacity: 1; }

/* Applied filters, each one a link that removes itself. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: -6px 0 12px; }
.chips-label { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-size: 0.82rem;
  font-family: var(--mono);
}
.chip:hover { border-color: var(--accent); text-decoration: none; }
.chip .x { color: var(--muted); font-weight: 700; }

/* --- the 404 and its search box ------------------------------------------- */

/* The one form on the site with no class of its own: an input and a button as
 * two blocks, stacked, was the only place a control looked unstyled. */
.notfound p { max-width: 62ch; }
.notfound form { display: flex; gap: 10px; max-width: 460px; margin: 22px 0; }
.notfound form input { flex: 1; padding: 9px 13px; }

/* An info icon inside a chips label is a lowercase "i" like every other one on
 * the page, and its *tooltip* is a sentence that must not be shouted:
 * `text-transform` on the uppercased label inherits into the tip, which is a
 * child of the icon rather than of the label as it looks. */
.chips-label .info { text-transform: none; letter-spacing: normal; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- company page --------------------------------------------------------- */

/* The header is a two-column grid so the mark sits beside both the name and the
 * metadata line rather than pushing them apart. `auto 1fr` and not a flexbox
 * row: the h1 wraps on a long company name, and the mark must stay top-aligned
 * against the first line of it instead of centring against two. */
.company-header { display: grid; grid-template-columns: auto 1fr; gap: 4px 18px; align-items: start; }
.company-header h1 { margin: 0; align-self: center; }
.company-header .meta { grid-column: 2; margin: 0; }

/* One box, two contents. The dimensions are fixed and identical for the image
 * and the monogram, so a page with a logo and a page without lay out the same -
 * a mark that changes size between companies makes the h1 beside it move, which
 * is the kind of thing a reader notices without being able to say why.
 *
 * `object-fit: contain` on a white ground because the provider's icons are a
 * mix of square, wide and transparent, and cropping a wordmark to a square is
 * worse than letterboxing it. */
.company-mark {
  grid-row: span 2;
  width: 56px;
  height: 56px;
  border-radius: 10px;
  flex: none;
}
img.company-mark { object-fit: contain; background: #fff; border: 1px solid var(--border); }

/* Initials on a hue derived from the CIK - see logos.monogram_hue. Only the hue
 * varies; saturation and lightness are fixed here so every placeholder has the
 * same weight and none can come out unreadable, which is what happens when a
 * hash picks all three channels. */
.company-mark-monogram {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: hsl(var(--mark-hue) 55% 28%);
  background: hsl(var(--mark-hue) 45% 92%);
  border: 1px solid hsl(var(--mark-hue) 35% 82%);
  user-select: none;
}

/* The monogram inverts; the logo does not. A brand icon is a fixed asset, most
 * of them are dark marks drawn for a light ground, and many are transparent
 * PNGs - so `img.company-mark` keeps its white background in both themes rather
 * than becoming a dark square with an invisible logo in it. Only the generated
 * placeholder, whose colours we own, follows the theme. */
@media (prefers-color-scheme: dark) {
  .company-mark-monogram {
    color: hsl(var(--mark-hue) 55% 78%);
    background: hsl(var(--mark-hue) 30% 20%);
    border-color: hsl(var(--mark-hue) 25% 30%);
  }
}

.company-header .ticker { font-family: var(--mono); color: var(--muted); font-size: 0.7em; }
/* Row and column gaps set apart. One 14px number for both made a metadata line
 * that wraps to three - sector, SIC, a four-level NAICS trail, an exchange -
 * read as three separate lines rather than as one wrapped sentence. */
.company-header .meta { color: var(--muted); font-size: 0.9rem; display: flex; gap: 5px 16px; flex-wrap: wrap; line-height: 1.5; }
/* The NAICS trail. One flex item however many levels it has, so it wraps as a
 * unit rather than leaving "Manufacturing >" at the end of a line and the rest
 * of the hierarchy on the next. The links take the meta line's colour rather
 * than the link colour: four blue links in a row read as navigation, and this
 * is a classification that happens to be clickable. */
.company-header .naics { display: inline-flex; align-items: baseline; gap: 4px; flex-wrap: wrap; }
.company-header .naics a { color: inherit; text-decoration-color: var(--border); }
.company-header .naics a:hover { color: var(--text); text-decoration-color: currentColor; }
/* Said quietly, and said. Nobody publishes a NAICS code per SEC filer, so this
 * one is our mapping of the SIC code beside it and the page has to say so
 * somewhere - a label at the end of the trail costs a word and answers it. */
.company-header .naics .derived {
  font-size: 0.78em; text-transform: uppercase; letter-spacing: 0.04em;
  border: 1px solid var(--border); border-radius: 3px; padding: 0 4px;
  cursor: help;
}

/* The lede. Set slightly larger than body text and given a wider measure than
 * the filing prose below it.
 *
 * **100ch, not the 72ch used for Item 1.** 72 is the right measure for reading
 * thirty thousand words of legal text; this is four lines under a heading, and
 * at 72ch inside a 1180px page it reads as a narrow column with a large empty
 * right-hand side. The class is `.summary-body` and not `.lede` because
 * `.lede` is already the home page hero's paragraph - `max-width: 62ch` and
 * muted grey - and a wrapper sharing that name silently inherited both, which
 * is what made this column narrower than the 72ch its own rule asked for. */
.business-summary { margin: 26px 0 4px; }
.business-summary p { max-width: 100ch; font-size: 1.05rem; color: var(--text); }

/* Four lines closed, all of it open, and the whole summary is in the HTML in
 * both states - this is a height clamp, not a truncation, so a crawler and a
 * reader with no stylesheet get the entire text.
 *
 * The line count is a custom property because it has to agree with
 * summary.CLAMP_ABOVE_CHARS on the server: that constant decides whether the
 * expand control is rendered at all, and if the two drift apart the page grows
 * a "Read more" that reveals half a line. Both are sized to the 100ch measure
 * above.
 *
 * The line height is set on the paragraph *and* named here as a variable,
 * rather than the calc quoting a number it hopes the paragraph still has: the
 * factor used to be written 1.55 because that was `body`'s, and a change to the
 * body's line height silently made the clamp shorter than four lines and shaved
 * the top off the fourth one. */
/* **The gap between paragraphs is exactly one line.** That is what makes the
 * clamp land on a line boundary instead of through the middle of one: with a
 * 12px margin between two paragraphs, a box four line-heights tall ended 14px
 * into the fifth line, so the summary was cut across the x-height of a row of
 * letters and the fade sat over the top halves of them. Every box edge inside
 * this section is now a multiple of the line height, so any clamp at a whole
 * number of lines cuts cleanly however many paragraphs the filer wrote. */
.business-summary { --lede-lines: 4; --lede-lh: 1.55; }
.business-summary p { line-height: var(--lede-lh); margin: 0 0 calc(1em * var(--lede-lh)); }
.business-summary p:last-child { margin-bottom: 0; }
.business-summary.is-clamped .summary-body {
  position: relative;
  max-height: calc(var(--lede-lines) * 1.05rem * var(--lede-lh));
  overflow: hidden;
}
/* A fade rather than a hard cut, so it reads as "continues" instead of "ends".
 * It sits on top of the text, so it must be the page background exactly - hence
 * a token, not a hardcoded white, or it becomes a grey band in dark mode. */
.business-summary.is-clamped .summary-body::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2.6rem;
  background: linear-gradient(to bottom, transparent, var(--bg));
  pointer-events: none;
}
.business-summary input:checked ~ .summary-body { max-height: none; }
.business-summary input:checked ~ .summary-body::after { display: none; }

.business-summary .more {
  display: inline-block;
  margin-top: 12px;
  color: var(--accent);
  font-size: 0.9rem;
  cursor: pointer;
}
.business-summary .more:hover { text-decoration: underline; }
/* The checkbox is visually hidden but still the thing that is focused and
 * tabbed to, so the focus ring has to be drawn on the label instead - without
 * this the control is invisible to a keyboard user. Same arrangement as the
 * screener's filter pills. */
.business-summary input:focus-visible ~ .more { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }
/* One name at a time: display:none takes the inactive span out of the
 * accessibility tree as well as off the page. */
.business-summary .when-open { display: none; }
.business-summary input:checked ~ .more .when-closed { display: none; }
.business-summary input:checked ~ .more .when-open { display: inline; }

/* The quote panel and the valuation cards below it are the same kind of thing -
 * a figure with a label over it - so they are bordered the same way. A filled
 * box with no border above a row of bordered boxes with no fill read as two
 * unrelated designs stacked. */
.quote {
  display: flex;
  gap: 14px 36px;
  flex-wrap: wrap;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin: 26px 0 8px;
}
.quote .stat { display: flex; flex-direction: column; gap: 3px; }
.stat .label, .kpi-grid .label { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat .value { font-family: var(--mono); font-size: 1.3rem; font-weight: 600; line-height: 1.25; }
.asof { font-size: 0.78rem; color: var(--muted); }

.kpi-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
/* One row of six. Fixed columns rather than auto-fill because the point is the
 * single row - auto-fill on a wide screen would fit seven and leave a gap. It
 * steps to 3 then 2 rather than reflowing one card at a time. */
.kpi-grid-6 { grid-template-columns: repeat(6, 1fr); }
@media (max-width: 860px) { .kpi-grid-6 { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 520px) { .kpi-grid-6 { grid-template-columns: repeat(2, 1fr); } }
.kpi-grid > div { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; }
.kpi-grid .value { font-family: var(--mono); font-size: 1.1rem; font-weight: 600; line-height: 1.25; }
.kpi-grid .delta { font-size: 0.8rem; font-family: var(--mono); }

.statements table.data th[scope="row"] { white-space: normal; min-width: 200px; font-weight: 500; padding-right: 20px; }

/* The row label stays put while the years scroll under it - the same bargain as
 * the screener's ticker column, and the same reason: eleven columns do not fit
 * in a 1180px page much below a desktop window, and a row of figures with
 * "Long-term debt" off the left edge is a row of numbers belonging to nobody.
 *
 * The background is explicit on every state, because a sticky cell is painted
 * over the cells sliding beneath it and a transparent one shows both at once.
 * The heavy divider is a `box-shadow` here rather than the next cell's
 * `border-left`: the border belongs to a cell that scrolls away, so the line
 * would travel off with it and leave the labels sitting directly on the
 * figures. The header's corner cell is sticky in both directions at once, which
 * is why it needs the higher z-index. */
.statements table.data thead th:first-child,
.statements table.data tbody th[scope="row"] {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg);
  box-shadow: 2px 0 0 var(--border);
}
.statements table.data thead th:first-child { z-index: 3; background: var(--surface); }
.statements table.data tbody tr:hover th[scope="row"] { background: var(--surface); }
/* Without this the divider is drawn twice: once by the sticky cell above and
 * once by the first figure column's own left border. */
.statements table.data thead th:first-child + th,
.statements table.data tbody th[scope="row"] + td { border-left: 0; }
/* Three stacked statement tables. The gap above each heading is what separates
   them - without it the balance sheet's header row reads as a continuation of
   the income statement's last line, which is the whole thing the split was for.
   `:first-of-type` keeps the first heading tight under the h2. */
.statements h3 { margin: 30px 0 10px; font-size: 0.95rem; letter-spacing: 0.01em; }
.statements h3:first-of-type { margin-top: 14px; }
.note { color: var(--muted); font-size: 0.85rem; max-width: 92ch; }
.statements .note { margin: 16px 0 0; }

details.section { border: 1px solid var(--border); border-radius: var(--radius-lg); margin: 12px 0; }
details.section summary { padding: 13px 16px; cursor: pointer; display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
details.section[open] summary { border-bottom: 1px solid var(--border); }
details.section .prose { padding: 10px 18px 18px; }

/* Long filing prose. A measure of ~72 characters is where a wall of legal text
 * stops being readable, so the column is capped regardless of window width.
 * The list items are capped with the paragraphs, not left to the article's own
 * width: an `<li>` that runs 6ch wider than the paragraph above it is a right
 * edge that steps in and out down the page. */
.prose p, .prose li { max-width: 72ch; }
.prose.narrow { max-width: 72ch; }
.prose p { margin: 0 0 1.1em; }
.prose ul, .prose ol { padding-left: 22px; }
.prose li { margin-bottom: 0.65em; }
.prose li:last-child { margin-bottom: 0; }

/* --- front page ----------------------------------------------------------- */

/* `main.wrap` already owns the space under the header, so the hero only owns
 * the space below itself. */
.hero { padding: 0 0 8px; }
.hero h1 { font-size: 2rem; max-width: 22ch; margin-bottom: 1rem; }
.lede { color: var(--muted); max-width: 62ch; font-size: 1.05rem; margin: 0; }
.hero-search { display: flex; gap: 10px; margin: 24px 0 0; position: relative; max-width: 560px; }
.hero-search input { flex: 1; padding: 11px 14px; }
.hero-links { margin: 16px 0 0; }

/* Three tables across, and their rows line up.
 *
 * Two of the three carry a note under the heading and the first does not, so
 * each `.mini` began its table at whatever height its own text ended at - three
 * tables of the same ten companies starting on three different lines, which is
 * the kind of near-miss that reads as a page that has come apart. `subgrid` puts
 * heading, note and table on rows shared by all three columns, so each band is
 * as tall as the tallest of them and the tables start together whatever a note
 * wraps to. It needs the note to be *there* on all three - see the empty `<p>`
 * in index.html - and where subgrid is unsupported the columns simply fall back
 * to their own heights. */
/* `row-gap: 0`, and it is not a stray zero: a subgrid uses its *parent's*
 * gutter in the subgridded axis and cannot set its own, so a row gap on the
 * grid is a gap between each section's heading, its note and its table - which
 * is 30px of nothing under three headings. The separation between the three
 * bands, when the grid wraps to fewer columns than tables, is the section's own
 * bottom padding instead. */
.mini-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 0 32px; margin: 26px 0 8px; }
.mini-grid > .mini { display: grid; grid-row: span 3; grid-template-rows: subgrid; align-content: start; padding-bottom: 30px; }
.mini h2 { margin: 0 0 10px; }
.mini .note { margin: 0 0 14px; }
table.data.compact th, table.data.compact td { padding: 7px 10px; font-size: 0.88rem; }

/* Three tables across a 1140px page leaves each about 365px, and a table of
 * nowrap cells with a 220px name column has a min-content width far past that.
 * A table cannot be squeezed below its min-content width, so it did not shrink
 * into its track - it spilled out of it and over the table beside it.
 *
 * table-layout: fixed makes the widths declarative instead: the four narrow
 * columns are sized to their worst case ("-$999.99B", "-99.9%") and the name
 * takes what is left and ellipsises. Scoped to .mini because admin uses
 * .compact too and those tables want to size themselves. */
.mini-grid > .mini { min-width: 0; }
.mini table.data.compact { table-layout: fixed; }
/* Every pixel these four do not need is a pixel of company name, and the name
 * is the column that ellipsises - so they are sized to the widest figure each
 * one can hold and no wider.
 *
 * **The worst case is not the figure on the page today.** `fmt_mult` and
 * `fmt_pct` are unbounded: a P/E of 1,234.5x and a growth rate of 1,234.5% are
 * both eight characters, and in a `table-layout: fixed` cell a figure wider
 * than its column does not wrap or clip - it bleeds left over the right-hand
 * end of the column beside it, so an outlier overlaps its neighbour's last
 * digit. Sized for eight characters of Plex Mono at this size plus the padding.
 * The name column takes what is left, which is the column that can afford to. */
.mini table.data.compact th, .mini table.data.compact td { padding: 7px 8px; }
.mini table.data.compact col.c-ticker { width: 3.6rem; }
.mini table.data.compact col.c-cap { width: 5.5rem; }
.mini table.data.compact col.c-metric { width: 5rem; }
.mini table.data.compact td.name { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini table.data.compact td.name a { display: block; overflow: hidden; text-overflow: ellipsis; }

/* --- admin ---------------------------------------------------------------- */

.status-failed { background: color-mix(in srgb, var(--neg) 12%, transparent); }
.status-partial { background: color-mix(in srgb, #c08a20 14%, transparent); }
.small { font-size: 0.8rem; color: var(--muted); white-space: normal; max-width: 420px; }

/* --- footer --------------------------------------------------------------- */

.site-footer { border-top: 1px solid var(--border); margin-top: 56px; padding: 24px 0 48px; color: var(--muted); font-size: 0.87rem; }
.site-footer p { margin: 0; }

@media (max-width: 700px) {
  main.wrap { padding-top: 24px; }
  .nav-search { margin-left: 0; width: 100%; }
  .nav-search input { width: 100%; min-width: 0; }
  .hero h1 { font-size: 1.7rem; }
  .quote { gap: 14px 24px; padding: 16px; }
  .company-header { grid-template-columns: auto 1fr; gap: 10px 14px; }
  .table-scroll { border-radius: var(--radius); }
}

/* The screener on a phone. It works - every control, and the table scrolls
 * sideways inside its own box - and it is still a page that wants a wide
 * screen, so it says so once at the top rather than pretending otherwise.
 *
 * 860px rather than 700: the cut-off is not "phone", it is "narrower than a few
 * columns of figures", which a small laptop window also is. */
@media (max-width: 860px) {
  .wide-hint {
    display: block;
    margin: 0 0 16px;
    padding: 10px 14px;
    border-left: 3px solid var(--accent);
    background: var(--surface);
    border-radius: 0 var(--radius) var(--radius) 0;
    color: var(--muted);
    font-size: 0.85rem;
  }
  .filter-top { gap: 10px; }
  .filter-top .filter-actions { margin-left: 0; }
  .filters fieldset.slicer { flex: 1 1 100%; }
  .filters fieldset.slicer select { width: 100%; max-width: none; }
  /* The filters stay in the headings on a phone, and they get bigger rather
   * than smaller: a phone keyboard covers half the screen and a 50px box under
   * it is a hard target. The table already scrolls sideways, so the width this
   * costs is width the reader was going to drag past anyway.
   *
   * The tools stay visible instead of waiting for a hover there is no pointer
   * to give: on a touch screen `:hover` is either never or sticky, and a × that
   * appears only after a tap that also sorted the column is worse than one that
   * is simply there. */
  /* Set on the *default* density, not on the boxes, so a reader who has
   * explicitly asked for Narrow on a phone still gets Narrow: `.d-narrow` and
   * `.d-wide` carry one more class than this and win the variable. Nobody
   * choosing anything is the case this is for. */
  table.data.dense {
    --box-w: 68px;
    --text-w: 148px;
    --search-w: 148px;
    --filter-font: 0.82rem;
    --filter-pad: 4px 6px;
  }
  .col-tools .tool { opacity: 1; }
  /* An info bubble anchored to a control at the left edge of a 360px screen
   * would otherwise render half off it. */
  .info .tip { max-width: 62vw; }
}
