/* pwa-starter: styles.css @ d2fad01  (design system kept; app layout is this repo's own)
   Everything is a CSS custom property so a color mode is a variable swap, not a second stylesheet.
   Light = the defaults; dark arrives two ways, keyed off <html data-theme> which theme.js sets
   (and the pre-paint script in index.html stamps before first paint):
     - @media (prefers-color-scheme: dark) :root:not([data-theme])  — 'auto': follow the OS
     - :root[data-theme="dark"]                                     — forced (toggle / screenshots)
   A forced 'light' needs no block: it carries data-theme, so the :not([data-theme]) media rule
   skips it and the light defaults win even when the OS is dark. Keep the two dark blocks in sync.

   The --c-* tokens below are read INTO JS by chart.js (via Theme.getCssColor) to paint SVG fills.
   Those are the ones a mode flip can't reach on its own — hence the rerender() contract in app.js. */

:root{
  --bg:#f6f5f2; --card:#ffffff; --ink:#191919; --muted:#5f6560; --line:#e2ded6; --accent:#1f6f9f;
  --sel:#c2410c;
  --plot:#fbfaf8; --grid:#e8e4dc; --axis:#6b7169; --dot-line:#ffffff;
  /* Lifespan reads HOT to COLD along YlGnBu: died young is yellow, lived long is deep blue.
     SEQUENTIAL, not diverging -- lifespan is a magnitude, and a diverging ramp needs a baseline
     to diverge from, which this never had (it used to pivot on the median of whoever was in the
     dataset, so the pivot moved when the data did). It also has no neutral middle to disappear
     against the plot.

     Stepped DARKER than canonical YlGnBu on purpose: #edf8b1 sits at 1.08:1 against --plot, so
     the pale end of the published ramp is invisible on this surface. These steps clear 3:1
     throughout and stay monotone in lightness (L* 59 / 50 / 28), which is what makes a sequential
     ramp readable. Re-run the dataviz palette validator against --plot, not --card, if you
     change them. */
  --c-short:#b8860b; --c-mid:#1b8574; --c-long:#243a8f;   /* sequential: hot / mid / cold */
  --c-living:#8b9188;                                      /* open circles; stroke only */
  --radius:12px; --maxw:1180px;
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme]){
    --bg:#14161a; --card:#1c1f25; --ink:#eceef0; --muted:#9aa3a8; --line:#333a42; --accent:#7ec2f0;
    --sel:#fb923c;
    --plot:#191c21; --grid:#2b313a; --axis:#98a1a8; --dot-line:#14161a;
    --c-short:#dcec8a; --c-mid:#4fbfa8; --c-long:#3b93da;
    --c-living:#8e979c;
  }
}
:root[data-theme="dark"]{
  --bg:#14161a; --card:#1c1f25; --ink:#eceef0; --muted:#9aa3a8; --line:#333a42; --accent:#7ec2f0;
  --sel:#fb923c;
  --plot:#191c21; --grid:#2b313a; --axis:#98a1a8; --dot-line:#14161a;
  --c-short:#dcec8a; --c-mid:#4fbfa8; --c-long:#3b93da;
  --c-living:#8e979c;
}

*{ box-sizing:border-box; margin:0; padding:0 }
html{ -webkit-text-size-adjust:100% }   /* stop iOS inflating text on rotate */

body{
  background:var(--bg); color:var(--ink);
  font-family:system-ui, -apple-system, "Segoe UI", sans-serif; line-height:1.45;
  overscroll-behavior-y:none;             /* kill the rubber-band bounce in standalone */
  padding:calc(18px + env(safe-area-inset-top)) calc(16px + env(safe-area-inset-right))
          calc(28px + env(safe-area-inset-bottom)) calc(16px + env(safe-area-inset-left));
}
main{ max-width:var(--maxw); margin:0 auto }

a{ color:var(--accent) }
a:not(:hover){ text-decoration:none }
:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; border-radius:4px }

