/* IBM Plex, served from our own origin: this page makes no third-party request and
   should keep it that way. The copy is English, so basic Latin first; headlines in the Latest
   panel bring each shown country's script, and Vietnamese is Latin with letters the basic
   subset lacks (ơ, ư, ệ), so without its subset a word would mix Plex with the system face
   letter by letter. Its range is day.666566b630.css's, which has the same faces. Thai still falls back to
   the system face as a whole line — see fonts/SOURCES.md. */
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 100 700;          /* a single variable file covers the whole range */
  font-display: swap;
  src: url('fonts/ibm-plex-sans-var.056e4e2459.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-400.c36f509c0a.woff2') format('woff2');
}

@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 100 700;
  font-display: swap;
  src: url('fonts/ibm-plex-sans-vietnamese-var.526a4dd36c.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('fonts/ibm-plex-mono-vietnamese-400.0a8b854cc1.woff2') format('woff2');
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

:root {
  color-scheme: dark;
  /* The gap between a panel's own edge and the rows inside it, on all four sides. */
  --panel-inset: 12px;
  --ink: #e8f0fb;
  --dim: #7d8ea6;
  --line: #2a3a55;
  --accent: #5aa9ff;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --pad: max(20px, env(safe-area-inset-left));
}

* { box-sizing: border-box; }

/* The UA rule for [hidden] is a plain `display: none`, which every id selector in this
   file outranks — so #pin kept showing a stale coordinate, and #modes and #controls both
   stayed on screen while the page still believed it had hidden them. One rule, rather
   than remembering to add an exception each time something gains a `display`. */
[hidden] { display: none !important; }

body {
  margin: 0;
  /* A definite height, not just a minimum. With only min-height the grid container is
     content-sized, so the 1fr row asks the canvas how tall it wants to be, the canvas
     answers with its own drawing-buffer size, the row grows, the next resize makes the
     buffer bigger still — and the globe ends up drawn larger than the window. */
  height: 100svh;
  display: grid;
  /* Three rows, but the globe is given all of them and the chrome is laid over the
     top and bottom ones. The globe used to stop at a hard horizontal line wherever
     the footer began, which read as a widget dropped into a page rather than as the
     thing the page is — and the taller the footer grew, the more Earth it cut off. */
  grid-template-rows: auto 1fr auto;
  grid-template-columns: 100%;
  background: #04060c;
  color: var(--ink);
  font: 400 15px/1.6 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
  overflow: hidden;
}

/* The starfield sits behind everything and never takes pointer events. */
#sky {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}

/* `body >`, not a bare element selector. The news card builds its own <header> for the
   place name and its close button, and `pointer-events` inherits — a bare `header` rule
   matched that one too and left the close button unclickable, which is a bug this file
   has already had once by another route. */
body > header {
  grid-row: 1;
  grid-column: 1;
  z-index: 2;
  padding: max(22px, env(safe-area-inset-top)) var(--pad) 0;
  text-align: center;
  /* The title is not a target; a drag that starts on it should still turn the globe. */
  pointer-events: none;
}

h1 {
  --track: .34em;               /* one source for the tracking, see .orb */
  margin: 0;
  font-size: clamp(19px, 2.6vw, 26px);
  font-weight: 300;
  letter-spacing: var(--track);
  text-indent: var(--track);    /* letter-spacing pads the right edge; recentre */
  text-transform: uppercase;
}

h1 b {
  font-weight: 600;
  color: var(--accent);
}

/* The O of WORLD, drawn rather than typed: the ◉ character this borrows from is
   missing or differently shaped in plenty of system fonts, which is not a risk worth
   taking with a wordmark. Sized in em so it tracks the heading at every breakpoint. */
.orb {
  /* Sized and placed on the font's cap height so the ring reads as the O it replaces.
     inline-block puts its bottom edge on the baseline, which centres it at half the
     cap height — plain `inline` does not, and left the ring sitting 0.26em low.
     --cap is the cap height of the heading's typeface; swapping the font means
     measuring it again. */
  --cap: .698em;                /* cap height of IBM Plex Sans, measured not guessed */
  display: inline-block;
  width: var(--cap);
  height: var(--cap);
  vertical-align: baseline;
  /* Chrome does not add letter-spacing after a replaced element, so the ring would
     sit hard against the R while the W keeps its full tracking. Carrying the same
     tracking as a right margin makes the ring space like the character it stands in
     for — which is what a text glyph gets for free. */
  margin-left: 0;
  margin-right: var(--track);
  color: var(--accent);
}

/* The globe fills the window; the header, readout and footer are drawn over it. */
#stage {
  grid-row: 1 / -1;
  grid-column: 1;
  position: relative;
  min-height: 0;
  touch-action: none;           /* we handle the drag ourselves */
  cursor: grab;
}

#stage.holding { cursor: grabbing; }

/* All of this is drawn over photography now, and the photography includes cloud that
   is very nearly white. A shadow rather than a plate: a full-width bar of background
   behind a credit line would be heavier than the line is worth. */
#readout,
footer .hint,
footer .credit,
h1 { text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 14px rgba(0, 0, 0, 0.7); }

/* Taken out of flow as well, so the canvas can never contribute its intrinsic size
   back to the row that sizes it. Either of these two changes prevents the feedback
   loop; both are here because the failure is invisible until a window is resized
   twice and the globe is suddenly off the bottom of the screen. */
#stage canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Sat behind the globe, which was fine for the half-second of "preparing the
   globe" and not fine once the same element started carrying "no news reached the
   globe" for as long as the API is unreachable: the sentence ran straight through
   the Earth and the middle of it was unreadable. It gets its own plate now. */
