/* ============================================================================
   AIToolsay — the SHARED LISTING DELTA
   Scope: .v2x.gdx — worn by /guides, /news and /blog
   ----------------------------------------------------------------------------
   ⚠ THIS IS NO LONGER A GUIDES-ONLY FILE. Since 2026-08-26 /news and /blog wear
     the `gdx` class alongside their own (`.v2x dir gdx nwx` / `.v2x dir gdx
     blx`) and are styled from here. The components below — the breadcrumb strip
     (§CRUMBS), the active-filter chips (§CHIP), the `.gdx-fact` meta line, the
     card line, the `is-busy` dim (§BUSY) and the four `.dir-*` corrections at
     the top — are the parts of the listing design that /ai-directory has no
     concept of, and all three pages need every one of them.

     AN EDIT HERE NOW REACHES THREE PAGES. Check /guides, /news and /blog.

     The name is kept because renaming a file whose `?v=` is `filemtime` and
     whose `text/css` is immutable for a year costs more than it buys; the
     alternative — duplicating 27KB under `.nwx` and `.blx` — is exactly the
     drift that rotted /browser-extensions' snapshot of the listing sheet in
     two days. What is genuinely news/blog-only lives in public-feed-listing.css.

   This file is ONLY what /ai-directory has no concept of. Everything else comes
   from the shared sheet, in this order:

     custom.*.css
       -> public-partials-styles.css      (the .v2x TOKEN layer)
       -> v2-public-directory-listing.css (the .dir-* design, SHARED — an edit
                                           there reaches nine public pages)
       -> public-guides.css               (this file, the delta)

   RULES FOR EDITING THIS FILE
   1. Never redefine a .dir-* rule here to "fix" it. If the directory is wrong,
      it is wrong on nine pages and belongs upstream. This file adds .gdx-*
      components and sizes the two places where the shared sheet assumes an
      <img> and a guide has an icon instead.
   2. Every colour rule needs a dark twin. The convention in the shared sheet is
      `body.theme-dark .v2x .x`, so the twin here is
      `body.theme-dark .v2x.gdx .x` — one specificity step higher, which is what
      makes it win against the shared dark rule it is paired with.
   3. Dark tokens resolve to: --white:#171922 · --v2-bg:#0E0F14 ·
      --v2-ink:#ECEDF3 · --v2-muted:#9CA2B3. Compose alpha against those, not
      against black, or every measured ratio is wrong.
   ========================================================================== */

/* ============================================================================
   §TILE — the hero tiles carry an ICON, not a logo
   The shared sheet sizes `.dir-hh-tile img` and says nothing about <svg>, so an
   inline icon renders at the UA default 300x150 and blows the tile apart.
   ========================================================================== */
.v2x.gdx .dir-hh-tile > svg    { width: 30px; height: 30px; display: block; color: var(--brand-blue-dark); }
body.theme-dark .v2x.gdx .dir-hh-tile > svg { color: var(--brand-blue-light); }

/* ============================================================================
   §IOS — stop Safari zooming the hero search on focus
   `.dir-hh-search input` is 15.5px in the SHARED sheet, and any font-size under
   16px makes iOS Safari zoom the viewport when the field takes focus. Pinned
   locally rather than upstream: the shared miss is real and wants its own pass
   across all nine pages.
   ========================================================================== */
.v2x.gdx .dir-hh-search input { font-size: 16px; }

/* ============================================================================
   §BUSY — `wire:loading.class="is-busy"` had no rule in ANY sheet in the stack
   Sixth module in a row to land this class dead. Defined locally so a filter
   change reads as pending instead of looking frozen.
   ⚠ Do not measure this in a CDP-driven tab: a hidden tab freezes every
     animated property, so `opacity` reads 1 while `pointer-events` applies.
     Strip the transition first, or instrument the call.
   ========================================================================== */
.v2x.gdx .dir-cards.is-busy,
.v2x.gdx .dir-lx.is-busy { opacity: .45; filter: saturate(.6); pointer-events: none; transition: opacity .18s ease, filter .18s ease; }

/* ============================================================================
   §CRUMBS — breadcrumb strip above the hero
   ========================================================================== */
.v2x.gdx .gdx-crumbs           { max-width: 1240px; margin: 0 auto; padding: 14px 20px 0; }
.v2x.gdx .gdx-crumbs ol        { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; padding: 0; list-style: none; font-size: 13px; }
.v2x.gdx .gdx-crumbs li        { display: flex; align-items: center; gap: 6px; color: var(--v2-muted); }
.v2x.gdx .gdx-crumbs li + li::before { content: '/'; opacity: .45; }
.v2x.gdx .gdx-crumbs a         { color: var(--v2-muted); text-decoration: none; transition: color .16s ease; }
/* #2060C0, not --brand-blue (#3680ED): the brand blue measures 3.84:1 on white
   and this is 13px text, which needs 4.5. Same fix the cheat-sheets pass made. */