.head{ display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:6px }
h1{ font-size:clamp(21px, 4.6vw, 32px); letter-spacing:-.015em; font-weight:650 }
.lede{ color:var(--muted); font-size:14px; max-width:62ch; margin-bottom:14px }
.lede a{ color:var(--accent) }

.card{
  background:var(--card); border:1px solid var(--line); border-radius:var(--radius); padding:14px;
}

/* --- layout: chart beside detail on wide screens, stacked on a phone ------------------------ */
.grid{ display:grid; gap:14px; grid-template-columns:1fr; margin-bottom:14px }
@media (min-width:900px){
  .grid{ grid-template-columns:minmax(0,1fr) 300px; align-items:start }
  /* Sticky so the pinned composer stays on screen while you read the table below it — the point
     of a PERSISTENT detail view rather than a tooltip. */
  #detail{ position:sticky; top:14px }
}

/* --- the filter row ---------------------------------------------------------------------------
   One row, above the chart and the table, because both filters scope both views. It is a sibling
   of .grid, not a child of either card — a filter drawn inside one card claims to scope that card.
   app.js moves it INTO the chart card in full screen (where the chart is everything there is) and
   hides the search half there, so the chart keeps the height it had. */
.filters{ display:flex; flex-direction:column; gap:8px; margin-bottom:14px; padding:12px 14px }
.filters .tablehead{ margin-bottom:0 }
.filters .filterbar{ margin:0 }
body.fs #filters{ margin:0 0 8px; padding:0; border:0; background:transparent }
body.fs #filters .tablehead{ display:none }

/* --- controls ------------------------------------------------------------------------------- */
/* margin-TOP: the controls sit under the plot now (see index.html). */
.controls{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:12px 0 0 }
.seg{ display:flex; border:1px solid var(--line); border-radius:999px; overflow:hidden }
.seg button{
  font:500 13px/1 system-ui, sans-serif; color:var(--muted); background:transparent;
  border:0; padding:0 13px; min-height:36px; cursor:pointer; white-space:nowrap;
}
.seg button + button{ border-left:1px solid var(--line) }
.seg button[aria-pressed="true"]{ background:var(--accent); color:#fff }
.btn{
  font:500 13px/1 system-ui, sans-serif; color:var(--muted); background:transparent;
  border:1px solid var(--line); border-radius:999px; padding:0 13px; min-height:36px; cursor:pointer;
}
.btn:disabled{ opacity:.4; cursor:default }
.spacer{ margin-left:auto }

/* The "you're on an old version — tap to update" tag (app.js). */
.ver{ font:500 11px/1 ui-monospace, monospace; color:var(--muted) }
.ver.behind{ color:var(--accent); text-decoration:underline; cursor:pointer }

/* --- readership filter (histogram + brush) ------------------------------------------------- */
.filterbar{ display:flex; align-items:center; gap:12px; margin:2px 0 10px }
.filterbar label, #gender-label{ font-size:12px; color:var(--muted); white-space:nowrap }
/* The gender pills are the SAME control as the view switcher, one notch smaller: this row's own
   Clear button is 28px, and a 36px segmented control beside it made the filter look like the
   primary action on the page. Touch sizing is restored in the phone block below — .seg.sm outranks
   the .seg button rule there on specificity, so it has to opt back in rather than inherit. */
.seg.sm button{ min-height:30px; padding:0 11px; font-size:12px }
/* flex:none is load-bearing, not tidiness. `.seg` is overflow:hidden, so when the Clear button
   appears and the row runs out of room the pill group SHRANK and clipped its last pill — the
   filter lost the word "Men" exactly when you were using the filter beside it. The histogram is
   the one thing here that may give up width (it has min-width:0); everything else is content that
   has to stay whole. margin-left:auto pins the group right, so the brush absorbs the change and
   the pills do not move at all. */
#gender{ margin-left:auto; flex:0 0 auto }
/* A little more air than the row's own 12px gap. With the readout gone, the readership Clear
   button ends up directly beside the word "Gender", where it reads as if it clears that. */
#gender-label{ margin-left:6px }
#hist{ flex:1 1 auto; min-width:0; margin-right:2px; touch-action:none }   /* touch-action:none so d3-brush owns the drag */
#hist svg{ display:block; width:100%; height:auto; overflow:visible }
#hist-clear{ min-height:28px; padding:0 10px; font-size:12px }
/* d3-brush chrome: keep the unselected area readable and give the handles a visible grip. */
#hist .selection{ fill:var(--accent); fill-opacity:.10; stroke:var(--accent); stroke-opacity:.55 }
#hist .handle{ fill:var(--accent); fill-opacity:.65 }
#hist .overlay{ cursor:crosshair }

/* --- chart ---------------------------------------------------------------------------------- */
#viz{ position:relative }                      /* the compact detail panel anchors to this in full screen */
#plot{ position:relative; touch-action:none }   /* touch-action:none so d3-zoom owns the gesture */
/* overflow:hidden, not the SVG default of visible: chart.js clips the dots to the plot rectangle,
   and this is the backstop for anything else a zoom pushes past the frame. Nothing in the chart is
   drawn outside its own viewBox, so there is nothing legitimate to lose here. */
#plot svg{ display:block; width:100%; height:auto; overflow:hidden }
.hint{ font-size:12px; color:var(--muted); margin-top:8px }

/* Name flag that tracks the cursor on a real pointer. Never shown on touch — there the tap
   goes straight to the persistent detail panel, so there is no hover bubble to double-fire. */
#flag{
  position:absolute; z-index:5; pointer-events:none; opacity:0; transform:translate(-50%,-140%);
  background:var(--card); border:1px solid var(--line); border-radius:6px;
  padding:3px 7px; font:600 12px/1.3 system-ui, sans-serif; white-space:nowrap;
  box-shadow:0 2px 8px rgba(0,0,0,.12);
}
#flag.on{ opacity:1 }
#flag small{ display:block; font-weight:400; color:var(--muted) }