#stage-note {
  grid-row: 2;
  grid-column: 1;
  z-index: 4;
  align-self: end;
  justify-self: center;
  max-width: calc(100% - 2rem);
  margin: 0 0 1rem;
  padding: 0.4rem 0.9rem;
  border-radius: 0.6rem;
  background: rgba(4, 6, 12, 0.82);
  text-align: center;
  text-wrap: balance;
  color: var(--dim);
  font: 400 12px/1.6 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
}

#readout {
  position: fixed;
  z-index: 2;
  top: max(22px, env(safe-area-inset-top));
  left: var(--pad);
  margin: 0;
  display: grid;
  gap: 14px;
  font-family: var(--mono);
  /* Reading matter over the globe, not a target — a drag that begins on it should
     still turn the Earth. */
  pointer-events: none;
}

#readout div { display: grid; gap: 3px; }

#readout dt {
  color: var(--dim);
  font-size: 9.5px;
  letter-spacing: .22em;
  text-transform: uppercase;
}

#readout dd {
  margin: 0;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  letter-spacing: .04em;
}

#pin dd { color: var(--accent); }

body > footer {
  grid-row: 3;
  grid-column: 1;
  z-index: 2;
  padding: 0 var(--pad) max(20px, env(safe-area-inset-bottom));
  text-align: center;
  /* Nothing here is a background any more — it is a strip of glass over the globe.
     Without this the footer's whole rectangle would swallow drags that begin low on
     the Earth, which is most of the bottom of the screen once the view is zoomed. */
  pointer-events: none;
}

/* The two text lines get their pointer events back so a pinch on them is a page zoom
   again rather than a globe zoom: they are siblings of the stage, so the stage's
   `touch-action: none` never applied to them — only the fact that they were transparent
   to the pointer did. The cost is that a drag *starting* on those two thin lines no
   longer turns the globe, which is a worse place to start a drag than it is to lose the
   only way left to enlarge small text. */
footer .hint,
footer .credit { pointer-events: auto; touch-action: auto; }

/* The buttons are targets again — the buttons, not the rows that centre them.
   `#controls` is a centred flex row with no width of its own, so its box spans the
   whole screen: giving the row back its pointer events handed it a full-width strip
   of the Earth that looked draggable and was not. The text lines stay inert too;
   they sit over imagery and there is nothing to do with them. */
#modes button,
#controls button { pointer-events: auto; }

footer p { margin: 0; }

#controls {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 12px;
}

#controls button {
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* Opaque: a translucent control over a white cloud is a light control with dark
     text on it, which is not what any of this was designed against. */
  background: #0a1220;
  color: var(--ink);
  font: 400 11px/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color .2s ease, background .2s ease;
}

#controls button:hover { border-color: var(--accent); background: #13233d; }
#controls button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.hint {
  font: 400 11px/1.8 var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--dim);
  transition: opacity .6s ease;
}

/* Faded out, and out of the way. The hint keeps its box when it goes — which is what
   stops the layout jumping — so once it was given its pointer events back for the sake
   of page zoom, an invisible line was left sitting across the bottom of the globe
   swallowing drags that started on it. */
.hint.gone { opacity: 0; pointer-events: none; }

/* The keyboard route is worth announcing, but only where there is a keyboard to use it
   with: on a phone this line already wraps to two, and naming keys nobody has would push
   it to three. `hover: hover` is the closest thing a stylesheet has to that question. */
.hint .keys { display: none; }
@media (hover: hover) {
  .hint .keys { display: inline; }
}

.credit {
  margin-top: 6px !important;
  font-size: 11px;
  color: #8796ab;
}
/* A link in the credit line reads as the line does, with only the underline to say so. */
.credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.credit a:hover, .credit a:focus-visible { color: var(--accent); outline: none; }
/* One country's links never split across a wrap; the gap between countries is wider than
   the " · " inside one. On a narrow phone each country takes a line of its own, and 24px
   lines keep the links stacked there far enough apart to tap (WCAG 2.5.8). */
#credit-countries { line-height: 24px; }
.credit .country { white-space: nowrap; margin: 0 0.6em; }

/* Which build is on screen. Quiet enough to ignore, present enough to quote back
   when something looks wrong. */
#version {
  font: 400 10px/1.6 var(--mono);
  letter-spacing: .1em;
  color: #6d7d95;
}

/* Under ~700px the readout used to move into the page flow, because floating it would
   have put it on top of the globe. Everything is on top of the globe now — that is the
   layout — so it stays out of flow and only changes shape: one centred row under the
   title instead of a column down the left, where the title is. Leaving it in flow after
   the stage grew to fill the window cost the globe a 145px band at the bottom. */
@media (max-width: 700px) {
  #readout {
    /* Under the title rather than beside it: at this width the title is centred across
       the whole row, so the left column the desktop layout uses is already taken. */
    top: calc(max(22px, env(safe-area-inset-top)) + 42px);
    left: 0;
    right: 0;
    grid-auto-flow: column;
    justify-content: center;
    gap: 26px;
    padding: 0 var(--pad);
    text-align: center;
  }
  #readout div { justify-items: center; }
  /* The marked point is three values long while the others are one, so it takes the
     slack instead of a line of its own — a full-width row for it reordered the whole
     readout, because `grid-column: 1 / -1` under `grid-auto-flow: column` places it
     first. */
  #readout { grid-auto-columns: minmax(0, auto); }
  #pin { min-width: 0; }
  h1 { --track: .26em; }
}

@media (prefers-reduced-motion: reduce) {
  .hint { transition: none; }
}

/* The news layer: a mode switch beside the globe controls, and the card a dot opens. */
/* One control with two states, not two buttons. ROTATE and RESET VIEW are verbs —
   press one and something happens — so they stay separate pills. This is a switch:
   exactly one of the two is true at any moment, and a segmented pill is the shape
   that says so. Sharing the type, the radius and the border keeps them family;
   the filled half is what keeps them distinguishable. */