.v2x.gdx .gdx-crumbs a:hover   { color: #2060C0; }
.v2x.gdx .gdx-crumbs li[aria-current] { color: var(--v2-ink); font-weight: 600; }

body.theme-dark .v2x.gdx .gdx-crumbs a:hover { color: var(--brand-blue-light); }

/* ============================================================================
   §PIPS — difficulty shown WITHOUT relying on colour (WCAG 1.4.1)
   Four pips, N filled. The pill tone only reinforces what the pips already say,
   so the level survives a screenshot, a colourblind reader and a printout.
   ========================================================================== */
.v2x.gdx .gdx-pips        { display: inline-flex; align-items: center; gap: 2px; margin-right: 5px; vertical-align: middle; }
.v2x.gdx .gdx-pips i      { width: 4px; height: 4px; border-radius: 50%; background: currentColor; opacity: .26; display: block; }
.v2x.gdx .gdx-pips i.is-on{ opacity: 1; }
/* In the facet rail the pips sit on muted text, so they need a touch more
   presence than they do inside a toned pill. */
.v2x.gdx .dir-fx-chk .gdx-pips i { width: 5px; height: 5px; }

/* ============================================================================
   §FACT — the small icon+value facts on cards and rows
   ========================================================================== */
.v2x.gdx .gdx-fact        { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; color: var(--v2-muted); white-space: nowrap; }
.v2x.gdx .gdx-fact svg    { width: 13px; height: 13px; flex: none; opacity: .85; }
.v2x.gdx .gdx-fact.is-link{ color: var(--v2-muted); text-decoration: none; transition: color .16s ease; }
.v2x.gdx .gdx-fact.is-link:hover { color: #2060C0; }

body.theme-dark .v2x.gdx .gdx-fact          { color: var(--v2-muted); }
body.theme-dark .v2x.gdx .gdx-fact.is-link:hover { color: var(--brand-blue-light); }

/* ============================================================================
   §CARDLINE — category + format on one line under the card title
   ========================================================================== */
.v2x.gdx .gdx-cardline    { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 2px 0 0; }
.v2x.gdx .gdx-fmt         { display: inline-flex; align-items: center; font-size: 11px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
                            color: var(--v2-muted); background: rgba(16, 24, 40, .05); border-radius: 999px; padding: 2px 8px; white-space: nowrap; }
body.theme-dark .v2x.gdx .gdx-fmt { background: rgba(255, 255, 255, .07); color: var(--v2-muted); }

/* Format icon in the facet rail */
.v2x.gdx .gdx-fxico       { display: inline-flex; margin-right: 6px; opacity: .8; }
.v2x.gdx .gdx-fxico svg   { width: 14px; height: 14px; }

/* ============================================================================
   §CHIPS — the active-filter row above the results
   Each chip removes exactly one filter; the last one clears everything.
   ========================================================================== */
.v2x.gdx .gdx-chiprow     { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 16px; }
.v2x.gdx .gdx-chip        { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-size: 12.5px; cursor: pointer;
                            background: #EAF2FD; color: #1A4F9E; border: 1px solid #CFE0FA; border-radius: 999px; padding: 5px 10px 5px 11px;
                            transition: background .16s ease, border-color .16s ease, transform .16s ease; }
.v2x.gdx .gdx-chip:hover  { background: #DDE9FC; border-color: #B5D0F5; transform: translateY(-1px); }
.v2x.gdx .gdx-chip-k      { opacity: .72; font-weight: 600; }
.v2x.gdx .gdx-chip-v      { font-weight: 700; }
.v2x.gdx .gdx-chip-x      { width: 12px; height: 12px; opacity: .6; flex: none; }
.v2x.gdx .gdx-chip:hover .gdx-chip-x { opacity: 1; }
.v2x.gdx .gdx-chip.is-clear { background: transparent; border-color: var(--v2-line); color: var(--v2-muted); font-weight: 600; }
.v2x.gdx .gdx-chip.is-clear:hover { background: rgba(16, 24, 40, .04); color: var(--v2-ink); }

body.theme-dark .v2x.gdx .gdx-chip        { background: rgba(54, 128, 237, .16); color: #BBD5FA; border-color: rgba(96, 164, 245, .30); }
body.theme-dark .v2x.gdx .gdx-chip:hover  { background: rgba(54, 128, 237, .24); border-color: rgba(96, 164, 245, .45); }
body.theme-dark .v2x.gdx .gdx-chip.is-clear { background: transparent; border-color: var(--v2-line); color: var(--v2-muted); }
body.theme-dark .v2x.gdx .gdx-chip.is-clear:hover { background: rgba(255, 255, 255, .06); color: var(--v2-ink); }

/* ============================================================================
   §TAGMORE — the tag facet collapses past eight rows
   Collapsed by DEFAULT in CSS, not by JS, so there is no flash of eighteen tags
   before the script runs.
   ========================================================================== */
.v2x.gdx .dir-fx-catlist .gdx-tag-extra { display: none; }
.v2x.gdx .dir-fx-catlist.is-expanded .gdx-tag-extra { display: flex; }
.v2x.gdx .dir-fx-showmore .gdx-more-off,
.v2x.gdx [aria-expanded="true"] .gdx-more-on { display: none; }
.v2x.gdx [aria-expanded="true"] .gdx-more-off { display: inline; }

/* ============================================================================
   §READS — the right-hand figure on the most-read rail
   The rail's shared slot is a star rating. Guides have no rating column, so the
   slot carries READS — which is also what the rail ranks on, so the number
   explains the order instead of decorating it.
   ========================================================================== */
/* ⚠ NO COLOUR HERE — INHERIT IT.
   On the trending rail this span also carries `.dir-tr-rt`, which the shared
   sheet paints as a designed, contrast-checked pair: #A85B08 on
   rgba(245,158,11,.14) in light, #FBC97A on rgba(245,158,11,.2) in dark.
   Forcing --v2-ink on the <b> put near-white text on that amber pill and
   measured 1.84:1 in dark mode — caught on RENDERED pixels, after a computed
   token check had passed, because the token check assumed the card background
   rather than the pill actually behind it.
   Standalone (the archive's sibling rail) it inherits `.dir-tr-row`, which the
   directory also designed. Either way the pair stays the directory's. */
.v2x.gdx .gdx-reads       { display: inline-flex; align-items: center; gap: 4px; color: inherit; }
.v2x.gdx .gdx-reads svg   { width: 13px; height: 13px; opacity: .8; }
.v2x.gdx .gdx-reads b     { font-size: 12.5px; font-weight: 800; color: inherit; }

/* ============================================================================
   §CATMORE — the "See all topics" link under the topic rail
   ========================================================================== */
.v2x.gdx .gdx-catmore     { display: flex; justify-content: center; margin-top: 18px; }
.v2x.gdx .gdx-catmore .dir-tr-more { display: inline-flex; width: auto; }

/* ============================================================================
   §TAGBAND — popular tags, a second way in
   ========================================================================== */
.v2x.gdx .gdx-tagband       { max-width: 1240px; margin: 0 auto; padding: 8px 20px 40px; }
.v2x.gdx .gdx-tagband-h     { font-size: 15px; font-weight: 700; color: var(--v2-ink); margin: 0 0 12px; }
.v2x.gdx .gdx-tagband-list  { display: flex; flex-wrap: wrap; gap: 8px; }
.v2x.gdx .gdx-tagband-tag   { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600; text-decoration: none;
                              color: var(--v2-ink); background: var(--white); border: 1px solid var(--v2-line); border-radius: 999px;
                              padding: 6px 12px; transition: border-color .16s ease, transform .16s ease, box-shadow .16s ease; }
.v2x.gdx .gdx-tagband-tag b { font-size: 11.5px; font-weight: 700; color: var(--v2-muted); }
.v2x.gdx .gdx-tagband-tag:hover { border-color: var(--brand-blue); transform: translateY(-1px); box-shadow: 0 6px 16px -8px rgba(54, 128, 237, .5); text-decoration: none; color: #2060C0; }
.v2x.gdx .gdx-tagband-tag:hover b { color: #2060C0; }

body.theme-dark .v2x.gdx .gdx-tagband-tag       { background: var(--white); border-color: var(--v2-line); color: var(--v2-ink); }
body.theme-dark .v2x.gdx .gdx-tagband-tag:hover { border-color: var(--brand-blue-light); color: var(--brand-blue-light); }
body.theme-dark .v2x.gdx .gdx-tagband-tag:hover b { color: var(--brand-blue-light); }

/* ============================================================================
   §RESP — responsive
   The shared sheet already cascades the card grid 4->3->2->1 and collapses the
   facet rail into its accordion below 1180px. These are only the deltas.
   ========================================================================== */
@media (max-width: 900px) {
    .v2x.gdx .gdx-crumbs   { padding-top: 10px; }
    .v2x.gdx .gdx-tagband  { padding-bottom: 28px; }
}

@media (max-width: 640px) {
    .v2x.gdx .gdx-chiprow      { gap: 6px; }
    .v2x.gdx .gdx-chip         { font-size: 12px; padding: 4px 9px; }
    /* The category label is the one that repeats on every card, so it is the
       one worth keeping; the format pill is redundant beside it at this width. */
    .v2x.gdx .gdx-cardline .gdx-fmt { display: none; }
    .v2x.gdx .gdx-tagband-h    { font-size: 14px; }
}

/* Touch: the shared sheet's hover lifts latch after a tap, which leaves two
   cards looking active at once. Same guard the rest of the site uses. */
@media (hover: none) {
    .v2x.gdx .gdx-tagband-tag:hover { transform: none; box-shadow: none; }
    .v2x.gdx .gdx-chip:hover        { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .v2x.gdx .gdx-tagband-tag,
    .v2x.gdx .gdx-chip,
    .v2x.gdx .dir-cards.is-busy,
    .v2x.gdx .dir-lx.is-busy { transition: none; }
}

/* ============================================================================
   §AHEAD — the archive / index head
   A COMPACT head, not the hub's hero. A reader on /guides/category/ai-coding
   already expressed their intent; a full hero with a search field and a stat
   deck would be furniture between them and the results.
   ========================================================================== */
.v2x.gdx .gdx-ahead        { max-width: 1240px; margin: 0 auto; padding: 18px 20px 6px; }
.v2x.gdx .gdx-ahead-in     { display: flex; align-items: flex-start; gap: 16px; }
.v2x.gdx .gdx-ahead-ico    { flex: none; width: 60px; height: 60px; border-radius: 18px; display: flex; align-items: center; justify-content: center;
                             background: linear-gradient(150deg, #2C6FD1 0%, #1B4C9E 100%); color: #fff;
                             box-shadow: 0 14px 30px -14px rgba(27, 76, 158, .65); }
.v2x.gdx .gdx-ahead-ico svg{ width: 30px; height: 30px; }
.v2x.gdx .gdx-ahead-copy   { min-width: 0; }
.v2x.gdx .gdx-ahead-eyebrow{ display: inline-block; font-size: 11.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: #2060C0; margin-bottom: 4px; }
.v2x.gdx .gdx-ahead-h1     { font-size: 34px; line-height: 1.16; font-weight: 800; letter-spacing: -.02em; color: var(--v2-ink); margin: 0 0 8px; }
.v2x.gdx .gdx-ahead-sub    { font-size: 15px; line-height: 1.6; color: var(--v2-muted); margin: 0 0 10px; max-width: 66ch; }
.v2x.gdx .gdx-ahead-facts  { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; }
.v2x.gdx .gdx-ahead-kids   { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }

body.theme-dark .v2x.gdx .gdx-ahead-eyebrow { color: var(--brand-blue-light); }
body.theme-dark .v2x.gdx .gdx-ahead-h1      { color: var(--v2-ink); }
/* The plate keeps its own gradient in dark: both stops clear 4.5:1 against a
   white glyph, and a token-driven plate would go muddy on --white:#171922. */
body.theme-dark .v2x.gdx .gdx-ahead-ico     { box-shadow: 0 14px 30px -16px rgba(0, 0, 0, .8); }

/* ============================================================================
   §ARCSEARCH — the archive's scoped search
   Sits above the results rather than in a hero, because the head is compact.
   16px, or iOS Safari zooms the viewport on focus.
   ========================================================================== */
.v2x.gdx .gdx-arcsearch      { position: relative; display: flex; align-items: center; margin: 0 0 14px; }
.v2x.gdx .gdx-arcsearch-ico  { position: absolute; left: 14px; display: flex; color: var(--v2-muted); pointer-events: none; }
.v2x.gdx .gdx-arcsearch input{ width: 100%; font: inherit; font-size: 16px; color: var(--v2-ink); background: var(--white);
                               border: 1px solid var(--v2-line); border-radius: 12px; padding: 11px 40px 11px 42px;
                               transition: border-color .16s ease, box-shadow .16s ease; }
.v2x.gdx .gdx-arcsearch input::placeholder { color: var(--v2-muted); }
.v2x.gdx .gdx-arcsearch input:focus { outline: none; border-color: var(--brand-blue); box-shadow: 0 0 0 3px rgba(54, 128, 237, .18); }
.v2x.gdx .gdx-arcsearch-x    { position: absolute; right: 10px; display: flex; align-items: center; justify-content: center; width: 26px; height: 26px;
                               border: 0; border-radius: 50%; background: transparent; color: var(--v2-muted); cursor: pointer; }
.v2x.gdx .gdx-arcsearch-x svg{ width: 14px; height: 14px; }
.v2x.gdx .gdx-arcsearch-x:hover { background: rgba(16, 24, 40, .06); color: var(--v2-ink); }
body.theme-dark .v2x.gdx .gdx-arcsearch-x:hover { background: rgba(255, 255, 255, .08); }

/* ============================================================================
   §CATGRID — /guides/categories
   A card per topic with three REAL guides under it, so the index is useful
   rather than a menu of names.
   ========================================================================== */
.v2x.gdx .gdx-catgrid      { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; padding: 10px 0 34px; }
.v2x.gdx .gdx-catcard      { display: flex; flex-direction: column; background: var(--white); border: 1px solid var(--v2-line); border-radius: 18px;
                             padding: 18px; transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease; }
.v2x.gdx .gdx-catcard:hover{ border-color: var(--brand-blue); transform: translateY(-2px); box-shadow: 0 18px 38px -22px rgba(16, 24, 40, .3); }
.v2x.gdx .gdx-catcard-head { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; }
.v2x.gdx .gdx-catcard-head:hover { text-decoration: none; }
.v2x.gdx .gdx-catcard-ico  { flex: none; width: 46px; height: 46px; border-radius: 14px; display: flex; align-items: center; justify-content: center;
                             background: var(--dtc-bg, #EFF4FE); color: var(--dtc-ink-dark, #1B4C9E); }
.v2x.gdx .gdx-catcard-copy { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.v2x.gdx .gdx-catcard-nm   { font-size: 16px; font-weight: 700; color: var(--v2-ink); line-height: 1.3; }
.v2x.gdx .gdx-catcard-cnt  { font-size: 12px; font-weight: 600; color: var(--v2-muted); margin-top: 2px; }
.v2x.gdx .gdx-catcard-arr  { flex: none; display: flex; color: var(--v2-muted); transition: transform .18s ease, color .18s ease; }
.v2x.gdx .gdx-catcard-arr svg { width: 18px; height: 18px; }
.v2x.gdx .gdx-catcard:hover .gdx-catcard-arr { color: var(--brand-blue); transform: translateX(3px); }
.v2x.gdx .gdx-catcard-desc { font-size: 13.5px; line-height: 1.55; color: var(--v2-muted); margin: 12px 0 0; }
.v2x.gdx .gdx-catcard-list { list-style: none; margin: 12px 0 0; padding: 12px 0 0; border-top: 1px solid var(--v2-line); display: flex; flex-direction: column; gap: 2px; }
.v2x.gdx .gdx-catcard-list a { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 8px; margin: 0 -8px;
                               border-radius: 9px; text-decoration: none; color: var(--v2-ink); transition: background .14s ease, color .14s ease; }
.v2x.gdx .gdx-catcard-list a:hover { background: #F4F8FE; color: #2060C0; text-decoration: none; }
.v2x.gdx .gdx-catcard-lnk  { font-size: 13.5px; font-weight: 600; line-height: 1.4; }
.v2x.gdx .gdx-catcard-list .gdx-pips { flex: none; }
.v2x.gdx .gdx-catcard-more { display: inline-flex; align-items: center; gap: 5px; margin-top: 12px; font-size: 12.5px; font-weight: 700;
                             color: #2060C0; text-decoration: none; }
.v2x.gdx .gdx-catcard-more svg { width: 14px; height: 14px; }
.v2x.gdx .gdx-catcard-more:hover { color: #17408A; }

body.theme-dark .v2x.gdx .gdx-catcard          { background: var(--white); border-color: var(--v2-line); }
body.theme-dark .v2x.gdx .gdx-catcard:hover    { border-color: var(--brand-blue-light); box-shadow: 0 18px 38px -22px rgba(0, 0, 0, .7); }
body.theme-dark .v2x.gdx .gdx-catcard-ico      { background: rgba(54, 128, 237, .16); color: var(--brand-blue-light); }
body.theme-dark .v2x.gdx .gdx-catcard-list a:hover { background: rgba(255, 255, 255, .05); color: var(--brand-blue-light); }
body.theme-dark .v2x.gdx .gdx-catcard-more     { color: var(--brand-blue-light); }
body.theme-dark .v2x.gdx .gdx-catcard-more:hover { color: #9CC4FA; }

/* ============================================================================
   §RESP2 — archive + index responsive
   ========================================================================== */
@media (max-width: 1100px) {
    .v2x.gdx .gdx-catgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
    .v2x.gdx .gdx-ahead-h1  { font-size: 28px; }
    .v2x.gdx .gdx-ahead-ico { width: 52px; height: 52px; border-radius: 15px; }
    .v2x.gdx .gdx-ahead-ico svg { width: 26px; height: 26px; }
}

@media (max-width: 680px) {
    .v2x.gdx .gdx-catgrid   { grid-template-columns: minmax(0, 1fr); gap: 14px; }
    .v2x.gdx .gdx-ahead     { padding: 14px 16px 4px; }
    .v2x.gdx .gdx-ahead-in  { gap: 12px; }
    .v2x.gdx .gdx-ahead-h1  { font-size: 24px; }
    .v2x.gdx .gdx-ahead-sub { font-size: 14px; }
    .v2x.gdx .gdx-ahead-ico { width: 44px; height: 44px; border-radius: 13px; }
    .v2x.gdx .gdx-ahead-ico svg { width: 22px; height: 22px; }
}

@media (hover: none) {
    .v2x.gdx .gdx-catcard:hover { transform: none; box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
    .v2x.gdx .gdx-catcard,
    .v2x.gdx .gdx-catcard-arr,
    .v2x.gdx .gdx-arcsearch input { transition: none; }
}

/* ============================================================================
   §SAVED — /guides/saved
   ========================================================================== */
.v2x.gdx .gdx-note        { display: flex; align-items: flex-start; gap: 10px; margin: 14px 0 0; padding: 11px 14px; border-radius: 12px;
                            background: #F4F8FE; border: 1px solid #CFE0FA; color: #1A4F9E; font-size: 13.5px; line-height: 1.55; }
.v2x.gdx .gdx-note svg    { flex: none; margin-top: 2px; }
.v2x.gdx .gdx-note p      { margin: 0; }
.v2x.gdx .gdx-note a      { color: #17408A; font-weight: 700; }
.v2x.gdx .gdx-note.is-ok  { background: #EDF9F2; border-color: #BFE6D2; color: #1B5E3F; }
.v2x.gdx .gdx-note.is-ok a{ color: #14472F; }

body.theme-dark .v2x.gdx .gdx-note       { background: rgba(54, 128, 237, .12); border-color: rgba(96, 164, 245, .28); color: #BBD5FA; }
body.theme-dark .v2x.gdx .gdx-note a     { color: #9CC4FA; }
body.theme-dark .v2x.gdx .gdx-note.is-ok { background: rgba(34, 160, 100, .14); border-color: rgba(80, 200, 140, .30); color: #A6E4C4; }
body.theme-dark .v2x.gdx .gdx-note.is-ok a { color: #C9F0DC; }

/* Reading progress on a saved row. */
.v2x.gdx .gdx-prog        { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.v2x.gdx .gdx-prog-bar    { flex: 1; max-width: 260px; height: 6px; border-radius: 999px; background: rgba(16, 24, 40, .09); overflow: hidden; }
.v2x.gdx .gdx-prog-bar span { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--brand-blue), var(--brand-blue-dark)); }
.v2x.gdx .gdx-prog small  { font-size: 12px; font-weight: 600; color: var(--v2-muted); white-space: nowrap; }

body.theme-dark .v2x.gdx .gdx-prog-bar { background: rgba(255, 255, 255, .10); }

/* ============================================================================
   §ARCHIVE-HERO — `.dir-hh.is-compact`
   ----------------------------------------------------------------------------
   `is-compact` has NO rule in any sheet in this stack, so the archive hero was
   rendering at the hub's full 44px height with none of the hub's content —
   no tiles, no counter deck, no quick chips — pushing the first result below
   the fold on a laptop. The base is `.dir-hh{padding:44px 16px 8px}` and steps
   down to 32px then 24px at the shared sheet's own breakpoints, so this
   mirrors those steps rather than inventing a third scale.

   Scoped to `.v2x.gdx` so it cannot reach the other seven pages that share the
   directory sheet.
   ========================================================================== */
.v2x.gdx .dir-hh.is-compact { padding: 28px 16px 4px; }
.v2x.gdx .dir-hh.is-compact .dir-hh-title { margin-bottom: 6px; }
.v2x.gdx .dir-hh.is-compact .dir-hh-sub   { margin-bottom: 14px; }

/* The decoration is dialled back rather than removed — removing it makes the
   archive read as a different template from the hub.

   ⚠ This used to target `.dir-hh-arc`, the two mirrored SVGs the orbit was
   built from before 2026-08-17. Those are gone: the ring is now ONE ellipse
   that sizes itself to the hero box, so at this height it no longer crops and
   the reason the rule was written has been designed out. What is left is a
   composition problem — a compact hero is mostly headline, and a full-strength
   starfield behind three lines of copy is louder than the copy. The ring holds
   its weight (it is the shape that ties this page to the hub) and the field
   and the glints step back behind it.

   ⚠ Do NOT re-point this at `.dir-hh-orb` as a straight rename of the old rule.
   Fading the ring here would leave the tiles on an invisible path — which is
   exactly the mismatch §HH-ORB was written to remove.

   ⚠ `filter: opacity()`, NOT the `opacity` property. Both of these elements
   ANIMATE their own opacity (`dir-hh-star-twinkle`, `dir-hh-spark-flash`), and
   a running animation outranks a normal declaration in the cascade — an
   `opacity: .45` here would simply never apply and the rule would look correct
   while doing nothing. A filter is a separate stage that MULTIPLIES the
   animated value, so the twinkle survives at a lower amplitude, which is what
   "step back" is supposed to mean. */
.v2x.gdx .dir-hh.is-compact .dir-hh-stars { filter: opacity(.55); }
.v2x.gdx .dir-hh.is-compact .dir-hh-spark { filter: opacity(.5); }

@media (max-width: 900px) {
    .v2x.gdx .dir-hh.is-compact { padding: 22px 16px 4px; }
}
@media (max-width: 640px) {
    .v2x.gdx .dir-hh.is-compact { padding: 18px 12px 4px; }
}

/* ============================================================================
   §CATBOX — the CATEGORY listbox in the results toolbar (2026-08-26)
   ----------------------------------------------------------------------------
   /news and /blog dropped their 236px facet rail and moved the one filter that
   earned its space — category — into the toolbar, beside Sort and Grid/List.
   It reuses `.dir-sortbox` / `.dir-sort-btn` / `.dir-ts-*` wholesale so the two
   controls read as one family; only the two things a SORT box never needed are
   added here.

   ⚠ These live in the SHARED delta, not in public-feed-listing.css, because
     nothing about them is news- or blog-specific: /guides can adopt the same
     control by passing `catOpts` to `_dir-toolbar` and it will already be
     styled. Both are additive — no existing rule changes.

   ⚠ TWO `[data-dir-sortbox]` ON ONE PAGE ONLY WORK because `_dir-scripts`
     resolves each box from the clicked element. It used to take the first
     match document-wide.
   ========================================================================== */

/* A category name is arbitrary user data — "Fashion & Beauty Tools" is as
   likely as "News". `.dir-sort-btn` is `white-space:nowrap`, so without a cap
   one long name stretches the trigger until the Grid/List group wraps to its
   own line. Capped and ellipsised instead; the full name is still in the menu
   and on the button's aria-label. */
.v2x.gdx .dir-catbox .dir-ts-val {
    display: block;
    max-width: 148px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
@media (max-width: 900px) {
    .v2x.gdx .dir-catbox .dir-ts-val { max-width: 108px; }
}

/* ⚠ THE COUNT BADGE `.dir-ts-opt-n` WAS REMOVED (2026-08-26) — markup and all
   four of its rules. It printed the same live count the facet rail used to
   show, but the component already DROPS any category that would return
   nothing, so on these catalogues every surviving row printed "1": a column of
   identical figures that carried no information and turned each row into two
   chips beside the tone plate. The count is still passed to the toolbar and
   still does the dropping; it is only never painted. Nothing below this point
   reserves space for it — `.dir-ts-opt-lb` is `flex:1`, so the tick sits hard
   right with or without a badge. */


/* ============================================================================
   §COVER — a full-width featured image on the card, replacing the icon plate
   (2026-08-26)
   ----------------------------------------------------------------------------
   /guides, /news and /blog used the directory's `.dir-tc-logo` — a 66px square
   tinted plate holding either a category glyph or a shrunk-to-60% thumbnail.
   That is the right shape for a TOOL card, where the art is a logo. It is the
   wrong shape for an article, where the art is a photograph and the whole
   point is to show it.

   `.gdx-cover` is a NEW class rather than a modifier on `.dir-tc-logo`, on
   purpose: the shared sheet gives that plate a hover transform
   (`translateY(-2px) scale(1.06)`) which on a full-bleed band would lift the
   image off the card edge and reveal the background behind it. Starting clean
   means nothing to fight.

   ⚠ THE NEGATIVE MARGINS MIRROR `.dir-tc`'s PADDING AND MUST TRACK IT.
     The card is `padding: 42px 16px 18px`, dropping to `20px 16px 18px` at
     ≤680px. The cover cancels that padding to reach the card edge, so a change
     to either padding without the matching change here leaves a visible band
     of card background above or beside the image. The card already carries
     `overflow:hidden` and a 20px radius, so the top corners clip for free.

   ⚠ `align-self: stretch` RATHER THAN `width: calc(100% + 32px)`.
     `.dir-tc` is `align-items:center`, so a child wider than the content box
     would be centred and overflow — which happens to look identical until a
     card gets an odd width. Stretch plus negative margins is deterministic.
   ========================================================================== */
.v2x.gdx .gdx-cover {
    align-self: stretch;
    margin: -42px -16px 16px;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    /* The card's own positional tone, so the fallback glyph and any letterbox
       sit on the colour this card was already assigned. */
    background: var(--dtc-bg);
    flex: 0 0 auto;
    position: relative;
}

.v2x.gdx .gdx-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    /* A photo scaled a hair on hover reads as "this card is live" without the
       card itself moving. Paired with the .dir-tc hover the sheet already has. */
    transform: scale(1);
    transition: transform .5s var(--ease-out);
}

/* ── No image: the category glyph, centred on the tone ──────────────────────
   /guides has the `featured_image` column but NOT ONE of its 37 rows has a
   value, so this is what that page renders today — a deliberate tinted band
   rather than a hole. It becomes photographic on its own the moment images are
   uploaded; no code change. */
.v2x.gdx .gdx-cover.is-icon { display: grid; place-items: center; }
.v2x.gdx .gdx-cover-ico     { color: var(--dtc-ink-dark); display: grid; place-items: center; opacity: .9; }
.v2x.gdx .gdx-cover-ico svg { width: 42px; height: 42px; }

/* ── The bookmark button has to survive landing on a photograph ─────────────
   `.dir-tc-bk` is `position:absolute; top:11px; right:11px` with a TRANSPARENT
   background and a pale `#c2c6d4` glyph — fine over a white card, invisible
   over a light image. Only /guides renders one, and /guides has no images
   today, so this is protection for the day it does. `:has()` keeps the plate
   off cards that have no cover at all. */
.v2x.gdx .dir-tc:has(.gdx-cover:not(.is-icon)) .dir-tc-bk {
    background: rgba(255, 255, 255, .82);
    color: #46506B;
    box-shadow: 0 2px 6px rgba(16, 24, 40, .18);
    backdrop-filter: saturate(1.4) blur(4px);
}
.v2x.gdx .dir-tc:has(.gdx-cover:not(.is-icon)) .dir-tc-bk:hover { background: #fff; color: var(--brand-blue); }
body.theme-dark .v2x.gdx .dir-tc:has(.gdx-cover:not(.is-icon)) .dir-tc-bk {
    background: rgba(18, 20, 27, .74);
    color: #D6DAE6;
    box-shadow: 0 2px 6px rgba(0, 0, 0, .5);
}

@media (hover: hover) {
    .v2x.gdx .dir-tc:hover .gdx-cover img { transform: scale(1.05); }
}

/* ⚠ The card's top padding halves at this width, so the pull-up must too. */
@media (max-width: 680px) {
    .v2x.gdx .gdx-cover { margin: -20px -16px 14px; }
}

@media (prefers-reduced-motion: reduce) {
    .v2x.gdx .gdx-cover img { transition: none; transform: none !important; }
}


/* ============================================================================
   §ROWMEDIA — the same picture-first idea in LIST mode
   ----------------------------------------------------------------------------
   Here `.dir-lc-logo` is KEPT and widened rather than replaced: unlike the card
   plate, its inherited hover (`scale(1.05)`) reads correctly on a landscape
   thumbnail, and keeping the element means the row's flex layout, its radius
   and its dark twin all continue to apply. `.gdx-rowcover` only changes the
   geometry.

   Landscape rather than square because these are article thumbnails — a 16:9
   press photo cropped to a square loses the part of the frame that carries the
   subject. Sizes step down with the row's own padding at 680 and 400.
   ========================================================================== */
.v2x.gdx .dir-lc-logo.gdx-rowcover {
    width: 132px;
    height: 84px;
    border-radius: 12px;
    background: var(--dtc-bg);
    overflow: hidden;
}
.v2x.gdx .dir-lc-logo.gdx-rowcover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.v2x.gdx .dir-lc-logo.gdx-rowcover.is-icon { display: inline-grid; place-items: center; }

@media (max-width: 680px) {
    .v2x.gdx .dir-lc-logo.gdx-rowcover { width: 96px; height: 62px; border-radius: 10px; }
    .v2x.gdx .dir-lc-logo.gdx-rowcover .gdx-cover-ico svg { width: 26px; height: 26px; }
}
@media (max-width: 400px) {
    .v2x.gdx .dir-lc-logo.gdx-rowcover { width: 76px; height: 50px; border-radius: 9px; }
    .v2x.gdx .dir-lc-logo.gdx-rowcover .gdx-cover-ico svg { width: 22px; height: 22px; }
}


/* ============================================================================
   §HCHIPS — the hero quick-filter row, rebuilt on the HOMEPAGE chip design
   (2026-08-26)
   ----------------------------------------------------------------------------
   The three listings each carried a bare `.dir-hh-chips` line — a small muted
   label and a run of flat outline pills that wrapped onto a second row as soon
   as a catalogue grew. The homepage solved the same problem properly in
   `.hpx-chipsrow`: one pill-shaped container holding a labelled brand mark, a
   single-line scrolling rail, and prev/next arrows that appear only when there
   is something to scroll.

   THIS IS A PORT OF THAT DESIGN, NOT A BORROW OF THE KIT. Loading the homepage
   kit on these three pages would mean:
     • `ThemeCompiler::css()` on every one of them, because `.hpx [data-tone]`
       rules have NO fallback and paint transparent without the compiled
       `--hpx-t-*` block (see the /services rebuild note);
     • homepage.css and the kit JS as new dependencies, for one row;
     • and `data-hpx-seed` on a chip makes the kit SWALLOW the click and open
       the search popup — fatal here, where a chip must run a Livewire filter.
   Porting the look into this delta costs ~90 lines and no dependencies.

   ⚠ THE RAIL REUSES THE CATEGORY BAND'S HOOKS — `data-dir-cat-railwrap`,
     `data-dir-cat-rail`, `data-dir-cat-nudge`. That JS in `_dir-scripts` is
     already multi-instance (querySelectorAll + closest), so a second rail on
     the page needs NO new script. It writes `data-scrollable` /
     `-at-start` / `-at-end` onto the WRAPPER, which is what the arrow rules
     below key on — an arrow that scrolls nothing is never painted.
     ...AND IT ALSO INJECTS A DRAG BAR. See the `.dir-cat-railbar` rule below:
     borrowing the hooks borrows `ensureBar()` too, and on a flex wrapper that
     bar is a flex ITEM. That is what this row's second pass was mostly about.

   ⚠ THE HOMEPAGE CHIPS HAVE NO SELECTED STATE; THESE DO. `.is-on` is the one
     thing this design adds — a brand-filled pill, because these are stateful
     filter toggles and a reader has to see which one is running. It is also
     why HOVER here takes the homepage's lift and glow but NOT its fill.

   ── Second pass, same day ──────────────────────────────────────────────────
   The first port looked right in isolation and wrong on the page, because the
   injected drag bar was quietly holding 193px of a 780px track: /guides sliced
   `Advanced` in half, hid `Expert` behind an arrow, and showed scroll controls
   for four chips that fit. Fixed here, plus four things the port had left off
   the homepage recipe:
     • the track SHRINK-WRAPS now (`fit-content`) — /news has one chip and
       /blog two, and a fixed 780px pill around one 141px chip reads broken;
     • the chips carry the homepage's real fill recipe (`--sc-fill`, copied in
       as `--gdx-fill`) and its sweep, instead of a flat `--brand-blue`;
     • 18px chip padding and 34px arrows, the homepage's own numbers;
     • focus draws an OUTLINE, so `.is-on`'s box-shadow can no longer eat it.
   ========================================================================== */

.v2x.gdx .gdx-hchips {
    position: relative;
    display: flex;
    align-items: center;
    gap: 12px;
    /* ⚠ SHRINK-TO-FIT, NOT A FIXED TRACK — the one place this deliberately
       parts company with `.hpx-chipsrow`. The homepage row is `width:100%`
       inside its 860px hero copy because it ALWAYS carries seven chips, so a
       full-bleed track is a full track. These three do not: /news publishes
       ONE freshness window and /blog two categories, and a 780px pill holding
       a single 137px chip reads as a component that failed to load rather
       than as a shortcut.

       `fit-content` hugs whatever the module actually has; `max-width:100%`
       still hands the row the full width of `.dir-hh-copy` the moment a set
       outgrows it, at which point the rail scrolls and the arrows appear
       exactly as they do upstream. Only the sparse case changes. */
    width: fit-content;
    max-width: 100%;
    margin: 26px auto 0;
    padding: 8px 10px;
    border-radius: 999px;
    background: rgba(16, 24, 40, .035);
    box-shadow: inset 0 0 0 1px var(--v2-line);
}
body.theme-dark .v2x.gdx .gdx-hchips {
    background: rgba(255, 255, 255, .035);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06);
}

/* ⚠⚠ THE DRAG BAR THE SHARED RAIL JS INJECTS HAS TO BE TAKEN BACK OUT.
   Reusing `data-dir-cat-railwrap` buys the arrows and the scroll-state
   attributes for free, but `ensureBar()` in public-dir-catband.js ALSO appends
   a `.dir-cat-railbar` to every wrapper it binds, unconditionally.

   In a category band the wrapper is a block and the bar lands on its own line
   under the cards, which is what it was drawn for. HERE THE WRAPPER IS
   `display:flex`, so the bar becomes a FIFTH FLEX ITEM sitting on the row —
   `width: min(260px, 55%)` with a default `flex-shrink`, measured at 193px of
   the 780px track on /guides. That is where the row's space was going: the
   rail was handed 371px for 497px of chips, so `Advanced` was sliced through
   the middle, `Expert` was unreachable, and the arrows turned up on a set of
   four that fits with room to spare. The bar paints at `rgba(255,255,255,.10)`
   and 8px tall, so on the dark hero the hole read as a layout bug rather than
   as a control anyone could use.

   Hidden rather than not-injected: the injection is delegated and re-runs on
   every Livewire re-render, so a JS opt-out would have to be maintained in a
   file five other bands depend on. `display:none` also takes it off the flex
   line outright — `visibility` or `opacity` would leave the 193px behind. The
   homepage row carries no drag bar either, so this IS the port. */
.v2x.gdx .gdx-hchips > .dir-cat-railbar { display: none; }

/* ── The homepage's filled-control recipe, ported ──────────────────────────
   `--sc-fill` / `--sc-glow` are declared inside `.hpx` in homepage.css, which
   these three pages do not load. Copied here under local names rather than
   reached for: an undefined custom property makes `background-image` compute
   to `none`, and the selected chip would fill with nothing at all.

   Values verbatim from homepage.css — the colour LEADS the ramp so the
   top-left corner is #357FEC and every pixel a white label can sit on measures
   5.15:1 or better, and the dark twin lays that file's scrim rather than
   inventing a second blue. Declared on the row; the chips inherit. */
.v2x.gdx .gdx-hchips {
    --gdx-fill: linear-gradient(135deg, #357FEC 0%, #2B6BCC 30%, #1B4E9C 100%);
    --gdx-glow: rgba(53, 127, 236, .40);
}
body.theme-dark .v2x.gdx .gdx-hchips {
    --gdx-fill:
        linear-gradient(0deg, rgba(9, 18, 33, .34), rgba(9, 18, 33, .34)),
        linear-gradient(135deg, #357FEC 0%, #2B6BCC 34%, #1B4E9C 100%);
    --gdx-glow: rgba(53, 127, 236, .52);
}

/* ── The label, with the homepage's soft brand mark ─────────────────────── */
.v2x.gdx .gdx-hchips-lbl {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: 0 0 auto;
    padding-inline-start: 6px;
    color: var(--v2-muted);
    font-size: 13px;
    font-weight: 680;
    letter-spacing: -.005em;
    white-space: nowrap;
}
.v2x.gdx .gdx-hchips-ico {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    width: 24px; height: 24px;
    border-radius: 8px;
    background: var(--v2-accent-soft);
    /* #2060C0, not --brand-blue: this is a small mark on a pale tint and the
       brand blue measures under 4.5:1 against it. Same call §CRUMBS made. */
    color: #2060C0;
    box-shadow: inset 0 0 0 1px rgba(54, 128, 237, .22);
}
.v2x.gdx .gdx-hchips-ico svg { width: 13px; height: 13px; }
body.theme-dark .v2x.gdx .gdx-hchips-ico { background: rgba(54, 128, 237, .16); color: var(--brand-blue-light); box-shadow: inset 0 0 0 1px rgba(96, 164, 245, .24); }

/* ── A rail, not a wrap ────────────────────────────────────────────────────
   The old row wrapped, so a catalogue that grew to ten levels silently pushed
   the counter deck down two lines. One line that scrolls keeps the hero's
   height fixed no matter how many chips a module ends up with.
   ⚠ `scrollbar-width:none` MUST come with the ::-webkit rule or Chrome keeps
     painting a bar; see the scrollbar memory — declaring `scrollbar-width`
     makes Chrome DROP the ::-webkit-scrollbar rules, so the order matters. */
.v2x.gdx .gdx-hchips-rail {
    display: flex;
    align-items: center;
    gap: 9px;
    flex: 1 1 auto;
    min-width: 0;
    /* Room for the chip's 1px hover lift, which `overflow` would otherwise
       clip flat. Same 3px/2px the homepage rail uses. */
    padding: 3px 2px;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;

    /* ── The cut edge is FADED, not sliced ──────────────────────────────────
       The rail overflows by design, but a chip sliced mid-word reads as a
       layout bug rather than as "there is more" — which is exactly how
       "Advanced" looked on /guides, chopped through the middle and butted
       against the next arrow. Fading the cut edge turns it into the
       affordance instead. Ported from the homepage rail, same idea and same
       distances.

       ⚠ KEYED OFF THE WRAPPER, NOT THE RAIL. The homepage's `syncRail()`
         stamps the attributes onto BOTH the rail and its wrapper; the shared
         `_dir-scripts` rail JS this row reuses stamps the WRAPPER ONLY. So the
         setters below descend from `.gdx-hchips[…]` rather than sitting on the
         rail as they do upstream — copying the homepage selector verbatim
         matches nothing here and the fade silently never appears.

       The fade shows only on the side that actually has more to reach, and a
       rail short enough not to scroll is never dimmed at all. `mask` rather
       than a gradient overlay, so it works on any surface and needs no dark
       twin. */
    --gdx-fade-l: 0px;
    --gdx-fade-r: 0px;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--gdx-fade-l),
                                        #000 calc(100% - var(--gdx-fade-r)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--gdx-fade-l),
                                #000 calc(100% - var(--gdx-fade-r)), transparent 100%);
}

/* ⚠ NO `scroll-behavior: smooth` ON THE RAIL. It applies to a direct
   `scrollLeft` assignment as well as to scrollBy(), so any instant fallback
   would animate too — and when the animation is the thing being suppressed
   (a background tab, reduced-motion), that fallback stalls exactly like the
   call it exists to rescue. `_dir-scripts` already asks for smoothness
   per-call via `scrollBy({behavior:'smooth'})`, which is the right place.
   Removing it here also stopped a measurement stalling during testing. */

.v2x.gdx .gdx-hchips[data-scrollable="1"]:not([data-at-end="1"])   .gdx-hchips-rail { --gdx-fade-r: 34px; }
.v2x.gdx .gdx-hchips[data-scrollable="1"]:not([data-at-start="1"]) .gdx-hchips-rail { --gdx-fade-l: 24px; }

.v2x.gdx .gdx-hchips-rail::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* ── The chip ─────────────────────────────────────────────────────────────
   The homepage's raised pill: a card-coloured ground with a top-light, a
   hairline border and a 1px contact shadow, so it reads as lifted rather than
   as an outlined box. */
.v2x.gdx .gdx-hchip {
    position: relative;
    /* Both for the sweep on the selected chip: `isolation` gives the pill its
       own stacking context so a `z-index:-1` pseudo paints ABOVE the chip's
       background and BELOW its label without the label needing an element of
       its own, and `overflow` keeps the skewed band inside the pill. Same
       two lines `.hpx-chip` carries, same reason. */
    isolation: isolate;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: 0 0 auto;
    /* 18px, the homepage's own chip padding — the port had trimmed it to 16
       to claw back width from a rail that was short because the drag bar was
       eating it, not because the chips were wide. */
    padding: 9px 18px;
    background-color: #fff;
    background-image: linear-gradient(180deg, rgba(255, 255, 255, .55) 0%, rgba(255, 255, 255, 0) 62%);
    border: 1px solid var(--v2-line);
    border-radius: 999px;
    color: var(--v2-ink);
    font-family: inherit;
    font-size: 13.5px;
    font-weight: 570;
    line-height: 1.2;
    white-space: nowrap;
    cursor: pointer;
    scroll-snap-align: start;
    box-shadow: 0 1px 2px rgba(16, 24, 40, .05);
    transition: background-color .25s ease, background-image .25s ease, border-color .25s ease,
                color .25s ease, transform .25s var(--ease-out), box-shadow .25s ease;
}

/* The homepage's sweep. It only ever runs on the SELECTED chip, because a
   white band is invisible crossing a white pill — upstream the chip has
   filled by the time this fires, and here the filled chip is the on one. */
.v2x.gdx .gdx-hchip::after {
    content: '';
    position: absolute;
    z-index: -1;
    inset-block: -40%;
    inset-inline-start: -55%;
    width: 42%;
    background: linear-gradient(100deg, transparent 0%, rgba(255, 255, 255, .55) 50%, transparent 100%);
    transform: skewX(-18deg);
    opacity: 0;
    pointer-events: none;
}

/* One keyframe, distances and timing verbatim from homepage.css `hpx-sweep`,
   under a local name — homepage.css is not loaded on these three pages, so
   naming the upstream animation here would animate nothing. */
@keyframes gdx-sweep {
    0%   { opacity: 0; transform: translateX(0) skewX(-18deg); }
    16%  { opacity: 1; }
    100% { opacity: 0; transform: translateX(430%) skewX(-18deg); }
}
.v2x.gdx .gdx-hchip svg { width: 14px; height: 14px; flex: 0 0 auto; opacity: .9; }

/* A 55% white top-light belongs on a light pill and blows out a dark one. */
body.theme-dark .v2x.gdx .gdx-hchip {
    background-color: rgba(44, 44, 49, .92);
    background-image: linear-gradient(180deg, rgba(255, 255, 255, .07) 0%, rgba(255, 255, 255, 0) 62%);
    border-color: var(--dk-line, rgba(255, 255, 255, .10));
    color: var(--v2-ink);
    box-shadow: 0 1px 2px rgba(0, 0, 0, .4);
}

/* ── Hover: the homepage's LIFT, deliberately not its FILL ─────────────────
   `.hpx-chip:hover` fills with `--sc-fill`, because on the homepage a chip has
   no other state to be confused with. These chips do: the running filter is a
   filled pill, and a hover that also fills would put two chips in the selected
   costume at once — on a four-item level rail that is a real misread, not a
   nicety. So hover takes everything else the homepage hover does — the 2px
   lift, the brand glow beneath, the brand border and ink — and leaves the
   fill to mean one thing. */
/* ⚠ `:not(.is-on)` ON BOTH HOVER RULES, and not for tidiness. The dark REST
   rule above is `body.theme-dark .v2x.gdx .gdx-hchip` — (0,5,1), an element
   plus five classes — which outranks a bare `.is-on` at (0,4,0). Leaving the
   two states to fight it out on specificity is how the selected chip loses its
   fill after dark. Making them mutually exclusive selectors means neither can
   reach the other's chip at all, whatever the counts say. */
@media (hover: hover) {
    .v2x.gdx .gdx-hchip:not(.is-on):hover {
        background-color: var(--v2-accent-soft);
        /* The rest-state top-light is 55% white, which over a #eaf1fe tint
           lands at #f6f9fe and all but erases the hover. Stepped down rather
           than dropped: `none` would take the pill's raised edge away at the
           exact moment it lifts. */
        background-image: linear-gradient(180deg, rgba(255, 255, 255, .30) 0%, rgba(255, 255, 255, 0) 62%);
        border-color: var(--brand-blue-light);
        /* #2060C0, not --brand-blue: the label now sits on a pale brand tint
           rather than on white, and the brand token falls under 4.5:1 there.
           Same call the label's icon plate makes ten rules up. */
        color: #2060C0;
        transform: translateY(-2px);
        box-shadow: 0 10px 22px -8px var(--gdx-glow);
    }
    body.theme-dark .v2x.gdx .gdx-hchip:not(.is-on):hover {
        background-color: rgba(54, 128, 237, .16);
        color: var(--brand-blue-light);
        border-color: var(--brand-blue);
    }
}
.v2x.gdx .gdx-hchip:active { transform: translateY(0) scale(.96); }

/* An OUTLINE, not a box-shadow ring. The ring this used to draw was a
   `box-shadow`, and `.is-on` sets `box-shadow` too — one property, last rule
   wins, so tabbing onto the selected chip showed no focus state at all.
   `outline` is a different property and nothing here competes for it, which
   is also why the homepage draws its ring that way. */
.v2x.gdx .gdx-hchip:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
    border-color: var(--brand-blue);
}

/* ── The selected state the homepage never needed ──────────────────────────
   These chips are filter toggles, so the running one has to be unmistakable.
   A solid brand fill rather than a tint: the row sits on the hero's own
   gradient, and a tinted "on" state reads as a hover on that ground. */
.v2x.gdx .gdx-hchip.is-on {
    /* The homepage's own filled-control gradient, not a flat `--brand-blue`.
       The floor underneath it matters: a #fff label on nothing at all is a
       hidden label anywhere the gradient does not paint (forced-colors,
       print), and #2B6BCC is the ramp's 30% stop, where white first clears
       4.5:1. */
    background-color: #2B6BCC;
    background-image: var(--gdx-fill);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 10px 22px -8px var(--gdx-glow);
}
.v2x.gdx .gdx-hchip.is-on svg { opacity: 1; }

/* ⚠ A DARK TWIN IS MANDATORY HERE EVEN THOUGH THE COLOURS ARE IDENTICAL.
   The colours come from `--gdx-fill`, which already carries its own scrim by
   night, so nothing about the paint changes. What changes is who wins: the
   dark REST rule is `body.theme-dark .v2x.gdx .gdx-hchip` at (0,5,1) and the
   rule above is (0,4,0), so without this restatement the selected chip goes
   back to being an ordinary dark pill the moment the theme flips — with no
   error and nothing to see in the light preview. */
body.theme-dark .v2x.gdx .gdx-hchip.is-on {
    background-color: #2B6BCC;
    background-image: var(--gdx-fill);
    border-color: transparent;
    color: #fff;
    box-shadow: 0 10px 22px -8px var(--gdx-glow);
}

/* The sweep fires ON SELECTION and nowhere else. The class ARRIVING is what
   starts it — Livewire morphs `is-on` onto the button you just clicked, the
   pseudo's `animation-name` goes from none to this, and the band crosses the
   chip once. (Declared here rather than under `:hover` on purpose: a second
   identical declaration on a hover state never restarts anything, because the
   animation shorthand's value has not changed. One place, one trigger.) */
.v2x.gdx .gdx-hchip.is-on::after { animation: gdx-sweep .85s var(--ease-out); }

@media (hover: hover) {
    /* Already filled, so hover deepens rather than re-fills — the ramp's dark
       end alone, plus the same 2px lift the unselected chips take. */
    .v2x.gdx .gdx-hchip.is-on:hover,
    body.theme-dark .v2x.gdx .gdx-hchip.is-on:hover {
        background-color: #1B4E9C;
        background-image: var(--gdx-fill);
        border-color: transparent;
        color: #fff;
        transform: translateY(-2px);
    }
}

/* ── Nudge arrows ─────────────────────────────────────────────────────────
   Painted ONLY once the shared rail JS measures an overflow. The wrapper's
   `data-scrollable` / `-at-start` / `-at-end` are written by that script; an
   arrow that scrolls nothing is a dead control, so the default is hidden. */
.v2x.gdx .gdx-hchips-nav {
    display: none;
    place-items: center;
    flex: 0 0 auto;
    width: 34px; height: 34px;   /* the homepage arrow's size, not 32 */
    padding: 0;
    border: 1px solid var(--v2-line);
    border-radius: 50%;
    background: #fff;
    color: var(--v2-muted);
    cursor: pointer;
    box-shadow: 0 1px 2px rgba(16, 24, 40, .06);
    transition: background .2s ease, color .2s ease, border-color .2s ease, transform .2s ease;
}
.v2x.gdx .gdx-hchips-nav svg { width: 15px; height: 15px; }
.v2x.gdx .gdx-hchips[data-scrollable="1"] .gdx-hchips-nav { display: inline-grid; }
.v2x.gdx .gdx-hchips[data-at-start="1"] .gdx-hchips-prev,
.v2x.gdx .gdx-hchips[data-at-end="1"]   .gdx-hchips-next { opacity: .3; pointer-events: none; }
@media (hover: hover) {
    .v2x.gdx .gdx-hchips-nav:hover { background: var(--v2-accent-soft); border-color: transparent; color: #2060C0; }
}
.v2x.gdx .gdx-hchips-nav:active { transform: scale(.94); }
body.theme-dark .v2x.gdx .gdx-hchips-nav { background: rgba(44, 44, 49, .92); border-color: var(--dk-line, rgba(255, 255, 255, .10)); color: var(--v2-muted); box-shadow: 0 1px 2px rgba(0, 0, 0, .45); }
body.theme-dark .v2x.gdx .gdx-hchips-nav:hover { background: rgba(54, 128, 237, .16); color: var(--brand-blue-light); }

/* ── Phone ────────────────────────────────────────────────────────────────
   The label takes the full width and centres above the rail, exactly as the
   homepage does at this breakpoint — a nowrap label plus a rail on one line
   leaves the rail about 90px wide on a 390px screen. */
@media (max-width: 720px) {
    /* ⚠ BACK TO `width:100%` HERE. The desktop rule shrink-wraps the track so
       a two-chip module does not sit in an 860px tube; at this breakpoint the
       label takes a centred line of its own, and a `width:100%` child inside a
       `fit-content` parent is a circular measurement. Full width is also the
       right answer on a phone — the row reads as a card, not as a pill. */
    .v2x.gdx .gdx-hchips     { width: 100%; flex-wrap: wrap; border-radius: 18px; padding: 8px; gap: 7px 8px; margin-top: 20px; }
    .v2x.gdx .gdx-hchips-lbl { width: 100%; justify-content: center; padding-inline-start: 0; font-size: 12px; }
    .v2x.gdx .gdx-hchips-ico { width: 21px; height: 21px; border-radius: 7px; }
    .v2x.gdx .gdx-hchips-ico svg { width: 12px; height: 12px; }
    .v2x.gdx .gdx-hchip      { padding: 8px 14px; font-size: 13px; }

    /* ⚠ ARROWS OFF ON TOUCH WIDTHS, and not just for taste.
       The row is `flex-wrap:wrap` here so the label can take its own
       centred line. That lets the two 34px arrows wrap too: measured on
       /guides at 390px, the rail did not shrink enough to seat prev +
       rail + next on one line and the NEXT arrow dropped to a third row
       on its own. A swipe already scrolls the rail on every device this
       breakpoint covers, so the arrows are redundant here rather than
       merely cramped — hiding them removes the wrap AND two 30px tap
       targets that were below the comfortable minimum anyway.
       `!important` because the `[data-scrollable="1"]` rule that reveals
       them is the same specificity and sits earlier in the file. */
    .v2x.gdx .gdx-hchips[data-scrollable="1"] .gdx-hchips-nav { display: none !important; }
}

@media (prefers-reduced-motion: reduce) {
    .v2x.gdx .gdx-hchip, .v2x.gdx .gdx-hchips-nav { transition: none; }

    /* ⚠ THE SELECTED CHIP HAS TO BE NAMED SEPARATELY, and the dark one at its
       own weight. `.gdx-hchip:hover` is (0,4,0); the lift it is meant to undo
       is set at (0,5,0) on `.is-on:hover` and (0,6,1) on the dark twin, so the
       short selector silently misses both and the one chip a reader is most
       likely to be pointing at keeps moving. */
    .v2x.gdx .gdx-hchip:hover,
    .v2x.gdx .gdx-hchip:active,
    .v2x.gdx .gdx-hchip.is-on:hover,
    body.theme-dark .v2x.gdx .gdx-hchip.is-on:hover,
    .v2x.gdx .gdx-hchips-nav:active { transform: none; }

    /* The sweep is decoration on a state change; reduced motion means the
       chip just fills. */
    .v2x.gdx .gdx-hchip.is-on::after { animation: none; }
    /* No `scroll-behavior` reset here any more: the rail never declares
       `smooth` in the first place (see the note above the fade setters),
       so there is nothing to undo. The nudge asks per-call in
       `_dir-scripts`; a CSS reset would not reach an explicit
       `scrollBy({behavior:'smooth'})` argument regardless. */
}


/* ============================================================================
   §TBRESP — the results toolbar on a phone, and the Google badge for all three
   (2026-08-26)
   ----------------------------------------------------------------------------
   Measured on /news, dark, real viewports, BEFORE this block:

     320   [count] [cat] [sort] [views|loadmode] [gps]          262px tall
     360   [count] [cat] [sort] [views|loadmode|gps]            218px tall
     414   [count] [cat] [sort] [views|loadmode|gps]            220px tall
     520   [count] [cat] [sort] [views|loadmode|gps]            220px tall
     560   [count] [cat|sort] [views|loadmode|gps]              168px tall
     600   [count] [cat|sort|views] [loadmode|gps]              168px tall   <-- split
     680   [count] [cat|sort|views] [loadmode|gps]              170px tall   <-- split
     768   [count] [cat|sort|views|loadmode] [gps]              162px tall

   TWO faults, not one:

   1. 360-520 — the last row carries THREE groups (two toggle pairs and the
      Google badge) in 288px of a 314px line. It fits by 26px, which is why it
      never overflowed and never looked right either. Meanwhile the row above
      it holds a 94px count in a full-width line.

   2. 600-680 — `.dir-tb-views` and the §LOADMODE group INJECTED beside it land
      on DIFFERENT rows. Above 520 the base sheet un-hides `.dir-tb-views-lbl`,
      so the two groups go from 80px each to 139 and 173, and the wrap point
      falls between them. Two toggle pairs that read as a matched set, split.

   ⚠ SCOPED TO `.v2x.gdx` — /news, /blog, /guides — NOT to the shared toolbar.
     Fault 2 is in `v2-public-directory-listing.css` + `public-dir-bands.css`
     and therefore reaches all twenty listing verticals. Fixing it there would
     change seventeen pages that were not measured for this pass. The scoping
     is the deliberate limit, not an oversight; the same three rules lifted to
     `.v2x` would fix the rest.
   ========================================================================== */

/* ── §TBRESP-GPS) The badge, moved here from public-feed-listing.css ───────
   It used to be scoped `.v2x.nwx` / `.v2x.blx` and lived in the feed sheet,
   which /guides does not load — so `showGps => true` on the guides hub would
   have rendered a raw 676x213 PNG at full size with no plate and no ring. One
   copy in the shared delta instead, which all three pages already link.

   The file is 676x213 (a 3.17:1 strip), scaled by HEIGHT so it lines up with
   the 52px `.dir-sort-btn` beside it; pinning the width instead makes it a
   different height at every zoom level.

   ⚠ THE PLATE AND THE RING ARE ON THE `img`, NOT ON THE `a`. On a phone the
     link is stretched to a full-width row (see §TBRESP-PHONE) so the badge can
     centre in it, and a ring drawn on the link would then be a 314px rectangle
     around a 108px badge. Painted on the image, the edge follows the artwork
     at every width. At desktop the two are indistinguishable — `line-height:0`
     means the link already hugs the image exactly. */
.v2x.gdx .dir-tb-gps {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;
    line-height: 0;
    text-decoration: none;
}
.v2x.gdx .dir-tb-gps img {
    height: 38px;
    width: auto;
    display: block;
    border-radius: 12px;
    /* The artwork is a pill on a transparent ground; the ring gives it the same
       edge the controls beside it have without recolouring Google's asset. */
    box-shadow: inset 0 0 0 1px rgba(15, 23, 42, .10);
    transition: box-shadow .2s ease, transform .2s ease, filter .2s ease;
}
@media (hover: hover) {
    .v2x.gdx .dir-tb-gps:hover img {
        box-shadow: inset 0 0 0 1px rgba(54, 128, 237, .45), 0 10px 20px -12px rgba(54, 128, 237, .5);
        transform: translateY(-1px);
    }
}
.v2x.gdx .dir-tb-gps:focus-visible { outline: none; }
.v2x.gdx .dir-tb-gps:focus-visible img { box-shadow: 0 0 0 4px rgba(54, 128, 237, .18); }

/* ── §TBGPS-THEME) One badge per theme ─────────────────────────────────────
   Two files, not one recoloured file: the light artwork is a white pill with
   black text, the dark one a transparent pill with #e3e3e3 text and a lighter
   ring. Both carry the same full-colour G — the mark itself is never touched.

   ⚠ `.aits-only-light` / `.aits-only-dark` CANNOT DO THIS JOB HERE. Those
     sitewide helpers in custom.ltr.css are (0,1,1) and (0,2,1); the
     `.v2x.gdx .dir-tb-gps img` rule above is (0,3,1) and sets `display:block`,
     so the generic pair LOSES and both files paint, stacked. The toggle has to
     carry the same depth as the rule it overrides — which is why the class sits
     on the `img` and every ancestor is spelled out.

   ⚠ THE THEME IS A BODY CLASS, NOT THE OS PREFERENCE. A `prefers-color-scheme`
     query — or a <picture> built on one — shows the light badge to a reader who
     chose dark on a light-set machine, which is exactly the case this replaces. */
.v2x.gdx .dir-tb-gps img.dir-tb-gps-d { display: none; }
body.theme-dark .v2x.gdx .dir-tb-gps img.dir-tb-gps-l { display: none; }
body.theme-dark .v2x.gdx .dir-tb-gps img.dir-tb-gps-d { display: block; }

/* Until 2026-08-26 one webp served both themes, so the dark rule here painted a
   near-white PLATE behind it (`background:#F5F7FA`) to stop a white pill from
   floating on the dark canvas with no edge. The dark SVG makes that plate
   actively wrong: its text is #e3e3e3, which on a near-white ground is
   unreadable. The artwork now carries its own #8e918f ring, so the inset ring
   goes too — drawn on a TRANSPARENT image it would trace the img box rather
   than the pill, i.e. a rectangle floating around a rounded badge. */
body.theme-dark .v2x.gdx .dir-tb-gps img {
    box-shadow: none;
}

/* Same reason the hover shadow changes shape: `box-shadow` follows the BOX, and
   with a transparent pill that reads as a glowing rectangle. `drop-shadow()`
   follows the artwork's own alpha, so the lift tracks the pill. */
@media (hover: hover) {
    body.theme-dark .v2x.gdx .dir-tb-gps:hover img {
        box-shadow: none;
        filter: drop-shadow(0 8px 14px rgba(0, 0, 0, .6));
    }
}

/* ⚠ THE DARK FOCUS RING HAS TO BE SPELLED OUT, or there is no focus ring at all
   in dark. `.dir-tb-gps:focus-visible img` is (0,4,1) and the dark img rule
   above is (0,4,2), so the plain focus rule LOSES on the third component and
   the badge takes keyboard focus with nothing to show for it. The link itself
   cannot cover for it either — `.dir-tb-gps:focus-visible { outline: none }`
   drops the UA ring, and no sitewide `a:focus-visible` rule exists to restore
   it. This predates the two-file badge; it is fixed here because the dark rule
   it loses to is the one that just changed.

   Unlike hover, a ring that traces the img BOX is the right shape for focus —
   a focus indicator is meant to read as an obvious rectangle around a control,
   which is also what the light rule draws. */
body.theme-dark .v2x.gdx .dir-tb-gps:focus-visible img {
    box-shadow: 0 0 0 4px rgba(96, 164, 245, .45);
}
@media (prefers-reduced-motion: reduce) {
    .v2x.gdx .dir-tb-gps img { transition: none; }
    .v2x.gdx .dir-tb-gps:hover img { transform: none; }
    body.theme-dark .v2x.gdx .dir-tb-gps:hover img { filter: none; }
}

/* ── §TBRESP-ICONS) Icons only below 769px, so the pair stays a pair ───────
   Fixes fault 2. The base un-hides the toggle captions at 521px, which is the
   width where they start to cost more than they explain: at 600px the two
   groups need 312px of a 526px line that already owes 320px to the category
   and sort pills, so the wrap lands BETWEEN them.

   Raising the threshold to 769 keeps both groups at 80px through the whole
   tablet band, which packs them onto one line together and — measured — pulls
   768px down to a two-row toolbar. Every one of these buttons keeps its
   `title` and its `aria-label`, so nothing is lost but the redundant caption.

   ⚠ `!important` is NOT used and must not be: the base rule is
     `.v2x .dir-tb-views-lbl` (0,2,0) inside `max-width:520px`, and this one is
     (0,3,0), so it simply outranks it — including inside the 520 tier where
     both apply and both say `none`. */
@media (max-width: 768px) {
    .v2x.gdx .dir-tb-views-lbl { display: none; }
}

/* ⚠ THE BUTTON SIZE IS FENCED ABOVE 520px, and getting this wrong cost a
   measurement pass. The base sheet ALREADY shrinks these buttons inside its own
   `max-width:520px` tier — `min-width:36px; padding:0 9px`. Written here as a
   plain `max-width:768px` rule this is (0,3,0) against its (0,2,0), so it
   reaches into the phone tier and makes the buttons BIGGER than the base
   intended: each toggle group went 80px -> 84px, which is exactly the 8px that
   pushed the merged first row of §TBRESP-PHONE over its line at 360px. The
   captions still hide all the way down — only the sizing stops at 521. */
@media (min-width: 521px) and (max-width: 768px) {
    .v2x.gdx .dir-tb-views button { min-width: 38px; padding: 0 10px; }
}

/* ── §TBRESP-PAIR) 521-640px: the pills take the whole line, so the pair can't
   be broken across it ─────────────────────────────────────────────────────
   Hiding the captions was not enough on its own. Measured after §TBRESP-ICONS,
   the line still packed category + sort + Grid|List and then ran out before
   Pages|Infinite:

     560   [count] [cat 180 | sort 198 | views 84] [load 84 | gps 121]   split
     600   [count] [cat 200 | sort 218 | views 84] [load 84 | gps 121]   split
     620   [count] [cat 163 | sort 181 | views 84 | load 84] [gps 121]   fine
     740   [count] [cat 215 | sort 233 | views 84 | load 84] [gps 121]   fine

   `flex-basis: 45%` on the two pills means the pair of them claims ~92% of the
   line, which is more than enough to deny it to an 84px toggle group: either
   BOTH toggles clear the pills onto the next line or neither does. It is the
   basis and not a width, so they still grow into the whole line afterwards —
   the pills are the same size they would have been.

   ⚠ THE BAND HAS A CEILING ON PURPOSE. From 620px up the natural packing
     already keeps the pair together, and at 768 it puts the entire toolbar on
     two rows. Letting this rule run to 768 would force a third row back for no
     reason. 640 is the round number just above the measured 615px edge — free,
     because at 640 both arrangements are three rows anyway. */
@media (min-width: 521px) and (max-width: 640px) {
    .v2x.gdx .dir-sortbox { flex-basis: 45%; }
}

/* ── §TBRESP-PHONE) One control strip, not five stacked pills ──────────────
   Fixes fault 1, and it needs the two halves of the toolbar to become ONE flex
   context to do it: the count lives in `.dir-tb-left` and the toggles in
   `.dir-tb-right`, so no amount of `order` can put them on the same line while
   the two wrappers exist as boxes.

   `display: contents` dissolves the right-hand wrapper into its parent for the
   duration of this tier, which makes count / cat / sort / views / loadmode /
   badge all direct children of `.dir-tb` and orderable against each other. The
   wrapper carries no role and no ARIA, so removing its box removes nothing an
   assistive tool was reading.

   ⚠ `.dir-tb-right`'s OWN declarations stop applying with it — `width:100%`,
     `gap:10px`, `justify-content:space-between` from the 820px tier all go.
     `.dir-tb`'s own `gap` and `justify-content` take over, which is why the
     count needs the auto margin below rather than relying on space-between:
     with three items on line 1, space-between would strand the first toggle
     group in the middle of the row instead of pairing it with the second.

   Resulting order, and what it measures at 390px (314px of line):

     1  count 94  +  views 80  +  loadmode 80   = 274, two 10px gaps  -> fits
     2  category, full width                    -> a 48px tap target
     3  sort, full width                        -> a 48px tap target
     4  the Google badge, centred on its own row

   The two pills stay FULL WIDTH deliberately. Two-up was measured and rejected:
   at 360px each would be 140px, which leaves the value about 65px after the
   icon tile and the chevron — "Newest first" ellipsises to "Newest f…". A
   shorter toolbar is not worth a control you cannot read. The row count is
   unchanged at 4; what changes is that none of them is crowded.

   At 320px line 1 no longer fits (250px of line against 274px of content) and
   the toggles drop to a row of their own. That is the wrap doing its job — no
   media query is needed for it. */
@media (max-width: 520px) {
    .v2x.gdx .dir-tb-right { display: contents; }

    /* Pushes everything after it to the end of line 1. `space-between` cannot
       do this once there are three items on the line. */
    .v2x.gdx .dir-tb-left { order: 1; margin-inline-end: auto; }

    /* Both groups — the second is injected by §LOADMODE and has no class of
       its own to target. Equal `order` keeps them in DOM order, so Grid|List
       stays left of Pages|Infinite. */
    .v2x.gdx .dir-tb-views { order: 2; flex: 0 0 auto; }

    /* ⚠ THE CATEGORY BOX IS ALSO A `.dir-sortbox`, so it needs the higher
       count to sit above the sort box rather than below it. */
    .v2x.gdx .dir-sortbox            { order: 4; flex: 1 1 100%; }
    .v2x.gdx .dir-sortbox.dir-catbox { order: 3; }

    /* Its own row, badge centred. `flex-basis:100%` is what makes it a row —
       the plate is on the image (see §TBRESP-GPS), so the stretched link is
       invisible and only the artwork reads. */
    .v2x.gdx .dir-tb-gps {
        order: 5;
        flex: 1 1 100%;
        justify-content: center;
    }
    .v2x.gdx .dir-tb-gps img { height: 34px; }

    /* ── The category name gets its width back ────────────────────────────
       `.dir-catbox .dir-ts-val` is capped at 108px from 900px down, and the
       reason was sound: the trigger is `white-space:nowrap`, so an arbitrary
       user-supplied name like "Fashion & Beauty Tools" stretched the pill until
       the toggle groups wrapped. That cannot happen here — the pill IS the
       whole line at this tier. Measured on /blog at 390px: a 312px pill with
       its value pinned to 108px, ellipsising a name with 150px of empty pill
       beside it. The overflow rule and `min-width:0` stay, so a genuinely long
       name still ellipsises; it just does it at the real edge. */
    .v2x.gdx .dir-catbox .dir-ts-val { max-width: none; }
}

/* ── §TBRESP-XS) Under 360px the merged row stops fitting ─────────────────
   Line 1 needs count + two 80px groups + two 10px gaps = 280px. A 320px
   viewport leaves 250px of line, so the SECOND toggle group wraps on its own
   and Pages|Infinite ends up stranded under Grid|List — the same split
   §TBRESP-ICONS exists to stop, arriving from the other end.

   Below the threshold the count takes its own row again and the two groups
   wrap together, which is the layout that was there before this pass. Five
   rows on a 320px screen is the honest answer; there is no arrangement of
   these controls that is both comfortable and shorter at that width.

   ⚠ 360, NOT 400. A 4-digit count ("1,234 stories") measures ~135px and would
     orphan the second group at 360 too. These three catalogues are in the tens,
     so the realistic content fits with 10px to spare, and the failure if one
     ever grows is one wrapped row rather than anything broken. Raising the
     threshold to cover the hypothetical would take the merged row away from
     360-400px — iPhone 12/13/14 and most of Android — to protect a count none
     of them has. */
@media (max-width: 359px) {
    .v2x.gdx .dir-tb-left { flex: 1 1 100%; margin-inline-end: 0; }
}