/* --- legend --------------------------------------------------------------------------------- */
/* column-gap 22px: the size key's outermost circle sits close to its cell edge, so the old 18px
   let it crowd the "Also" column on a phone. */
.legend{ display:flex; gap:14px 22px; flex-wrap:wrap; align-items:flex-end; margin-top:12px; font-size:12px }
.legend .lab{ color:var(--muted); display:block; margin-bottom:4px }
.ramp{ width:170px; height:10px; border-radius:2px; border:1px solid var(--line) }
.ticks{ display:flex; justify-content:space-between; color:var(--muted); font-size:11px; margin-top:2px }
.swatches{ display:flex; gap:12px; align-items:center }
.sw{ display:flex; gap:5px; align-items:center; color:var(--muted) }
.sw i{ width:12px; height:12px; border-radius:50%; display:inline-block }
.sizes{ display:flex; gap:6px; align-items:flex-end; color:var(--muted) }

/* --- detail panel --------------------------------------------------------------------------- */
#detail h2{ font-size:17px; letter-spacing:-.01em; margin-bottom:2px; line-height:1.25 }
#detail .dates{ color:var(--muted); font-size:13px; margin-bottom:10px }
#detail dl{ display:grid; grid-template-columns:auto 1fr; gap:4px 12px; font-size:13px; margin-bottom:10px }
#detail dt{ color:var(--muted) }
#detail dd{ font-variant-numeric:tabular-nums }
#detail .empty{ color:var(--muted); font-size:13px }
#detail .rank{ color:var(--muted); font-size:12px }
.detail-nav{ display:flex; gap:6px; margin-top:8px; flex-wrap:wrap }

/* The readership sparkline. Colours live HERE and not in app.js on purpose: every other baked
   colour in this app is inside an SVG that JS generates from a d3 scale, so it has to be re-read
   through Theme.getCssColor on a theme flip (see the note in app.js's Theme.subscribe). This one
   is plain inline SVG in the document, so a CSS variable reaches it and a theme change costs
   nothing — one fewer component to remember to re-bake.
   The svg is stretched by the width with preserveAspectRatio="none"; the line survives that
   because app.js marks it vector-effect="non-scaling-stroke". */