#modes {
  display: flex;
  width: fit-content;
  margin: 0 auto 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  /* Opaque like the controls below it: the selected half needs a surface to be read
     against, and a translucent one changes with whatever is behind the page. */
  background: #0a1220;
  overflow: hidden;
}

#modes button {
  /* Thumbed, not clicked — same as the controls below it. */
  min-height: 44px;
  padding: 0 20px;
  border: 0;
  background: transparent;
  /* Both halves are available, so both read as available. Selection is carried by the
     surface and the underline, not by dimming the one you did not pick. */
  color: var(--ink);
  font: 400 11px/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .2s ease, color .2s ease;
}

#modes button + button { box-shadow: -1px 0 0 #3d5171; }

#modes button:hover:not([aria-pressed='true']) { background: #13203a; }

/* The page's own accent, not the dots'. Amber matched the dot gradient and that was
   the wrong thing to match: the dots are data drawn on the Earth, while this is chrome,
   and every other piece of chrome — the title, the marked point, the focus ring — is
   this blue. Two signals, not one: a lit surface and a marker along the bottom edge,
   so the state survives being read in greyscale. */
#modes button[aria-pressed='true'] {
  background: #17315c;
  color: #d7e9ff;
  box-shadow: inset 0 -2px 0 var(--accent);
}


/* Inset, because the pill clips anything drawn outside it. */
#modes button:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }

/* Both panels have the same two forms and swap between them on the same signal: over
   the window when there is no room beside the globe, a column down the side when there
   is. They used to disagree — the card asked a width media query and the feed asked
   fit(), so a portrait tablet got a full-window feed beside a small boxed card. Now
   they are the same width, so it is the same question, and one class answers it. */
#news-card {
  grid-row: 1 / -1;
  grid-column: 1;
  z-index: 5;
  pointer-events: auto;
  /* Declared rather than inherited: the card is a sibling of the stage now, but the
     stage's touch-action:none used to reach it and cost the phone 91% of the densest
     card. Stated here so moving it again cannot quietly take its scrolling away.
     overscroll-behavior keeps a flick that reaches an end from becoming a globe drag. */
  touch-action: pan-y;
  overscroll-behavior: contain;
  overflow-y: auto;
  /* A column so the fade below can claim the space a short list leaves. */
  display: flex;
  flex-direction: column;
  padding: 0 var(--panel-inset);
  /* Both bands are opaque and both are inside the scrollport, so Chrome counts a row
     behind either of them as already visible and focuses it without scrolling — a Tab
     through the list left the focused headline sitting under the panel's own name.
     scroll-padding on the scroller rather than scroll-margin on the rows: the rows are
     three different elements across two panels, the bands are one pair, and the button
     that used to carry its own margin had it set to 4.5rem against a 72.4px header,
     which is how a rule that looks right lands half a pixel short. */
  scroll-padding-top: calc(max(var(--panel-inset), env(safe-area-inset-top)) + 4rem);
  scroll-padding-bottom: calc(max(var(--panel-inset), env(safe-area-inset-bottom)) + 0.5rem);
  background: rgba(4, 7, 14, 0.62);
  backdrop-filter: blur(2px);
}

/* Sticky, because the card scrolls and the close button is most of how it is
   dismissed: scrolled out of the clip box, a tap where the × appears to be lands on
   the globe behind it and opens a different province. */
#news-card header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.6rem;
  padding: max(var(--panel-inset), env(safe-area-inset-top)) 0 0.4rem;
  /* Opaque, not 0.96: the rows scrolling under it are 44px bordered buttons, and at
     four percent they read as ghost text above the heading. */
  background: #080c16;
}

#news-card h3 {
  margin: 0;
  /* Shrinks instead of pushing the close button off the card when a back button
     shares the header with a long place name — and then truncates honestly, because
     a shrunk box does not clip its own text: between 736px and ~790px the place name
     painted straight through the close button with nothing to scroll to. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 1.05rem;
  letter-spacing: 0.01em;
}

/* The chooser's heading is a count, not a place name — chrome, like the feed's, and it
   gets the same treatment. The class is what keeps the tracking off the other use of
   this element: a province name is content, and Thai does not take tracking. */
#news-card h3.count {
  font-size: 0.95rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

#news-card .close,
#news-feed .close {
  /* 44px: the card is opened by a fingertip and closed by one too. */
  min-width: 44px;
  min-height: 44px;
  padding: 0 0.45rem;
  font-size: 1.2rem;
  line-height: 1.4;
}

/* Focus moves here when a card opens so a screen reader announces the place. The
   only way to open one is a pointer tap, so there is no keyboard user to show a
   ring to — and Chrome treats this programmatic focus as :focus-visible, which put
   a blue box around the place name on every tap. */
#news-card h3:focus { outline: none; }

/* The close button carried no styling of its own and rendered as the platform's grey
   one — the only control on the page in a different language, and now sitting in the
   same header as a styled back button. */
#news-card .choices button,
#news-card .back,
#news-card .close,
#news-feed .close,
#news-feed .more {
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  /* Opaque, like the globe controls: the card is translucent over whatever is under
     it, and a translucent control on top of that has no contrast of its own. */
  background: #0a1220;
  color: var(--ink);
  cursor: pointer;
  transition: border-color .2s ease, background .2s ease;
}

/* The chooser's rows aim the globe, so their accent carries the claim the story rows'
   does and answers to the same class — the rule beside those rows says what it is for. The
   card's own buttons keep the browser's answer: a close button that looks hovered while
   the cursor sits still promises nothing about where the globe is. */
:root.pointing #news-card .choices button:hover,
#news-card .back:hover,
#news-card .close:hover,
#news-feed .close:hover,
#news-feed .more:hover { border-color: var(--accent); background: #13233d; }