/* pan-y, not none: a horizontal drag reads months, a vertical one still scrolls the page. On a
   phone the panel sits inside the chart card and a sparkline that ate vertical swipes would trap
   the scroll. cursor:crosshair says it is readable; the outline is the global :focus-visible. */
.spark{ display:block; width:100%; height:34px; overflow:visible; touch-action:pan-y;
  cursor:crosshair }
.spark-line{ fill:none; stroke:var(--accent); stroke-width:1.25; stroke-linejoin:round }
.spark-area{ fill:var(--accent); opacity:.13; stroke:none }
.spark-peak{ stroke:var(--muted); stroke-width:1; opacity:.45; vector-effect:non-scaling-stroke }
/* The month under the pointer (or the arrow keys). Drawn in the accent so it reads as "you are
   here" against the peak marker's muted "this is the month the caption names". */
.spark-cursor{ stroke:var(--accent); stroke-width:1; opacity:0; vector-effect:non-scaling-stroke }
.spark-cursor.on{ opacity:.7 }
.spark-ax{ display:flex; justify-content:space-between; color:var(--muted); font-size:11px;
  margin-top:1px; font-variant-numeric:tabular-nums }
/* tabular-nums so stepping through months with the arrow keys does not make the line jitter. */
.spark-cap{ color:var(--muted); font-size:11.5px; margin-top:3px; margin-bottom:8px;
  font-variant-numeric:tabular-nums }

/* The compact form: app.js moves this same element INTO the chart card on a phone and in full
   screen (see placeDetail), where it must read as part of the card, not as a card inside a card.
   Hidden until something is pinned, so an unselected chart keeps the full width for the chart. */
#detail.compact{
  border:0; border-top:1px solid var(--line); border-radius:0; background:transparent;
  padding:10px 0 0; margin-top:10px;
}
#detail.compact h2{ font-size:15px }
#detail.compact .dates{ margin-bottom:6px }
#detail.compact dl{ margin-bottom:6px }
/* On a touch screen it is hidden until something is pinned: there is no hover to churn the layout
   and the space belongs to the chart. Where a real pointer exists the panel PREVIEWS on hover, so
   its box is reserved and always drawn — otherwise the legend under it would jump every time the
   cursor crossed a dot. Same split chart.js uses for TOUCH. */
@media (hover:none), (pointer:coarse){
  #detail.compact:not(.on){ display:none }
}
@media (hover:hover) and (pointer:fine){
  /* Tall enough for the tallest state — a pinned composer, which adds the Prev/Next/Clear row —
     so neither a hover nor a click moves the legend below it. The name is ellipsized here for the
     same reason: a two-line name would blow the reservation. A touch screen gets neither rule,
     because it has no hover and would just be losing chart space. */
  /* The sparkline block (34px of line + two label rows) is inside the reservation, because it is
     drawn on a hover PREVIEW like everything else in the panel. The number is MEASURED, not
     guessed: ui.test.mjs prints the pinned panel's real height beside the "pinning does not shove
     it either" check, and fails when this falls short of it. */
  #detail.compact{ min-height:268px }
  #detail.compact h2{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis }
}

/* Full screen: a FIXED-height strip between the readership filter and the chart, drawn whether or
   not anything is pinned. It used to float over the bottom of the chart, which buried the x-axis
   and the legend; giving it its own slice costs 76px of chart once, instead of covering the part
   you were reading every time you tapped. The height is fixed because #plot is flex:1 here — a box
   that grew on select (or on HOVER) would re-lay out the chart under the pointer. */
body.fs #detail.compact{
  /* display, explicitly: it beats the touch rule above that hides an empty compact panel. In full
     screen the empty box is the point — it is what keeps the chart the same height either way. */
  display:block; flex:0 0 auto; position:relative; min-height:0; height:76px; overflow:hidden;
  margin:0 0 8px; padding:8px 82px 8px 10px;             /* right padding clears the Clear button */
  border:1px solid var(--line); border-radius:8px; background:var(--plot);
}
body.fs #detail.compact h2{
  font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
body.fs #detail.compact .dates{ margin:0; font-size:12px }
body.fs #detail.compact .empty{ font-size:12px }
body.fs #detail.compact .detail-nav{ position:absolute; top:6px; right:6px; margin:0 }
/* The strip carries none of these: they are back in the panel the moment you leave full screen. */
body.fs #detail.compact dl, body.fs #detail.compact .rank, body.fs #detail.compact > a,
body.fs #detail.compact .spark, body.fs #detail.compact .spark-ax,
body.fs #detail.compact .spark-cap{ display:none }

/* --- table ---------------------------------------------------------------------------------- */
/* Visible to a screen reader, not on screen. display:none would remove it from the a11y tree too. */
.sr-only{
  position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
}

.tablehead{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px }
#q{
  flex:1 1 180px; min-width:0; min-height:38px; padding:0 12px; font:400 15px/1 system-ui, sans-serif;
  color:var(--ink); background:var(--bg); border:1px solid var(--line); border-radius:999px;
}
#count{ font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums }
/* 880 rows behind a 46vh window with nothing saying so: macOS and iOS both draw overlay
   scrollbars that vanish when idle, so the box looked like a short, complete list. Two cues —
   a scrollbar that stays put where the platform honors these properties, and a fade at the bottom
   edge painted ONLY while there is more below. The fade is two stacked backgrounds: the "cover"
   is attachment:local so it travels with the rows and slides over the shadow exactly as the last
   row arrives; the shadow is attachment:scroll so it is pinned to the bottom of the box. */
.scroll{
  position:relative; max-height:46vh; overflow:auto; border:1px solid var(--line);
  border-radius:8px; -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
  scrollbar-color:color-mix(in srgb, var(--muted) 45%, transparent) transparent;
  background:
    linear-gradient(to top, var(--card) 55%, transparent) bottom / 100% 22px no-repeat local,
    linear-gradient(to top, color-mix(in srgb, var(--ink) 20%, transparent), transparent)
      bottom / 100% 12px no-repeat scroll;
}
.scroll::-webkit-scrollbar{ width:10px; height:10px }
.scroll::-webkit-scrollbar-track{ background:transparent }
.scroll::-webkit-scrollbar-thumb{
  background:color-mix(in srgb, var(--muted) 45%, transparent); border-radius:999px;
  border:2px solid transparent; background-clip:padding-box;
}
table{ border-collapse:collapse; width:100%; font-size:13.5px }
thead th{
  position:sticky; top:0; z-index:1; background:var(--card); text-align:left; white-space:nowrap;
  font-weight:600; color:var(--muted); border-bottom:1px solid var(--line); padding:0;
}
thead th button{
  width:100%; text-align:inherit; font:inherit; color:inherit; background:transparent; border:0;
  padding:10px 10px; min-height:40px; cursor:pointer;
}
th.num, td.num{ text-align:right; font-variant-numeric:tabular-nums }
th.num button{ text-align:right }
thead th[aria-sort] button{ color:var(--ink) }
thead th[aria-sort="ascending"] button::after{ content:" \2191" }
thead th[aria-sort="descending"] button::after{ content:" \2193" }
tbody td{ padding:9px 10px; border-bottom:1px solid var(--line) }
tbody tr{ cursor:pointer }
tbody tr:hover td{ background:color-mix(in srgb, var(--accent) 8%, transparent) }
tbody tr[aria-selected="true"] td{ background:color-mix(in srgb, var(--sel) 16%, transparent) }
td .chip{ display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:7px; vertical-align:baseline }
.empty-row td{ color:var(--muted); text-align:center; padding:24px 10px }

footer{ margin-top:22px; font-size:12px; color:var(--muted) }
footer p + p{ margin-top:4px }