#news-card .choices button:focus-visible,
#news-card .back:focus-visible,
#news-card .close:focus-visible,
#news-feed .close:focus-visible,
#news-feed .more:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* The two ways out of the panel sit together: on to the day page, or closed. The link is
   drawn as the button beside it, because it is one of a pair. */
#news-feed .ways { display: flex; gap: 0.4rem; align-items: center; }
/* The country the list holds, beside the way on to that country's day page: one button
   naming it, opening every country shown (countrymenu.js — the news page's header draws
   the same). Absent while the owner shows one country. */
#news-feed .country-menu { position: relative; }
#news-feed .country-toggle {
  display: inline-flex; align-items: center; gap: .5em; min-height: 44px; padding: 0 12px;
  border: 1px solid var(--line); border-radius: 999px; background: #0a1220; color: var(--ink);
  font: 400 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; cursor: pointer;
}
#news-feed .country-toggle:hover, #news-feed .country-toggle[aria-expanded="true"] { border-color: var(--accent); background: #13233d; }
#news-feed .country-toggle:focus-visible, #news-feed .country-list a:focus-visible, #news-feed .country-list button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#news-feed .country-toggle .caret { color: var(--dim); }
#news-feed .country-list {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 20; min-width: 12rem; max-height: 60vh; overflow-y: auto;
  margin: 0; padding: 4px; list-style: none;
  border: 1px solid var(--line); border-radius: 10px; background: #0a1220; box-shadow: 0 10px 30px rgba(0, 0, 0, .55);
}
#news-feed .country-list[hidden] { display: none; }
#news-feed .country-list a, #news-feed .country-list button {
  display: flex; align-items: center; gap: .6em; width: 100%; min-height: 44px; padding: 0 10px;
  border: 0; border-radius: 6px; background: transparent; color: var(--ink); text-align: left; text-decoration: none;
  font: 400 .9rem var(--sans, inherit); cursor: pointer;
}
#news-feed .country-list a:hover, #news-feed .country-list button:hover { background: #13233d; }
#news-feed .country-list [aria-current="true"], #news-feed .country-list [aria-pressed="true"] { color: var(--accent); }
#news-feed .country-list [aria-current="true"]::after, #news-feed .country-list [aria-pressed="true"]::after { content: '✓'; margin-left: auto; }
#news-feed .flag { display: block; width: 22px; height: auto; border-radius: 2px; flex: none; }
#news-feed .more {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; padding: 0 0.45rem; font-size: 1.2rem; line-height: 1.4;
  color: var(--ink); text-decoration: none;
}

/* The one row in that list that is not a control of the panel: a place name is the same
   kind of thing as a headline, so it answers the cursor the same way — nothing until it
   is pointed at. The close and back buttons keep the opaque fill because they act on the
   panel rather than list what is in it. */
#news-card .choices button {
  display: block;
  width: 100%;
  padding: 0 0.7rem;
  background: transparent;
  font: inherit;
  font-size: 0.95rem;
  text-align: start;
}

/* A control, so it is lettered like the globe's controls rather than like the place
   names it sits beside — those share this button's box but not its type, because they
   are data. */
#news-card .back {
  flex: none;
  padding: 0 0.7rem;
  font: 400 11px/1 var(--mono);
  letter-spacing: .18em;
  text-transform: uppercase;
}

/* Both of the card's "there is more than this" lines: the report list is a sample of at
   most five, and the card itself holds at most six stories of however many the place has. */
#news-card .more {
  margin: 0.3rem 0 0;
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.4);
}

#news-card article + article {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}

/* The place-level one, which sits after the last article and is about the card rather
   than about the story above it. Without this it lands 0.3rem under an "and N more
   outlets" in the same grey at the same size, and the two read as one list: measured on
   live data, three of the eleven cards that show this line today end in exactly that
   pair. Separated the way one article is separated from the next, because that is the
   boundary it is on the far side of. */
#news-card .more.rest {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}

#news-card h4 {
  margin: 0 0 0.2rem;
  font-size: 0.95rem;
  font-weight: 600;
}

/* The story's own line, above its reports. Scoped to the paragraph because the rows
   underneath carry a `.meta` too — and it takes the same grey as those, because it is the
   same kind of writing: when this line was accented, the words that label a time appeared
   in two colours on one screen for no reason a reader could name. The accent is left to the
   place in a feed row, which is the one piece of data among labels. */
#news-card p.meta {
  margin: 0 0 0.45rem;
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.45);
}

#news-card ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One list, one row, one gap — the chooser's and the story's, which are the same kind of
   thing: a stack of targets under a heading. Every row is 44px for the same reason the
   close button is, because the list exists where a fingertip could not separate the dots.
   The rows were a flex line back when a report was three columns; now each holds one
   link, and the gap is the feed's. */
#news-card li {
  display: block;
  font-size: 0.85rem;
}

#news-card li + li { margin-top: 0.3rem; }

/* Same row, same answer to the cursor, as the chooser and the feed — and the whole row,
   time included, because the time is part of what you are pointing at. It is inside the
   link rather than beside it for that reason; a box that stopped short of it read as
   having missed something. */
#news-card .outlet {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Both column rules take the border and the corners, which are what make a floating panel
   a card, and keep the fill and the blur the overlay form already declares. They used to
   carry a denser pair of their own, which read as a solid slab beside a narrow screen's
   glass — a difference that was only making one panel into two objects. */
body.panel-beside #news-card {
  grid-row: 2 / -1;
  justify-self: end;
  align-self: stretch;
  width: 20rem;
  margin: 1.5rem 1.5rem 1.5rem 0;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 0.9rem;
}

/* An outlet column beside the headline needs room in the *card*, and past the
   breakpoint above the card is a fraction of the viewport rather than all of it: at
   736px it is narrower than the phone card while running the wide layout, which left
   the headline a 30px column and wrapped one of them over sixteen lines. A fixed
   144px outlet plus a nowrap timestamp cannot shrink, so the headline absorbs every
   pixel they keep. Asking the card how wide it is, rather than the window, is the
   condition this layout actually has. */

/* The top band carries what is shown; the footer carries the globe. Absolute rather
   than a flex row so the wordmark keeps the centre it has, and the band itself stays
   pointer-transparent with only this opting back in — the rule the footer already uses. */
body > header { position: relative; }

/* The newspaper link and the Latest button, side by side at the band's right. */
.top-actions {
  position: absolute;
  /* `top` measures from the header's padding edge, so it has to repeat the header's own
     inset rather than inherit it — otherwise a notched phone moves the wordmark down and
     leaves these buttons behind, under the status bar. */
  top: max(22px, env(safe-area-inset-top));
  right: var(--pad);
  display: flex;
  gap: 8px;
}

#feed-toggle, #paper-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: #0a1220;
  color: var(--ink);
  font: 400 11px/1 var(--mono);
  /* The same treatment as the globe's own buttons, down to the tracking: it is one of
     them, just in the other band. The narrower .12em here was for a Thai label, which
     does not take tracking the way capitals do. */
  letter-spacing: .18em;
  text-transform: uppercase;
  pointer-events: auto;
  cursor: pointer;
  transition: border-color .2s ease, background .2s ease;
}

:is(#feed-toggle, #paper-link)[hidden] { display: none; }
#paper-link { text-decoration: none; }
:is(#feed-toggle, #paper-link) .glyph { display: none; width: 16px; height: 16px; }

/* Below this the band cannot hold a centred wordmark and a worded button at once. The
   number is measured, not chosen: the wordmark's ink is 10.5px wide per px of font size
   and its font bottoms out at 19px, so it is 200px across from 731px down; centred, its
   right edge passes the button's left edge (viewport − 20 − 85) at about 426px. */
@media (max-width: 430px) {
  #feed-toggle { padding: 0 13px; }
  #feed-toggle .glyph { display: block; }
  #feed-toggle .label { display: none; }
}

/* Both buttons worded at the right meet the wordmark by the same arithmetic below about 630px
   (196px of buttons): from 640px down the newspaper takes the left corner, the band's mirror
   of Latest, as an icon — worded there, it would meet the wordmark below about 455px. */
@media (max-width: 640px) {
  .top-actions { left: var(--pad); justify-content: space-between; }
  /* Latest keeps the right corner when a country without topics hides the newspaper. */
  #feed-toggle { margin-left: auto; }
  #paper-link { padding: 0 13px; }
  #paper-link .glyph { display: block; }
  #paper-link .label { display: none; }
}

/* The icon is narrower but the wordmark is not: at 320px a 42px button still meets it.
   Same arithmetic, solved for the font instead — 16px puts the ink 12px clear. */
@media (max-width: 344px) {
  h1 { font-size: 16px; }
}

:is(#feed-toggle, #paper-link):hover { border-color: var(--accent); background: #13233d; }
:is(#feed-toggle, #paper-link):focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
#feed-toggle[aria-pressed='true'] { border-color: var(--accent); background: #13233d; }

/* The feed, in the form a narrow window has room for: over the globe, because there is
   no margin to put it in. Covering the globe is also what stops a dot being tapped, so
   the two panels cannot both be up — and it spans every row deliberately: at full zoom
   the Earth reaches behind the footer, which is pointer-transparent, so anything less
   would let a tap fall through and open a card underneath the feed.

   Translucent on purpose. An open feed does not hold the spin (unlike an open card), so
   the globe must still visibly turn behind it or that decision has no observable form. */
#news-feed {
  grid-row: 1 / -1;
  grid-column: 1;
  z-index: 6;
  background: rgba(4, 7, 14, 0.62);
  backdrop-filter: blur(2px);
  /* A column so the fade below can claim the space a short list leaves. */
  display: flex;
  flex-direction: column;
  padding: 0 var(--panel-inset);
  /* Both bands are opaque and both are inside the scrollport, so Chrome counts a row
     behind either of them as already visible and focuses it without scrolling — a Tab
     through the list left the focused headline sitting under the panel's own name.
     scroll-padding on the scroller rather than scroll-margin on the rows: the rows are
     three different elements across two panels, the bands are one pair, and the button
     that used to carry its own margin had it set to 4.5rem against a 72.4px header,
     which is how a rule that looks right lands half a pixel short. */
  scroll-padding-top: calc(max(var(--panel-inset), env(safe-area-inset-top)) + 4rem);
  scroll-padding-bottom: calc(max(var(--panel-inset), env(safe-area-inset-bottom)) + 0.5rem);
  /* Declared, never inherited — the same reason the card states its own: moving this
     panel later must not be able to take its scrolling away quietly. */
  pointer-events: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  overflow-y: auto;
}

/* A column down the side, not a box floating in the corner — the shape this had before
   was the card's, declaration for declaration, and the card is a dialog that opens over
   a place while this is a list that keeps running.

   The class is written by fit(), not by a media query, because what decides it is how
   much of the drawn sphere the panel would cover, and the sphere's width is a fraction
   of the window's HEIGHT. The width below is the one fit() measures against, so the two
   must move together.

   It starts below the header, which keeps the button that opened it reachable, and runs
   to the bottom of the window like the overlay form does, so the two read as the same
   panel in two widths rather than two different things. */
body.panel-beside #news-feed {
  grid-row: 2 / -1;
  justify-self: end;
  align-self: stretch;
  width: 20rem;
  margin: 1.5rem 1.5rem 1.5rem 0;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 0.9rem;
  z-index: 5;
}

/* Two dialogs over one window, not one on top of the other: the card takes the margin
   on the other side while the feed is up, and returns to its own corner when it closes.
   Both classes are needed — `panel-beside` is whether there is room, `feed-open` is
   whether anything is in it, and keying on the first alone left the card on the wrong
   side of a window with no feed in it. */
body.panel-beside.feed-open #news-card { justify-self: start; margin: 1.5rem 0 1.5rem 1.5rem; }

/* Nothing in the footer moves or is lifted when the column crosses it. A panel that
   covers what is under it is what this page does everywhere else — the overlay form
   covers the whole window, and a dot behind either form is not tappable — and the
   alternatives both read worse: shifting the two text lines alone left them 250px out of
   line with the buttons directly above them, and shifting the buttons too would take the
   globe's own controls off the globe, which has not moved.

   What it costs, measured: at 1366 with a mouse the hint's tail (", space") is behind the
   column, and on a window short and narrow enough to be offered the column at all
   (880x330) the right 11px of RESET VIEW is too — the button still works, it is only its
   edge. Both are the same trade the owner already took for the dots. */

#news-feed header {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.4rem;
  padding: max(var(--panel-inset), env(safe-area-inset-top)) 0 0.6rem;
  background: #080c16;
}

/* The bottom edge answers the top one. The inset there is not empty space, it is the
   header's own box, opaque, with the list running under it — so a row never meets the
   window edge, it meets the panel. Below, a row did meet the edge and was cut across the
   middle of a line, which read as the panel having no bottom rather than as more to come.
   Sticky, not padding, for exactly that reason: padding leaves a gap only once the list
   has been scrolled to its end, and the edge in question is the one you are looking at
   the whole time you scroll. It is the last child of the scroll container, so at the end
   of the list it comes to rest as the inset it also is. */
#news-feed::after,
#news-card::after {
  content: '';
  position: sticky;
  bottom: 0;
  display: block;
  /* Every other decoration on this page opts out of the pointer and this one did not,
     which made the bottom of the scrollport belong to the fade rather than to the row
     visible through it: measured, the last 12px of the panel answered `#news-feed` and
     not the row, so a click there opened nothing and a cursor resting there aimed
     nothing. On a phone with a home indicator the strip is the safe-area inset, nearer
     34px. The row is deliberately still legible under the fade, so it has to stay
     reachable too. */
  pointer-events: none;
  /* It is the one flex item with no content of its own, so it is the one the default
     flex-shrink empties out the moment the list overflows — which is exactly when it is
     needed. */
  flex: none;
  height: max(var(--panel-inset), env(safe-area-inset-bottom));
  /* A fade, where the header is a flat fill. The header is opaque because it carries the
     panel's name and reads as a title bar; this one carries nothing, so once the list is
     scrolled to its end and there is no row left underneath, a flat fill is just a dark
     slab lying on the glass. Fading instead means a row dissolves on the way out and the
     end of the list shows only a soft edge.
     `margin-top: auto` for the panel that is not full: sticky holds a box against the
     bottom of the scrollport only while there is content below to hold it there, so a
     card with two stories in it left the fade sitting under the last one, banded across
     the middle of the glass. The auto margin eats the leftover height instead. */
  margin-top: auto;
  background: linear-gradient(to top, #080c16, rgba(8, 12, 22, 0));
}

/* Capitals, unlike the card's heading, because this one is not the same kind of thing:
   the card's is a place name, which is content, and this is a fixed label for a panel —
   the same word as the button that opens it. */
#news-feed h3 {
  margin: 0;
  font-size: 0.95rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

#news-feed h3:focus { outline: none; }

#news-feed .note {
  margin: 0.6rem 0 0;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.5);
}