/* --- fullscreen ------------------------------------------------------------------------------
   CSS-driven, not the Fullscreen API: iPhone Safari has no requestFullscreen at all, which is
   precisely the device that needs this most. app.js additionally calls the API where it exists
   (desktop, Android) to drop the browser chrome too — but the layout below is what does the work,
   so the feature can't be missing on the platform it was built for. */
body.fs{ overflow:hidden }
body.fs #viz{
  position:fixed; inset:0; z-index:40; border-radius:0; border:0; margin:0;
  padding:calc(8px + env(safe-area-inset-top)) calc(10px + env(safe-area-inset-right))
          calc(8px + env(safe-area-inset-bottom)) calc(10px + env(safe-area-inset-left));
  display:flex; flex-direction:column; background:var(--card);
}
body.fs #plot{ flex:1 1 auto; min-height:0; order:2 }
body.fs #filters{ order:0 }
body.fs #detail.compact{ order:1 }
body.fs .controls{ order:3; margin-top:8px }
body.fs .legend{ order:4 }
body.fs #plot svg{ height:100% }
body.fs .hint{ display:none }
body.fs .filterbar{ margin-bottom:6px }
body.fs main > *:not(.grid), body.fs .grid > *:not(#viz){ display:none }
body.fs .grid{ display:block }

/* A phone on its side is 390px TALL, and full screen was spending two thirds of that on chrome:
   the controls, the filter, the detail strip, and ~80px of legend. Below 560px of height (and wide
   enough that this is a landscape phone rather than a short narrow window) the legend goes and the
   strip drops to one line. The chart is the whole reason for full screen. */
@media (max-height:560px) and (min-width:560px){
  body.fs .legend{ display:none }
  body.fs #detail.compact{ height:50px; padding:6px 82px 6px 10px }
  body.fs #filters{ margin-bottom:6px }
  body.fs .controls{ margin-top:6px }
}

/* Phone-specific sizing. Targets REAL touch devices, not just a narrow desktop window. */
@media (hover:none) and (pointer:coarse) and (max-width:800px){
  .scroll{ max-height:56vh }
  .seg button, .seg.sm button, .btn{ min-height:40px }   /* keep tap targets at the 44px-ish floor */
  #q{ min-height:42px }
}

/* Six columns don't fit a phone: the name wraps to three lines and Quartets scrolls off. Drop the
   two that are one tap away in the detail panel. Width-only (not hover/pointer) so a narrow
   desktop window gets the same fit — and so it still applies when printing to a narrow page. */
@media (max-width:640px){
  .filterbar{ flex-wrap:wrap; gap:6px 10px }
  .filterbar #hist{ flex:1 0 100%; order:3 }
  /* Three filters do not fit two lines here, and the brush is the one that needs the full width.
     So the gender pills take a third line of their own, WITH their label — sharing line one with
     the readership readout put "Women | Men" next to a views range, where the two read as one
     control. margin-left is dropped with it: the row is theirs now, so there is no slack to push
     against and `auto` would have shoved the label away from its own buttons. */
  #gender-label{ order:4 }
  #gender{ order:5; margin-left:0 }
  .wide-only{ display:none }
  tbody td{ padding:9px 8px }
  thead th button{ padding:10px 8px }
}

@media (prefers-reduced-motion: reduce){
  *{ transition:none !important; animation:none !important }
}

/* Print: ink-friendly, no chrome, chart + table only. */
@media print{
  body{ background:#fff; color:#000; padding:0 }
  :root{ --plot:#fff; --grid:#ddd; --axis:#444; --card:#fff; --line:#999; --dot-line:#fff }
  .card{ border:0; padding:0 }
  .controls, .tablehead, #flag, .detail-nav, #theme, .ver, .hint, .filterbar, .filters{ display:none }
  .grid{ display:block }
  .scroll{ max-height:none; overflow:visible; border:0; background:none }
  thead th{ position:static }
  tbody tr{ break-inside:avoid }
}