#news-feed .stories {
  margin: 0;
  padding: 0;
  list-style: none;
}

#news-feed .stories li + li { margin-top: 0.3rem; }

/* The same row the chooser draws — a bordered box that takes the accent on hover —
   rather than a line of text that gets underlined. It is the page's one answer to "this
   is the thing under your cursor", and a list of headlines is no less a list of targets
   than a list of place names. The border is there before the hover, transparent, so
   nothing shifts when it takes a colour.

   The box sits in the panel's own column, not pulled outward from it: once a row carries
   a border of its own the border *is* the edge the eye lines up, and a row hanging past
   the heading it belongs to also hung past the heading's opaque background, which let the
   sides of it show through the sticky band. The headline indents inside the box the way
   the chooser's place names do. */
#news-card li a,
#news-feed .stories li {
  display: block;
  /* 44px for the same reason every other target here is: this list is read and tapped
     on a phone at arm's length. It is a floor, not a height — most rows run to two lines
     and the padding decides how tall those are. */
  min-height: 44px;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  color: inherit;
  text-decoration: none;
  transition: border-color .2s ease, background .2s ease;
}

/* Two guards, and they answer different questions.

   The media query asks whether this pointer hovers at all. It is here rather than on the
   card's own buttons — not because those are buttons, the chooser's buttons aim the globe
   too — but because of what survives the tap that leaves the hover stuck. Activating one of
   these rows opens a story in a new tab and the row is still there when the reader comes
   back, wearing the accent and claiming the globe is at its province while the ring and the
   readout name somewhere else. A chooser button's own click replaces the card's markup and
   takes the element, and the stuck state, with it.

   The gap that leaves, stated rather than implied: `(hover: hover)` asks about the
   *primary* pointer, so a touch laptop reports true and keeps the rule. Separating that
   case needs the script, not a media query.

   The class asks whether the reader has moved since the rows did — for the geometry changes the
   script answers for, which decision 24 lists. Chrome re-resolves
   `:hover` onto whatever slides under a motionless cursor — a rebuild replaces every row,
   a scroll moves all of them, and the chooser drops a row whose count has run out — and
   none of those is anyone pointing at anything. Without it the accent names one row while
   the globe holds the one the reader actually chose; the script that aims the globe on
   `pointermove` puts the class on with the same gesture, and takes it off when markup is replaced,
   a chooser row is dropped, a panel is scrolled, or the globe lets go of a captured pointer.

   It is not the script that paragraph asks for, though it is written in one. Nothing adds the
   class for a finger, and there is no finger-specific removal, so a tap on a machine that has both
   can leave the accent behind. The listener that clears the class when the globe gives up a
   captured pointer tests no pointer type, though, so on browsers that capture a touch pointer
   implicitly some real taps may already clear it. That was not measured — an emulator asked for
   touch stops reporting `(hover: hover)`, which is the machine this is about — and what stays open
   is a tap that ends without a lost capture. */
@media (hover: hover) {
  :root.pointing #news-card li a:hover,
  :root.pointing #news-feed .stories li:hover { border-color: var(--accent); background: #13233d; }
}

/* The announcement, played once per story rather than once per render: `render` replaces
   every row whenever anything in the list changes, and three rebuilds in four carry no
   arrival at all, so an animation left to restart with the element would fire on
   departures and placement passes. `arriving` is added by the script the first time the row
   can be shown to someone who is there **and** the row is inside the panel's rectangle —
   coming back after a night settles two hundred rows and flashes the dozen on screen — and
   it is never re-added.

   The flash is now the whole mark. It used to leave a bar behind for the ten minutes the
   story counted as young, and that bar was asked for and taken away again: the arrival is
   the thing worth saying, and a reader who watched it happen does not need the row to keep
   saying it afterwards. What is left of the bar lives under the reduced-motion rule below,
   because that reader never sees the flash. */
/* Two layers at both ends, the same two, so the shadow interpolates cleanly rather than
   snapping — `none` and a two-layer list have nothing to tween between. The row is left with
   no edge of its own at rest: the inset bar that used to sit here for the whole 1.65s made
   the announcement read as a bar that happened to pulse, when what it is is a pulse. The
   bar has one job left and it is not this one; it lives under reduced motion, where there
   is no pulse to read instead. */
@keyframes arriving {
  from { box-shadow: 0 0 0 1px transparent, 0 0 12px -2px transparent; }
  50% { box-shadow: 0 0 0 1px var(--accent), 0 0 14px -2px var(--accent); }
  to { box-shadow: 0 0 0 1px transparent, 0 0 12px -2px transparent; }
}

/* Three breaths, not three blinks: the ring rises and falls over a second and a bit,
   eased both ways, so the repeat reads as breathing rather than as an alarm. */
#news-feed .stories li.arriving { animation: arriving 1.2s ease-in-out 3; }

/* After the rule it overrides, not before: the two selectors have the same specificity,
   so whichever comes last in the file wins and an override written up beside the other
   reduced-motion rule loses silently. This page stops its own Earth for a reader who asks
   for less motion, so a blinking row would be the one thing left moving on it.

   The bar is what they get instead, and the script only adds it here — it names the batch
   just announced and comes off when the next one is, which is the only ending it can have:
   there is no animation to raise `animationend`, and the age test that used to sweep it is
   gone. So it says "these arrived while you were away" and holds until something else does,
   usually the better part of a sweep. The same accent the hover uses — the page has one
   colour that means something is being singled out. Inset, because the row's radius clips
   anything drawn outside it.

   What this is not: an edge the hover leaves alone. The hover rule above paints
   border-colour on all four sides, and scanning the rendered pixels down the left edge says
   what the declarations do not — a marked row on its own reads [dim, dim, accent x4,
   ground], a hovered row reads [accent x2, ground] over a lifted ground, and a row that is
   both reads [accent x6, ground] over that same lifted ground. The two states are legible
   while they are apart; a marked row under the cursor is a thicker left edge and nothing
   more. That is worth knowing because both edges are now the reader's own doing: the hover
   rule above is held until they move, so a row wearing both was pointed at and did arrive.
   A second colour is still not worth it here: this reader has asked the page to stop
   moving, and a mark competing for attention is what they asked it to stop doing. */
@media (prefers-reduced-motion: reduce) {
  #news-feed .stories li.arriving { animation: none; }
  #news-feed .stories li.fresh { box-shadow: inset 2px 0 0 var(--accent); }
}

/* The link is the headline now rather than the whole row, so it carries the row's ink and
   no underline of its own: the box around it is what says the row is a target.

   The scroll margins are the rest of the row, and they are here because a browser scrolls a
   focused element's own box into view — which since the split is the headline, not the row it
   sits in. Measured while tabbing down the list: rows arrived with up to 31px of their own
   height cut off below the panel's edge. 56px is the tallest measured drop from the anchor's
   bottom to the row's (a meta line that wraps to two); 7px is the row's padding above. Being
   generous here only scrolls a little further than needed. */
#news-feed .stories a {
  color: inherit;
  text-decoration: none;
  scroll-margin-top: 7px;
  scroll-margin-bottom: 56px;
}

/* And it is still the whole row that opens the story. Splitting the province out of the anchor
   shrank the link to the headline, which left the outlet, the time and the row's own padding
   opening nothing at all — measured, `elementFromPoint` on the outlet came back with no anchor
   above it. The anchor covers the row again by laying a box over it rather than by wrapping it,
   which is what keeps the province a sibling control instead of a nested one. The province is
   raised above that box; everything else in the row is text and wants nothing.

   What this costs, stated rather than discovered later: the covering box takes the row's text
   with it, so a headline in this list can no longer be selected with the mouse. The rows are a
   list of links to follow, the full headline is one tap away at the source, and the alternative
   is script standing in for what a link does by itself. */
/* `isolation` so the row is the whole of its own stacking world. The province is raised over the
   covering layer, which is a question about two boxes inside one row — but without this the raise is
   answered against the panel, where the sticky heading also sits at `z-index: 1` and loses on DOM
   order. Scrolled, a place name painted straight over the word LATEST. */
#news-feed .stories li { position: relative; isolation: isolate; }
#news-feed .stories a::after { content: ''; position: absolute; inset: 0; }
#news-feed .where { position: relative; z-index: 1; }

/* One ring, and the selectors name whatever is the row in each panel: the card's rows are still
   their link, the feed's are the `<li>` since the province was split out of the link. */
#news-card li a:focus-visible,
#news-feed .stories li:has(a:focus-visible),
#news-feed .where:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The link keeps the focus but not the ring: its element is the headline now, so an outline on it
   rings two lines of text inside a row that can be clicked all over, which reads as the row being
   skipped rather than focused. Drawn on the row above instead — and on the row itself rather than
   on the covering layer, because that layer is `inset: 0`, which is the *padding* box: a ring there
   sits a border-width inside the edge the eye lines up, with its corners struck from a different
   centre, and the gap around it came out visibly uneven. */
#news-feed .stories a:focus-visible { outline: none; }

#news-feed .headline {
  display: block;
  font-size: 0.9rem;
  line-height: 1.4;
}

/* One shape for a row's second line, wherever the row is. The card used to give each
   report three columns — outlet, headline, time — which is a table, and a card that is
   two taps deep into one story does not need one; it needs to read like the list the
   reader came from. */
#news-card li .meta,
#news-feed .meta {
  /* Named because the separators below are drawn from inside whatever they follow, and one of
     them now sits inside a control that is a different colour on purpose. */
  --meta-ink: rgba(255, 255, 255, 0.45);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.2rem;
  font-size: 0.74rem;
  color: var(--meta-ink);
}

/* The same interpunct the card's own line uses, between whatever this row happens to
   carry — an outlet, sometimes a place, sometimes a time. Drawn from CSS rather than
   written into the markup because most rows have no time at all: Google gives a
   publication date only to the article it leads a cluster with, so 79% of the reports
   we hold have none, and a separator written between two spans would be left hanging
   beside nothing. A generated one cannot outlive the thing it separates. */
#news-card li .meta > * + *::before,
#news-feed .meta > * + *::before {
  content: '·';
  margin-right: 0.5rem;
}

/* The place is the one piece of data in the row, so it is the part that carries colour;
   the outlet and the time are labels. The accent, not an amber of its own: the page has
   one colour that means "this one", and a reader meets it first as the edge a row takes
   when they point at it. A second highlight made the list look like it was marking two
   different kinds of thing. */
/* And now it is also the one part of the row that does not lead off the page. It is still
   drawn as the word it was: given the fill and hairline the card's controls carry, it read as
   a chip stamped into the middle of a sentence, and the colour was already saying "this one".
   What it gains is a target — the row around it is a link to a news site, and a finger that
   misses by a millimetre leaves the page — so the padding is cancelled by an equal negative
   margin: the line is laid out exactly as it was and the box is 25px tall instead of 12.
   Vertical only. A horizontal pair would close the 0.5rem the flex row keeps between the
   outlet and the interpunct, which is a gap the reader can see.

   The amount is the room actually there, measured on the running page: 3px between the headline's
   box and this line. A first attempt took 0.5rem each way, which reaches 8px above —
   `elementFromPoint` at the headline's last pixel came back `where`, so a tap the reader aimed at
   the story opened the province instead. Below it takes the 7px of the row's own padding, which is
   conventionally part of the control sitting in it — an asymmetry that would read as a lopsided
   focus ring if this were a tab stop, and it is not one. Rounded like every other button here. */
#news-feed .where {
  /* A button stripped back to the word it was. The element is a `<button>` because it is
     one — it was tried as a `span role="button"` inside the row's link, which is the same
     thing the spec forbids a nested `<button>` for — and everything a browser draws around
     one is taken off here: the reader is meant to see the name they always saw. */
  appearance: none;
  border: 0;
  background: none;
  font: inherit;
  color: var(--accent);
  padding-inline: 0;
  padding-block: 3px 7px;
  margin-block: -3px -7px;
  border-radius: 999px;
  cursor: pointer;
}

/* Held until the reader moves, like every other hover on this page: these rows slide under
   a motionless cursor by themselves, and one that has not moved has not asked for anything. */
@media (hover: hover) {
  :root.pointing #news-feed .where:hover { text-decoration: underline; }
}

/* The interpunct before the province is punctuation between two things in the line, not part
   of either — but it is generated from inside the province, so it took the accent along with
   it. That was quiet while the accent was only a colour; once the word became a control with
   an underline, the separator was being underlined as a piece of it. `inline-block` is what
   stops that: a text decoration does not cross an atomic inline box. */
#news-feed .where::before {
  display: inline-block;
  color: var(--meta-ink);
}

/* The tag form, in the panel's header. Two rows, one per group; the chips share the row
   with the field that adds to them. */
#news-feed header:has(.filter) {
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
}
#news-feed .filter { display: flex; flex-direction: column; gap: 0.4rem; }
#news-feed .tags { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
/* A real flex list, not `display: contents`: contents drops the element from the
   accessibility tree, so the group's name ("Provinces" / "Keywords") vanished exactly
   when it had chips in it. Empty, it takes no room. */
#news-feed .chips { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0; padding: 0; list-style: none; }
#news-feed .chips:empty { display: none; }
#news-feed .chips li {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  padding: 0 0.15rem 0 0.6rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  font-size: 0.82rem;
  line-height: 1.7;
}
#news-feed .chips .remove {
  width: 1.4rem;
  height: 1.4rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
#news-feed .chips .remove:hover { background: #13233d; }
#news-feed .chips .remove:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
#news-feed .filter input {
  flex: 1 1 9rem;
  min-width: 9rem;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 0.5rem;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
}
#news-feed .filter input::placeholder { color: var(--dim); }
#news-feed .filter input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }
