/* =========================================================
   MSCBSC  gao-a.css  -  shared design-system sheet for 稿A pages
   Path: /assets/gao-a.css
   ---------------------------------------------------------
   Why this file exists
     Every 稿A page today copies its own baseline <style> block
     (11 templates carry an identical ':root{--navy:#0B3D91...}').
     Phase 2 migrates 8 more views from the news-v2 system into
     稿A; copying the baseline again would take the duplication
     from 11 to 19. This sheet is the single source instead.

   Status / blast radius
     NEW FILE. Nothing links to it yet, so it has ZERO effect on
     the live site until a page opts in with:
   (Sections 17/18 add the article-body and article-shell layers
     for batch 5 - the largest-traffic page. Sections 19 through 23
     are documentation only and declare nothing. Still unlinked,
     same zero blast radius. Section 21 is the one to read first:
     it lists what the OUTGOING shell - layout/header.php plus
     layout/footer.php - does that v2top.php does not, i.e. the
     head counter, canonical, the noindex element, the login popup
     and the lazy-load fallback, all of which a migration drops
     without raising a single error.)
       <link rel="stylesheet" href="/assets/gao-a.css?v=<?= h((string) config('asset_ver')) ?>">
     It only ADDS rules; it overrides nothing that any page
     already declares, because a <link> in <head> precedes every
     page's inline <style> block, so inline always wins on ties.
     Behaviour companion: /assets/gao-a.js - also new, also
     unlinked, same zero-impact-before-opt-in property. A page
     that uses .ga-side-tabs needs BOTH files (see section 9).
     READ SECTIONS 19-24 BEFORE PLANNING A MIGRATION: sections 0-1
     are the bare-name rules this sheet shares with the host shell,
     except one - .wrap, which the partial emits itself rather than
     the host (section 1) - and section 19 is the contract for them;
     section 20 is the contract for the hooks a migration must NOT
     rename, including
     two hooks that fail in silence (20.B the article body's id,
     20.C1 the same id as a hardcoded string inside the draft's
     zoom function - 20.B and 20.C1 must be applied together);
     section 21 is the per-page list of what the shell swap itself
     costs; section 22 is the one inline attribute that silently
     decides whether the sidebar tabs work after a class swap;
     section 23 is the breakpoint inventory and the correction to
     reading 21.1's 520px as if it governed every rule - a grid
     collapse does not move to 520 just because the shell does;
     section 24 is the head element, which none of the eight
     pending views owns today and which therefore has to be
     written from scratch - read it before the first migration,
     because the viewport line it covers is what decides whether
     a 520px acceptance screenshot tests anything at all.

   Encoding
     Pages are served as GBK, and a stylesheet with no HTTP
     charset, no @charset and no BOM is decoded using the
     referring document's encoding. Every byte that carries CSS
     meaning below is ASCII; non-ASCII characters appear only
     inside comments, so a mis-decode cannot change how the
     sheet parses. Keep it that way - no non-ASCII in selectors,
     property names, values or content: strings.

   Naming rules (deliberate, see docs/设计-claudecode-稿A组件表-20260918.md)
     - BARE names are used ONLY for the four things that
       partials/v2top.php depends on:
       :root tokens, .wrap, .topbar, .nav (plus html/body/a resets).
       THREE of the four are host-supplied; .wrap is NOT. The
       partial emits its own <div class="wrap">, so a host must
       merely not rename it or redefine it locally. Section 1 below
       has the measurement, and why reading .wrap as host-supplied
       is the one mistake in this set that fails silently.
       v2top.php is a shared file and must keep working unchanged.
     - Everything else is .ga- prefixed, because the bare names
       already mean different things on different 稿A pages
       (.tabs = channel tab buttons on a.php but filter chips on
       bidding/list.php; .main = 585/595 on a.php but 820/360 on
       bidding/list.php). Freezing an ambiguous bare name into a
       shared sheet would be a trap.

   Tokens: identical to views/homev2/a.php and views/bidding/list.php
   ========================================================= */


/* ---------- 0. Tokens + reset (v2top.php depends on these) ---------- */
:root{--navy:#0B3D91;--navy-d:#082C6B;--accent:#E64A19;--ink:#1a1a1a;--gray:#666;--line:#e3e8f0;--bg:#f2f4f8}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:"Microsoft YaHei","PingFang SC",sans-serif;background:var(--bg);color:var(--ink);font-size:15px;line-height:1.6}
a{color:var(--ink);text-decoration:none}
a:hover{color:var(--navy)}
/* The one line in this block that is NEW rather than same-value.
   Measured 2026-09-19: the 11 templates that carry the baseline
   declare the reset, body, a and a:hover as above - section 19 has
   the two recorded exceptions in that set (search/index.php uses a
   1.65 body line-height, flash/detail.php 1.75, this sheet 1.6); a
   12th file, homev2/b.php, carries the reset but is the 稿B draft
   with its own palette, so it is not in the set. But NO 稿A
   template declares an img rule at all - the nearest precedent
   is news-v2.css:9 ".v2 img{border:0;max-width:100%}", which is
   scoped to the v2 body class, and three views cap images locally
   instead (homev2/a.php:57 :141, admin/article_form.php:203). So
   linking this sheet adds an UNCONDITIONAL image cap to a page
   that never had one. On a migrated news-v2 page the delta is nil
   - everything there already sat under ".v2 img". On a 稿A page it
   is real but small: the three page-local caps become redundant
   and any image outside them is capped. Recorded because the
   design doc argues this block's safety from "same values", and
   this line is the exception to that argument.
   Second role of this rule, found on the SOURCE side 2026-09-19 and
   written down because this is the one line that looks like it
   already covers the case: max-width:100% only scales an image that
   LOADED. It does nothing for one that failed. The site's
   failed-image fallback is a separate mechanism and it is attached
   per element - the class img.img-broken is defined exactly once
   (partials/v2top.php line 67) and it only takes effect on an img
   that ALSO carries an inline onerror attribute. Counted repo-wide
   that attribute appears exactly twice: partials/v2top.php line 78
   and homev2/a.php line 257. The eight pending migration views carry
   24 img elements between them and ZERO onerror attributes, so none
   of their images has a fallback - not a degraded one, none. Four of
   the 24 (news/channel.php lines 29, 81, 111, 146) instead fall back
   to one shared placeholder path when the KB supplies no thumbnail,
   which is a data-level answer to the same question. This cannot be
   retrofitted by CSS: onerror is an attribute on the element, not a
   rule, so it has to be decided while the img markup is being
   written, not during a 2.4 screenshot pass. */
img{border:0;max-width:100%}


/* ---------- 1. Shell: wrap / topbar / nav ---------- */
/* Required by partials/v2top.php: it styles .v2util/.v2brand/.v2search
   and overrides ".topbar .wrap" / ".topbar .nav a" at <=520px, but it
   never defines .wrap / .topbar / .nav itself. The three are NOT the
   same kind of thing, measured 2026-09-19:
     .wrap     the partial emits <div class="wrap"> itself (v2top.php
               :69, :81, :89) and only ever uses it as a descendant,
               so a host does NOT provide it - it must merely not
               rename or locally redefine it. All 11 稿A pages also
               declare .wrap{width:1200px;margin:0 auto} locally.
     .topbar   genuinely host-supplied. The partial names it only
     .nav      inside the <=520px block, so desktop geometry is 100%
               host-supplied - this is why a 稿A page cannot be
               markup-only, and why a page that omits them renders a
               correct-on-phones, bare-links-on-desktop header.
   The distinction matters because reading .wrap as host-supplied
   invites renaming the partial's own container, which silently drops
   the 1200px header width sitewide. */
.wrap{width:1200px;margin:0 auto}
.topbar{background:linear-gradient(180deg,var(--navy) 0%,var(--navy-d) 100%);color:#fff}
.topbar .wrap{display:flex;align-items:center;height:64px}
.nav{display:flex;gap:2px;flex:1}
.nav a{color:rgba(255,255,255,.88);padding:0 13px;height:64px;line-height:64px;font-size:15px;white-space:nowrap}
.nav a:hover,.nav a.on{background:rgba(255,255,255,.12);color:#fff}


/* ---------- 2. Breadcrumb band ---------- */
/* Replaces news-v2 .v2-crumbs (white box) with the 稿A band.
   min-height + wrap instead of a fixed 42px height so the long
   guidearr chains on category pages cannot overflow. */
.ga-crumbs{background:#fff;border-bottom:1px solid var(--line)}
.ga-crumbs .wrap{min-height:42px;display:flex;align-items:center;flex-wrap:wrap;padding-top:6px;padding-bottom:6px;font-size:13px;color:#999}
.ga-crumbs a{color:#777}
.ga-crumbs a:hover{color:var(--navy)}
.ga-crumbs .sep{margin:0 6px;color:#ccc}
.ga-crumbs b{color:#555;font-weight:400}


/* ---------- 3. Content + sidebar layout ---------- */
/* 820 + 20 + 360 = 1200, matching views/bidding/list.php .main.
   NOT named .main: a.php already uses .main for 585/595. */
.ga-layout{display:grid;grid-template-columns:minmax(0,820px) 360px;gap:20px;margin-top:16px;align-items:start}
.ga-layout > *{min-width:0}
.ga-side{min-width:0}


/* ---------- 4. Card ---------- */
.ga-card{background:#fff;border-radius:4px;box-shadow:0 1px 3px rgba(11,61,145,.06);margin-bottom:20px;min-width:0}
.ga-card-h{display:flex;align-items:baseline;padding:13px 16px 9px;border-bottom:2px solid var(--navy)}
.ga-card-h b,.ga-card-h h2,.ga-card-h h3,.ga-card-h strong{font-size:16px;font-weight:700;color:var(--navy);margin-right:auto}
.ga-card-h :first-child::before{content:"";display:inline-block;width:4px;height:14px;background:var(--accent);margin-right:8px;vertical-align:-2px;border-radius:2px}
.ga-card-h a{font-size:12px;color:#999}
.ga-card-h a:hover{color:var(--accent)}
.ga-card-bd{padding:8px 16px 12px;min-width:0}
/* Anti-double-padding guard. news-v2 nests the list INSIDE a padding
   wrapper (.v2-card-bd > ul.v2-list), while 稿A declares the padding on
   the list itself (.card-h + ul.flink, no wrapper - see bidding/list.php).
   A literal class swap would therefore give every list 16+16=32px side
   padding and stack the wrapper's vertical padding on the list's own.
   The guard keeps the mechanical swap honest: a list that is a direct
   child of .ga-card-bd contributes no padding of its own and lets the
   wrapper own it.
   CORRECTION, measured 2026-09-19: the sentence that used to stand here
   said the guarded result "matches today's news-v2 geometry". Only half
   of that is true, and both halves are two lines of news-v2.css apart:
     side    .v2-card-bd 16px, this sheet .ga-card-bd 16px -> preserved
     top     news-v2  .v2-card-bd padding-top 14 + .v2-list li
             padding-top 8                    = 22px to the first row
             this sheet .ga-card-bd 8 + .ga-list li 8 = 16px,
             and with .ga-rank li 7                   = 15px
   So the side inset is kept exactly and the vertical one is NOT: a
   migrated card's first row sits 6px higher (7px on a ranked list) and
   its last row 2-3px nearer the bottom, because .ga-card-bd ships
   8px/12px where .v2-card-bd ships 14px/14px. That is the intended
   direction - the guarded result is the 稿A card, which carries the
   whole inset on the list itself in one place (bidding/list.php .flink,
   a.php .list / .rank) - but it is a visible shift on every migrated
   card, so do not read this guard as "news-v2 geometry preserved", and
   do not go hunting for a missing 6px during 2.4.
   Specificity (0,2,0) beats the <=520px .ga-list/.ga-rank rule (0,1,0)
   below, so both widths stay consistent. Do NOT drop this. */
.ga-card-bd > .ga-list,.ga-card-bd > .ga-rank{padding:0}


/* ---------- 5. Plain text list (dot-free, dashed separators) ---------- */
.ga-list{list-style:none;padding:6px 16px 12px;min-width:0}
.ga-list li{display:flex;justify-content:space-between;align-items:baseline;padding:8px 0;border-bottom:1px dashed #eee;font-size:14px;min-width:0}
.ga-list li:last-child{border-bottom:0}
.ga-list a{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.ga-list .d,.ga-list cite{flex:0 0 auto;margin-left:10px;color:#bbb;font-size:12px;font-style:normal;white-space:nowrap}
/* Two rows of news-v2.css are deliberately NOT carried here. Both were
   measured on 2026-09-19 and neither was recorded before, so they are
   written down rather than left to be rediscovered in a 2.4 screenshot:
     the 6px bullet   .v2-list li::before (news-v2.css:78). Absent on
                      purpose - 稿A lists are dot-free (a.php .list).
     the date         news-v2.css:339 hides the date below 768px:
                        .v2-list li .v2-date, .v2-list li cite
                        { display:none; }
                      There is no such rule here, so a migrated row
                      keeps its <cite> at 768, 520 and 390. It costs
                      title width, because .ga-list li is a flex row
                      and the date is nowrap; the title keeps its
                      ellipsis, so nothing overflows and nothing errors.
                      Affected rows, all of them <cite> inside
                      <ul class="v2-list">: news/category.php:49 and
                      news/channel.php:67, :127, :157 - i.e. 2 of the 8
                      pending views. Keeping the date is what the 稿A
                      homepage does today (a.php .list hides nothing
                      either), so this is not obviously wrong and the
                      choice is escalated rather than assumed - Q19.
                      (Q19 and not Q17: 17 and 18 were taken by the 4.2
                      m-site audit of 2026-09-19. Numbering here follows
                      the design doc's running list.)
                      Do not add the hide rule here on your own: it
                      would make /news and /category-* disagree with
                      the homepage at the same width. */


/* ---------- 6. Ranked list (numbered) ---------- */
/* Standalone: use class="ga-rank", do NOT stack it with .ga-list.
   Keeps the news-v2 markup contract of an empty <em></em> per row
   and supplies the number via a CSS counter, so migration stays a
   pure class swap with no per-row renumbering in the view. */
.ga-rank{list-style:none;padding:8px 16px 12px;min-width:0;counter-reset:garank}
.ga-rank li{display:flex;align-items:baseline;padding:7px 0;border-bottom:1px dashed #eee;font-size:14px;min-width:0}
.ga-rank li:last-child{border-bottom:0}
.ga-rank em{counter-increment:garank;flex:0 0 22px;font-style:italic;font-weight:700;color:#bbb}
.ga-rank em::before{content:counter(garank)}
.ga-rank li:nth-child(-n+3) em{color:var(--accent)}
.ga-rank a{flex:1 1 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}


/* ---------- 7. Media row (image + title + summary + meta) ---------- */
/* Migration target for news-v2 .v2-media / .v2-media-img /
   .v2-media-body / .v2-media-meta. Thumb 180x120 desktop,
   120x84 <=768px, 104x72 <=520px; radius follows the 稿A 4px
   baseline rather than news-v2's 6px. */
.ga-media{display:flex;gap:14px;padding:14px 0;border-bottom:1px solid var(--line)}
.ga-media:last-child{border-bottom:0}
.ga-media-img{flex:0 0 180px;width:180px;height:120px;overflow:hidden;border-radius:4px;background:#eef1f4;display:block}
.ga-media-img img{width:100%;height:100%;object-fit:cover;display:block}
.ga-media-body{flex:1 1 auto;min-width:0}
.ga-media-body h4{font-size:17px;font-weight:700;line-height:1.45;margin-bottom:6px}
.ga-media-body h4 a{color:#16325f}
.ga-media-body h4 a:hover{color:var(--navy)}
.ga-media-body p{color:#6b7885;font-size:13px;line-height:1.6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.ga-media-meta{margin-top:8px;font-size:12px;color:#9aa7b3;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.ga-media-meta .more{margin-left:auto;color:var(--accent)}

/* Small category/tag chip, migration target for .v2-tag inside
   .v2-media-meta and .v2-article-tags. */
.ga-tag{display:inline-block;background:#eef4fb;color:var(--navy);border-radius:3px;padding:1px 8px;font-size:12px}
.ga-tag:hover{background:var(--accent);color:#fff}


/* ---------- 8. Tag cloud ---------- */
/* Migration target for news-v2 .v2-tags-cloud. */
.ga-tags{padding:4px 0 0}
.ga-tags a{display:inline-block;background:#eef4fb;color:var(--navy);border-radius:4px;padding:3px 12px;margin:0 8px 10px 0;font-size:13px}
.ga-tags a:hover{background:var(--accent);color:#fff}


/* ---------- 9. Sidebar: tabbed list ---------- */
/* Migration target for news-v2 .v2-side-tabs + its two .v2-tab-body
   <ul>s. The active tab uses .on (稿A convention) instead of
   .active, and panes hide purely in CSS, so the first pane must
   carry class="ga-tab-body on" and the rest nothing. That INITIAL
   state needs no JS, and a page whose JS never runs still shows
   the first pane. SWITCHING does need a script: news-v2's switcher
   is news-v2.js, which needs jQuery and is not loaded on 稿A
   pages, so the batch 3/4 companion /assets/gao-a.js is
   mandatory here - without it the tabs render but cannot be
   switched. See its header comment and design doc sections
   2.9 / 6-Q3.
   The existing panes also carry style="display:none;" in their
   markup, which outranks this rule and silently breaks the switch.
   READ SECTION 22 before swapping pane classes. */
.ga-side-tabs{display:flex;border-bottom:1px solid var(--line)}
.ga-side-tabs span{flex:1;text-align:center;padding:11px 0 9px;cursor:pointer;font-size:15px;color:#666}
.ga-side-tabs span.on{color:var(--accent);font-weight:700;border-bottom:2px solid var(--accent);margin-bottom:-1px}
.ga-tab-body{display:none}
.ga-tab-body.on{display:block}


/* ---------- 10. Sidebar: picture card + ad slot ---------- */
.ga-side-pic{position:relative;border-radius:4px;overflow:hidden;margin-bottom:10px}
.ga-side-pic img{width:100%;height:170px;object-fit:cover;display:block;background:#eef1f4}
.ga-side-pic h4{position:absolute;left:0;right:0;bottom:0;margin:0;padding:20px 10px 8px;font-size:14px;line-height:1.5;background:linear-gradient(transparent,rgba(0,0,0,.72))}
.ga-side-pic h4 a{color:#fff}
.ga-ad{text-align:center;margin-bottom:20px}
.ga-ad img{border-radius:4px;display:inline-block}


/* ---------- 11. Pager ---------- */
/* Styles BOTH pagination markups in one place:
   (a) CateController::multi() raw output - <div class="p_bar"> with
       a.p_total / a.p_pages / a.p_redirect / a.p_num / a.p_curpage
       and input.p_input. Wrap it as:
         <div class="ga-pager"><?= $multipage ?></div>
       NOTE: .p_bar currently has NO styling anywhere in newsite/ -
       new/css/mscbsc.css owns it on the legacy site only - so
       /category-*.html pagination renders unstyled today. This rule
       set fixes that with zero controller changes.
   (b) hand-built pagers using a / strong / span.cur / span.dis. */
.ga-pager{text-align:center;padding:16px 0 4px;font-size:13px}
.ga-pager a,.ga-pager span,.ga-pager strong{display:inline-block;padding:4px 12px;margin:0 3px;border:1px solid var(--line);border-radius:3px;background:#fff;color:#666;font-weight:400}
.ga-pager a:hover{border-color:var(--navy);color:var(--navy)}
.ga-pager strong,.ga-pager .cur,.ga-pager .p_curpage{background:var(--navy);border-color:var(--navy);color:#fff}
.ga-pager .dis{color:#ccc}
.ga-pager a.p_total,.ga-pager a.p_pages{border:0;background:none;color:#999;padding:4px 2px}
.ga-pager a.p_pages{padding:0}
.ga-pager .p_input{width:44px;height:26px;border:1px solid var(--line);border-radius:3px;text-align:center;font-size:13px;vertical-align:middle;background:#fff}


/* ---------- 12. Empty state ---------- */
/* Unified slot for finding F8 (three different empty-state classes
   and three different wordings across the pending views). */
.ga-empty{padding:40px 0;text-align:center;color:#8ea3c8;font-size:14px}


/* ---------- 13. Channel heading block ---------- */
.ga-channel-head h1{font-size:24px;line-height:1.35;color:var(--navy)}
.ga-channel-head p{margin-top:8px;color:#6b7885;font-size:14px;line-height:1.8}


/* ---------- 14. Single-column promo tiles + button + 2-up grid ---------- */
.ga-promo{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:14px 16px 16px}
.ga-promo a{display:block;background:linear-gradient(135deg,#f4f7fd,#eaf0fb);border:1px solid var(--line);border-radius:6px;padding:13px 10px;text-align:center}
.ga-promo a:hover{border-color:var(--navy)}
.ga-promo b{display:block;color:var(--navy);font-size:14px;margin-bottom:2px}
.ga-promo span{font-size:11px;color:#888}
.ga-btn{display:inline-block;background:var(--accent);color:#fff;border-radius:4px;padding:7px 16px;font-size:14px}
.ga-btn:hover{background:#c63d12;color:#fff}
.ga-grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin-top:8px}


/* ---------- 15. Footer ---------- */
/* Current 稿A footer wording/scope. Site-wide compliance items
   (ICP filing number, sitemap, Matomo) do NOT exist on 稿A pages
   today - see finding F4 in the phase-2 plan; adding them here is
   a pending H123 decision, not part of this batch. */
.ga-footer{margin-top:24px;background:var(--navy-d);color:rgba(255,255,255,.6);text-align:center;padding:22px;font-size:12px}


/* ---------- 16. Responsive ---------- */
@media(max-width:1200px){
  .wrap{width:auto;padding:0 10px}
  .nav{overflow-x:auto}
  .ga-layout{grid-template-columns:minmax(0,1fr)}
  .ga-layout > *{min-width:0}
}
@media(max-width:768px){
  .ga-media{gap:12px}
  .ga-media-img{flex:0 0 120px;width:120px;height:84px}
  .ga-media-body h4{font-size:15px}
  .ga-grid2{grid-template-columns:minmax(0,1fr)}
  .ga-crumbs .wrap{font-size:12px}
}
@media(max-width:520px){
  .ga-layout{margin-top:10px;gap:12px}
  .ga-card{margin-bottom:12px}
  .ga-card-h{padding:12px 14px 8px}
  .ga-card-bd,.ga-list,.ga-rank{padding-left:14px;padding-right:14px}
  .ga-media{gap:10px;padding:11px 0}
  .ga-media-img{flex:0 0 104px;width:104px;height:72px}
  .ga-media-body h4{font-size:15px;margin-bottom:4px}
  .ga-media-body p{font-size:12px}
  .ga-side-pic img{height:150px}
  .ga-tags a{margin:0 6px 8px 0;padding:2px 10px}
  .ga-side-tabs span{font-size:14px;padding:10px 0 8px}
  .ga-pager{padding:12px 0 2px}
  .ga-pager a,.ga-pager span,.ga-pager strong{padding:3px 9px;margin:0 2px}
  .ga-promo{gap:8px;padding:12px 14px 14px}
  .ga-empty{padding:28px 0;font-size:13px}
  .ga-footer{margin-top:16px}
}


/* =========================================================
   17. Article body (batch 5 - news/view.php migration)
   =========================================================
   Why this section exists
     Batch 5 is the article-detail page: /info/Nw... is the
     single largest traffic entry on the site and the only
     remaining 稿A page that still renders through news-v2.
     view_v2.php (the 稿A draft, served noindex on /v2/info/..)
     already carries a .art-body block, but it holds only the
     generic typesetting. The early-brief layer it depends on -
     the mn-* family for auto-published 早报速递 articles - lives
     EXCLUSIVELY in news-v2.css section 10.5 today. Confirmed by
     grep: no 稿A template mentions any mn-* class, and
     news-v2.css is the only file in the repo that styles them.
     A view-layer-only migration would therefore silently strip
     the brief styling off every briefing article, so the layer
     is ported here first. Same method as batches 1-4: land the
     shared sheet, migrate the view afterwards.

   Relationship to art-body (the name a real 稿A page uses today)
     .art-body is what view_v2.php actually declares, and it is
     currently used by exactly ONE view - the v2 draft. The 15
     rules that touch it there (view_v2.php lines 68-82) are NOT
     frozen here: that page is still an unshipped draft and H123
     has not accepted it, so promoting its values into a shared
     sheet would freeze a draft. .ga-body is the name to migrate
     onto; the values below ARE the reviewed ones - they are
     view_v2.php's art-body plus the part art-body never had.
     Do not read this as "ga replaces art"; read it as "the 稿A
     detail body finally has a definition".
     The CLASS name is this section's business; the element's ID is
     not. Keep id="articlebody" on the .ga-body element (section
     20.B) - but for the reason given there, not for "renaming it
     deletes a behaviour": no migrated behaviour reads the id. The
     class is what articleFixes() reads, and it reads it as a
     DIRECT CHILD ('>'), so the tables must stay direct children of
     the .ga-body element. The earlier wording of this paragraph
     said the id rename "deletes a behaviour silently" and pointed
     only at 20.B; both halves were wrong, see section 22.

   The port is 1:1, not a redesign
     news-v2.css section 10.5 is the product of three rounds of
     claudecode repairs made 2026-08-19..2026-08-21 and reviewed
     line by line in docs/复核-claudecode-早报速递mn样式-20260819.md
     (31 rules verified against the live server). Section 17.1
     below copies it with two deliberate edits, both noted inline:
     the feature colour moves from news-v2 blue #0d5cab to the
     稿A navy #0B3D91, and light rules get the 稿A 4px baseline
     radius. Nothing else is rewritten. Colour mapping, for the
     record: #0d5cab/#1a3a6a -> var(--navy); #0d2c4f -> var(--navy-d);
     #e64a19 -> var(--accent); #2b333b/#444 -> var(--ink).

   Scope and blast radius
     Still ZERO until a page opts in - nothing links this sheet.
     Everything is scoped under .ga-body, so it cannot reach any
     other block even after batches 1-4 go live.

   Encoding: ASCII outside comments, per the file header - NOT
   "comments included". The header states the real contract at
   line 56; this line said the opposite from its first commit
   (44f527d) until 2026-09-22, while the file carried CJK
   comments the whole time. The bound is what matters: every
   byte carrying CSS meaning is ASCII, so a GBK mis-decode
   cannot change how this sheet parses.
   ========================================================= */

/* ---------- 17.0 Generic article typesetting ---------- */
.ga-body{margin-top:22px;font-size:17px;line-height:1.9;color:#26282c;word-wrap:break-word}
.ga-body p{margin:0 0 1.35em;text-align:justify}
.ga-body img{max-width:100%;height:auto;display:block;margin:14px auto;border-radius:4px}
.ga-body h2,.ga-body h3{font-size:20px;color:var(--navy-d);margin:1.6em 0 .8em;padding-left:12px;border-left:4px solid var(--accent);line-height:1.4}
.ga-body h3{font-size:18px;border-left-color:var(--navy)}
.ga-body h4,.ga-body h5,.ga-body h6{font-size:16px;margin:1.4em 0 .7em;color:#222}
.ga-body blockquote{margin:1.2em 0;padding:12px 18px;background:#f7f8fa;border-left:4px solid #c8d4e8;color:#556;font-size:15px;border-radius:0 4px 4px 0}
.ga-body ul,.ga-body ol{margin:0 0 1.35em 1.6em}
.ga-body li{margin-bottom:.45em}
.ga-body table{border-collapse:collapse;width:100%;margin:1.2em 0;font-size:14px}
.ga-body td,.ga-body th{border:1px solid #dde3ec;padding:8px 10px}
.ga-body th{background:#f0f4fb}
.ga-body a{color:var(--navy);text-decoration:underline}
.ga-body hr{border:0;border-top:1px solid var(--line);margin:1.6em 0}
.ga-body pre{background:#f6f8fa;border:1px solid var(--line);border-radius:4px;padding:14px;overflow-x:auto;font-size:13px;line-height:1.7}
.ga-body video,.ga-body .video-js{max-width:100%}


/* ---------- 17.1 Early-brief layer (mn-*) ---------- */
/* Ported 1:1 from news-v2.css section 10.5. Keep these scoped
   under .ga-body: the mn-* names are emitted by the publishing
   service into article HTML, so they arrive unbidden on every
   auto-published briefing. */
.ga-body .mn-dt{text-align:center;color:#8a97a5;font-size:14px;margin:0 0 18px 0;line-height:1.6}
.ga-body .mn-sum{background:#f0f4f8;border-left:4px solid var(--navy);padding:14px 18px;margin:0 0 26px 0;font-size:16px;line-height:1.85;color:var(--ink);word-break:break-word;border-radius:0 4px 4px 0}
.ga-body .mn-sum strong{color:var(--navy);font-weight:bold}
.ga-body .mn-k{color:#c0392b;font-weight:bold}
.ga-body .mn-sec{margin:30px 0 0 0}
.ga-body .mn-st{font-size:20px;font-weight:bold;color:var(--navy-d);margin:0 0 16px 0;padding:2px 0 2px 12px;border-left:4px solid var(--accent);line-height:1.4}
.ga-body .mn-art{margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid var(--line)}
.ga-body .mn-art:last-child{border-bottom:none;padding-bottom:0;margin-bottom:0}
.ga-body .mn-at{font-size:16px;font-weight:bold;color:var(--navy-d);margin:0 0 10px 0;line-height:1.5}
.ga-body .mn-ab{font-size:16px;color:var(--ink);line-height:1.85}
/* 16px, not 10px: the port first landed at 10px, which made the brief
   body 41 percent tighter than the site's own 1em rhythm. See the
   2026-08-20 progress entry. Do not "tidy" this back down. */
.ga-body .mn-ab p{margin:0 0 16px 0;font-size:16px;line-height:1.85;text-indent:0}
.ga-body .mn-pts{background:#f8fafc;border-left:3px solid var(--navy);padding:12px 16px;margin:12px 0 0 0;border-radius:0 4px 4px 0}
/* 16px against a 15px li: the original port had 15px vs 15px, which
   collapsed the lead-in to the same level as the list items it
   introduces (2026-08-21 progress entry). */
.ga-body .mn-pl{font-size:16px;color:var(--navy-d);font-weight:bold;margin:0 0 10px 0;line-height:1.5}
.ga-body .mn-pts ul,.ga-body .mn-pts ol{margin:0;padding:0}
.ga-body .mn-pts li{list-style:none;font-size:15px;color:var(--ink);line-height:1.85;margin:6px 0;padding-left:18px;text-indent:-18px}
/* 5px dot + 13px gap = the 18px hanging indent exactly. At 10px the
   first line started 3px left of the wrapped lines (2026-08-20). */
.ga-body .mn-pts li::before{content:"";display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--navy);margin-right:13px;vertical-align:middle;text-indent:0}
.ga-body .mn-ft{border-top:1px solid var(--line);padding-top:16px;font-size:12px;color:#9aa7b3;text-align:center;margin-top:36px;line-height:1.8}
/* Above, text-indent:0 on .mn-ab p is a restatement, not a repair:
   稿A body paragraphs are justified without a first-line indent, so
   the value is already 0. It is written out so that adding an indent
   to .ga-body p later cannot silently indent every brief paragraph. */


/* ---------- 17.2 Article-body responsive ---------- */
/* Only the mn-* sizes are responsive here, matching news-v2 exactly.
   NOTE the deliberate omission: news-v2 also drops
   .v2-article-body from 17px to 16px at this breakpoint, and the 稿A
   draft does not - its .art-body stays 17px on phones. Adding that
   drop is a visible change to the largest-traffic page and is left
   as an open question for H123, not smuggled in with this port.
   Scope of that omission, counted 2026-09-19 so it is not reread as
   wider than it is: news-v2's <=768px block changes exactly ten mn-*
   rules - eleven lines, news-v2.css:326-336, one of them carrying two
   selectors - and ALL TEN are carried below, value for value. The body
   font size is the only thing left out. Do not read
   "deliberate omission" as "the whole 768 block was skipped" - adding
   a second copy of the ten rows below is the failure mode that
   reading invites. */
@media(max-width:768px){
  .ga-body .mn-dt{font-size:13px;margin-bottom:14px}
  .ga-body .mn-sum{padding:12px 14px;font-size:15px;line-height:1.8}
  .ga-body .mn-sec{margin-top:22px}
  .ga-body .mn-st{font-size:18px;margin-bottom:12px}
  .ga-body .mn-art{margin-bottom:18px;padding-bottom:14px}
  .ga-body .mn-at{font-size:15px}
  .ga-body .mn-ab,.ga-body .mn-ab p{font-size:15px;line-height:1.8}
  .ga-body .mn-pts{padding:10px 14px}
  .ga-body .mn-pl{font-size:16px}
  .ga-body .mn-pts li{font-size:14px;line-height:1.8}
}


/* =========================================================
   18. Article shells (batch 5 - news/view.php migration)
   =========================================================
   Why this section exists
     Section 17 landed the article BODY. The body alone does
     not make a detail page: view.php surrounds it with a
     column of blocks that have no counterpart anywhere in
     sections 0-17 - the category chip, the meta strip and its
     font-zoom buttons, the lede, the wire-copy strip, the
     share/copyright/prev-next box, the related-reading card,
     the related-topic capsule row, the WeChat-kit card, the
     article-level ad slot, and the sidebar's stacked picture
     card. Every one of those still runs through news-v2
     classes, so a body-only migration would restyle the prose
     and leave the chrome around it in the old skin.

   Source of the values: the 稿A article draft, NOT news-v2
     This is the one batch where a 稿A implementation already
     exists. views/news/view_v2.php is an accepted-shape draft
     of exactly this page - it already carries the 稿A tokens,
     v2top.php, .wrap/.topbar/.nav and the 27px title. So the
     values below are ported from that draft (its lines
     52-137), not from news-v2.css: news-v2 sizes the title
     at 26px on an 8px-radius card, the draft at 27px on the
     4px 稿A radius, and batch 5 is a migration TO 稿A.
     Consequence worth stating plainly: porting the draft
     freezes a noindex draft's numbers into the shared sheet.
     Section 17 refused to do that for .art-body, and the
     distinction is deliberate - .art-body had a live,
     reviewed alternative in news-v2.css section 10.5 (the
     early-brief layer), so the draft was the wrong source
     there. These seven shells have no such alternative:
     news-v2 has no share box, no WeChat card, no category
     chip and no lede, so the draft is the ONLY 稿A
     specification in the repo. The alternative to porting it
     is inventing values, which is worse. If H123 rejects the
     draft's look, this is the section to revise - and
     revising it is cheap while nothing links the sheet.

   What was intentionally NOT copied from the draft
     - .art-body -> .ga-body: already handled by section 17,
       which owns that rename. Do not re-add an .art-body rule.
     - The draft's #hottabs id (view_v2.php:229) is NOT
       promoted. That is a page-local id for its inline tab
       script; the shared sheet styles the class only. A
       migrated page may keep or drop the id freely.
     - No values were "harmonised" against news-v2. Where the
       draft differs from news-v2 (font sizes, radii, colours),
       these rules take the draft.

   Scope and blast radius
     Still ZERO until a page opts in - nothing links this
     sheet. Every selector is a new .ga-* name; none of them
     is declared anywhere else in the repo, and no existing
     selector is redeclared. Same zero-impact-before-opt-in
     property as sections 1-17.

   Encoding: ASCII outside comments, per the file header -
   the two CJK strings the draft renders as literal text (the
   lede label U+5BFC U+8BFB, and the middle dot U+00B7 in
   .ga-rel li) are written as CSS escapes for that reason.
   Both are inside content: strings, which are exactly the
   place a GBK mis-decode would do real damage.
   ========================================================= */

/* ---------- 18.1 Detail-page wrapper ----------
   .ga-layout is a two-column grid; the article column also
   stacks the related-reading and WeChat cards BELOW the
   article card, which is why those panels are 20px apart
   from it rather than flush. */
.ga-col{margin-bottom:20px}

/* ---------- 18.2 Article card ---------- */
.ga-article{background:#fff;border-radius:4px;box-shadow:0 1px 3px rgba(11,61,145,.06);padding:34px 40px 28px}
.ga-article h1{font-size:27px;line-height:1.45;font-weight:700;color:#111;margin-bottom:16px}
/* Category chip above the title, from the breadcrumb's second
   level. Distinct from .ga-tag: this one is a bordered chip
   with square-ish corners, .ga-tag is the inline meta chip. */
.ga-cate{display:inline-block;background:#eef3fc;color:var(--navy);font-size:12px;padding:2px 10px;border-radius:2px;border:1px solid #d7e2f5;margin-bottom:14px}
/* Meta strip. .ga-meta .ga-zoom IS declared: the batch-5
   migration target emits a font-zoom control here
   (view.php:27, the draft's is at view_v2.php:166), and
   without this rule its three buttons render as bare links
   with no box. The zoom class itself is section 4's; only
   this placement rule is new. */
.ga-meta{display:flex;flex-wrap:wrap;align-items:center;gap:0 22px;font-size:13px;color:#999;padding-bottom:16px;border-bottom:1px solid var(--line)}
.ga-meta b{color:#666;font-weight:400}
.ga-meta a{color:var(--navy)}
.ga-meta .ga-zoom{margin-left:auto}
.ga-meta .ga-zoom a{display:inline-block;border:1px solid var(--line);border-radius:3px;padding:0 8px;margin-left:5px;font-size:12px;color:#888;line-height:20px}
.ga-meta .ga-zoom a:hover{border-color:var(--navy);color:var(--navy)}
/* Lede: the meta description echoed back above the body. The
   label is drawn in CSS, so the view emits no wrapper text. */
.ga-lede{margin:20px 0 4px;padding:14px 18px;background:#f6f9ff;border-left:4px solid var(--navy);border-radius:0 4px 4px 0;font-size:14px;line-height:1.85;color:#445}
.ga-lede::before{content:"\5bfc \8bfb ";display:inline-block;background:var(--navy);color:#fff;font-size:12px;padding:0 7px;border-radius:2px;margin-right:10px;vertical-align:1px}
/* Flash-body strip: the wire-copy source text under a brief article. */
.ga-kx{margin:20px 0 4px;padding:12px 16px;background:#fff7f3;border:1px solid #ffd9c7;border-radius:4px;font-size:13px;color:#a04a25}
.ga-kx b{color:var(--accent)}

/* ---------- 18.3 Share + copyright + prev/next ----------
   Two deliberate deviations from the accepted draft, so the
   next reader does not read them as drift:

   (a) .ga-note is declared here rather than reused as a bare
       "note" class. The draft declares .art-foot .note because
       its own sheet owns that name; in this shared sheet a
       bare .note would be a new global name, which the naming
       rules in the file header forbid.
   (b) The QR box is sized 96px, matching the draft's width
       ATTRIBUTE (view_v2.php:188). news-v2 uses 120px flush
       left with no border; the draft shrinks it and adds a
       hairline. Draft wins.

   min-width:0 is required, not cosmetic: it lets the note
   column shrink inside the flex row instead of being forced
   wide by .ga-pn's nowrap headlines. The draft omits it,
   which is harmless there only because its QR is small. */
.ga-share{margin-top:30px;padding-top:18px;border-top:1px dashed var(--line);display:flex;gap:20px;align-items:flex-start}
.ga-share img{flex:0 0 96px;border:1px solid var(--line);border-radius:4px;padding:4px}
.ga-share .ga-note{font-size:12px;color:#999;line-height:1.8;flex:1;min-width:0}
.ga-pn{margin-top:8px;font-size:13px}
.ga-pn div{padding:4px 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ga-pn span{color:#999;margin-right:6px}
/* Article-level ad slot. Base margin only - news-v2's bare
   ".v2-ad img{max-width:100%}" is matched by the 稿A reset's
   "img{max-width:100%}" already, so it is not restated. */
.ga-article-ad{margin-top:20px}

/* ---------- 18.4 Related block + WeChat kit ----------
   .ga-media and .ga-tags are NOT reused here on purpose.
   news-v2 uses one .v2-rel-tags capsule for both tag modes,
   but the accepted draft splits them: this block is a solid
   navy capsule set, the other one (section 7 .ga-tag) is a
   tinted chip inside a card. Collapsing them into .ga-tags
   would restyle the block the draft specced. */
.ga-rel{margin-top:22px;background:#fff;border-radius:4px;box-shadow:0 1px 3px rgba(11,61,145,.06)}
.ga-rel-h{display:flex;align-items:baseline;padding:13px 20px 9px;border-bottom:2px solid var(--navy)}
.ga-rel-h b{font-size:16px;color:var(--navy)}
.ga-rel-h b::before{content:"";display:inline-block;width:4px;height:14px;background:var(--accent);margin-right:8px;vertical-align:-2px;border-radius:2px}
.ga-rel ul{list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:0 30px;padding:14px 20px 16px}
.ga-rel li{padding:7px 0;border-bottom:1px dashed #eee;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ga-rel li::before{content:"\b7 ";color:var(--accent);font-weight:700;margin-right:6px}
/* Solid capsule set, one per related-topic tag. */
.ga-rel-tags{margin-top:22px;display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.ga-rel-tags .lb{font-size:13px;color:#999}
.ga-rel-tags a{background:#eef3fc;color:var(--navy);font-size:13px;padding:4px 14px;border-radius:14px;border:1px solid #d7e2f5}
.ga-rel-tags a:hover{background:var(--navy);color:#fff}
/* WeChat resource card. Same component as the homepage's
   .ga-promo (section 14) in intent but built for two uneven
   columns, so it gets its own block name. */
.ga-wx{margin-top:22px;background:#fff;border:1px solid #cde6cd;border-radius:4px;padding:18px 20px;display:flex;gap:16px}
.ga-wx > img{flex:0 0 120px;height:120px;border-radius:4px}
.ga-wx .t{font-size:15px;color:#2c7a2c;font-weight:700;margin-bottom:6px}
.ga-wx ul{list-style:none;font-size:13px;color:#444;line-height:1.9}

/* ---------- 18.5 Sidebar: picture stack ----------
   The sidebar variant of the picture card. Not 1:1 with the
   draft on purpose, and this is the only place in the section
   where that is true. The draft (.pics, view_v2.php:129-132)
   puts the caption BELOW the image and lets the image keep
   its natural height; news-v2's .v2-side-pic overlays the
   caption on the image and crops to a fixed 170px, and
   section 10 (.ga-side-pic) already froze that treatment.
   Two contradicting picture treatments in one sheet is worse
   than one consistent one, and batch 4's sidebars
   (_side_common.php, channel.php, category.php) need the
   overlay variant, so this stack is defined as stacked
   .ga-side-pic items and the draft's below-caption variant
   is deliberately dropped. Flagged as open question Q7. */
.ga-pics{display:grid;gap:14px;padding-top:6px}
/* Section 10's .ga-side-pic carries margin-bottom:10px for its
   standalone sidebar slot. Nested in this grid the row gap already
   owns the spacing, so leaving it would give 10+14=24px between the
   pictures. Neutralised here so the grid is the single source of
   the gap, matching the draft's .pics (gap 14px, no per-item
   margin). Specificity (0,2,0) > (0,1,0), so it also wins over the
   <=520px .ga-pics gap rule without needing a copy there. */
.ga-pics > .ga-side-pic{margin-bottom:0}

/* ---------- 18.6 Article-shell responsive ----------
   Kept beside the rules it overrides rather than in section
   16, following section 17.2. The 1200px entry is the load
   bearing one: it is what makes .ga-article full width on
   tablets, since .ga-layout already collapses to a single
   column there (section 16) and a fixed 820px column would
   otherwise overflow.

   CORRECTION, measured 2026-09-19: read this block next to
   section 23, not next to section 21.1. Section 21.1 says the
   稿A narrow tier is 520 and not 768, and the obvious next
   step is to take the sizes the article page changes at 768
   today and move them to 520. That is the mistake section 23
   exists to prevent, and this block is where it would be made.
   The draft (view_v2.php) has exactly ONE media query, at
   1200, and changes no font size at all; every size below is
   this sheet's own tier, added because a 17px title and a
   34px-padded card are desktop geometry, not because 520 is
   where the shell changes. In particular the h1 drop to 21px
   is NOT the counterpart of news-v2's article h1 drop to 20px
   at 768: news-v2.css also drops .v2-article-body from 17px
   to 16px in the same block, and section 17.2 deliberately
   does not carry that second half. The two sizes move at
   different widths here on purpose. */
@media(max-width:1200px){
  .ga-article{padding:22px 18px}
  .ga-rel ul{grid-template-columns:1fr}
}
@media(max-width:520px){
  .ga-article{padding:18px 14px 16px}
  .ga-article h1{font-size:21px}
  .ga-meta{gap:0 14px;font-size:12px}
  .ga-meta .ga-zoom{margin-left:0;width:100%}
  .ga-share{gap:14px;margin-top:22px;padding-top:14px}
  .ga-share img{flex:0 0 84px;width:84px}
  .ga-rel-h{padding:12px 14px 8px}
  .ga-rel ul{padding:12px 14px 14px}
  .ga-rel-tags{gap:8px}
  .ga-rel-tags a{padding:3px 11px}
  .ga-wx{gap:12px;padding:14px}
  .ga-wx > img{flex:0 0 90px;height:90px}
  .ga-wx ul{font-size:12px}
  .ga-pics{gap:10px}
}


/* =========================================================
   19. v2top.php hosting contract  (migration aid, batch 2-5)
   =========================================================
   DOCUMENTATION ONLY - this section declares no selector. It
   is the written form of a rule that sections 0-1 implement, so
   that a migration never has to re-derive it by reading 11
   templates. Everything asserted below was measured on
   2026-09-19; the file:line references are the evidence.

   What partials/v2top.php consumes from its HOST page
     v2top.php is not self-sufficient. It renders the markup for
     .topbar / .wrap / .nav and styles .v2util* / .v2brand* /
     .v2search* itself, but it NEVER declares .wrap, .topbar or
     .nav. If the host supplies none of them the public header
     degrades to an unstyled stack of links. The exact surface:

       var(--navy)    v2top.php:35, :36          (2 uses)
       var(--navy-d)  v2top.php:18, :37          (2 uses)
       var(--line)    v2top.php:29               (1 use)
       .wrap          v2top.php:19,30,40,46,52,57,63
                      always a DESCENDANT (.v2util .wrap,
                      .v2brand .wrap, .topbar .wrap) - never
                      styled standalone, but it must exist
       .topbar        v2top.php:63   (1 override)
       .nav           v2top.php:64   (1 override)

     Note the asymmetry: .topbar and .nav appear in v2top.php
     ONLY inside the <=520px block. Their desktop geometry is
     100% host-supplied. That is the single most common way a
     稿A page looks half-migrated.

   The dupe census (measured, not estimated)
     The set of pages matching ":root{--navy:#0B3D91" and the set
     of pages requiring v2top.php are EXACTLY EQUAL - 11 files,
     with no exception in either direction:
       bidding/detail.php   bidding/list.php   company/detail.php
       company/list.php     flash/detail.php   flash/v2.php
       homev2/a.php         news/view_v2.php   person/detail.php
       person/list.php      search/index.php
     That equality is the usable rule: a page about to receive
     v2top.php must already carry the bare block, and any page
     that carries the bare block is already a 稿A page.

     The 11 blocks are NOT copies of one another. Three variants
     are in the repo and only one of them matches sections 0-1:

     (1) :root tokens
         8 pages  token-for-token equal to section 0
         2 pages  equal PLUS --gray:#666
                  (homev2/a.php:16, news/view_v2.php:27).
                  Section 0 took these, so section 0 is
                  currently a copy of THESE TWO, not of all 11.
         1 page   flash/detail.php:31 declares --muted:#778399
                  and does NOT declare --gray. --muted is
                  defined nowhere else in this sheet nor in
                  news-v2.css, so deleting that page's local
                  block would leave its var(--muted) unresolved.
     (2) body line-height
         1.6 on the majority. search/index.php:37 uses 1.65
         and flash/detail.php:33 uses 1.75. Section 0 ships 1.6.
     (3) .nav a font-size
         the majority sets font-size:15px; search/index.php:37
         omits it. Nothing moves today (it would inherit 15px
         from body) but it is a difference, and a future body
         font-size change would split the two.

   The rule this implies - the dupe is NOT forbidden
     Every 稿A page carries v2top.php's <style> in the BODY and
     its own bare block in the HEAD, so the body block wins
     same-selector ties no matter where /assets/gao-a.css is
     linked. The upstream phase-2 plan calls for a migrated page
     to "just <link> the sheet"; under that reading this section
     is inert and the LOCAL block is what actually styles the
     header. So:
       - Keep the local bare block on a migrated page.
       - Sections 0-1 exist so a page WITHOUT one still renders
         the header, and as the written record of the surface.
       - Where a local block disagrees with sections 0-1, the
         local block wins. That is correct, not drift - it is
         how flash/detail.php keeps --muted today.
     Deleting the local block is a separate, H123-decidable
     change: it needs a 2.4 check that the sheet really does
     load ahead of the partial's <style>. Do not fold it into a
     class-rename migration.

   The check, verbatim (two greps, no script required)
     grep -c 'root{--navy:#0B3D91' <page>   # expect 1
     grep -c 'v2top.php'            <page>   # expect 1
     A page about to receive v2top.php that scores 0 on the
     first line is exactly the bug this section exists to catch.

   WHO <page> IS, AND WHO IT IS NOT  (added 2026-09-20)
     <page> is the HOSTING page - a 稿A page about to receive
     v2top.php. It is never partials/v2top.php itself, and never
     this sheet. Both false readings below were measured, not
     imagined:

       partials/v2top.php  scores 0 on line 1 and 1 on line 2.
       0 is a FALSE FAIL - the partial CONSUMES the tokens, it
       does not own them; the :root they come from is section 0
       of this sheet. 1 is a FALSE PASS - and the count is right
       while the reason is wrong: the hit is the usage comment
       on :3, not a require. The partial requires itself zero
       times. Read the hit line, never just the number.
       this sheet          scores 6 and 41. Both are meaningless
       here - <page> is a page, and this sheet carries the
       census/prose that quotes those very strings; see EOF.

     On a real host the pair separates cleanly, so the two checks
     survive this correction unchanged:
       11 稿A hosts          1 and 1   (each requires v2top once)
       8 phase-2 targets     0 and 0   (they still run the old
                                        shell - which is exactly
                                        what this section catches)
       the partial           0 and 1   (see above)
     Two limits the numbers alone will not show you:

     Line 1 cannot carry the check on its own. It matches 12
     files, and one of them is this sheet - which is not a page
     and was never in scope. "Matches line 1" is not the same
     property as "is a host about to receive v2top.php".

     Neither line reaches news/_side_common.php. It sits inside
     the phase-2 migration set and scores 0 on BOTH lines, like
     the other seven targets - but it is a partial, rendered
     inside tag.php and tagid.php, so it inherits the host's
     shell and will NEVER score 1 on line 2 no matter how
     correct the migration is. Read as "not done yet" it stays
     outstanding forever; read as a target it invites a bogus
     require. Judge it only through its two hosts.

   Blast radius
     Sections 0-1 are the only rules in this file that use bare
     names, so they are the only ones that could reach a non-稿A
     page if one ever linked the sheet; every ga-* section is
     structurally incapable of that. Nothing links this sheet
     today, so the live site is still untouched.

   Blast radius, corrected (added 2026-09-20)
     The paragraph above states two things. The first is false
     as written; the second does not follow from it, and the
     reason is a fact this same section supplies 100 lines
     earlier. Enumerating before rewriting, per the rule this
     repository already wrote down (a claim of the form "only
     X" needs a full query with its hit count written out):

     (a) "sections 0-1 are the only rules in this file that use
         bare names" - FALSE. Measured: 14 rule lines in this
         file carry a bare selector. Twelve are in sections 0-1
         (:root :76, * :77, body :78, a :79, a:hover :80,
         img :117, .wrap :138, .topbar :139, .topbar .wrap
         :140, .nav :141, .nav a :142, .nav a:hover/.on :143).
         TWO ARE NOT: ".wrap{width:auto;padding:0 10px}" at :366
         and ".nav{overflow-x:auto}" at :367 - both inside
         section 16, inside its <=1200px block, and both naming
         the very selectors this section is about. The claim was
         written in the same commit that introduced them (both
         date to the section-16 batch, not to section 19).
         Both are benign today: the partial sets neither .wrap's
         width/padding nor .nav's overflow-x, so the delta on a
         migrated page is real but intended. Nothing here changes
         behaviour. What changes is that the sentence bounding
         this section's risk sits three sections back from the two
         rules that falsify it - section 16, reached from here by
         passing 18 and 17.

     (b) "so they are the only ones that could reach a non-稿A
         page if one ever linked the sheet" - the consequence
         does not hold, and section 19 itself says why: a
         migrated page loads the partial's style block into the
         BODY and keeps its own bare block in the HEAD, so the
         body wins same-selector ties no matter where the link
         sits. That is the same fact this section uses at :841 to
         tell every migration to KEEP the local block. So under
         this section's own instruction the local block styles
         the header and the sheet's bare rules decide nothing on
         the page the sentence warns about.

     The one configuration where the sheet's bare rules DO decide
     is "linked the sheet and dropped the local block" - which is
     exactly what :847-850 says must not be folded into a
     class-rename migration. And in that configuration the header
     is not the part at risk, because the partial declares two
     bare rules of its own: ".topbar .wrap{height:44px}" at
     v2top.php:63 and ".topbar .nav a{...}" at v2top.php:64.
     Both sit inside its <=520px block, so from 520px down the
     partial's versions win on the overlapping properties and the
     header keeps its mobile geometry. What the sheet supplies on
     that page is the DESKTOP geometry, and only that - which is
     the half-migrated header this section already names at
     :793-796. Same shape as section 25's miss: the sheet holds
     both facts and the paragraph connects neither.

     No rule was changed and nothing above was deleted. The
     usable form of the claim is "sections 0-1 are the only rules
     whose PURPOSE is the host contract" - true. "The only rules
     that use bare names" is not, and the difference is precisely
     what a reader auditing blast radius comes here to check.
   ========================================================= */

/* =========================================================
   20. Article-page behaviour contract  (migration aid, batch 5)
   =========================================================
   DOCUMENTATION ONLY - this section declares no selector. It is
   the written form of the coupling between the batch-5 page and
   the scripts that reach into it, so that a migration cannot
   delete a behaviour by renaming a hook. Measured 2026-09-19; the
   file:line references are the evidence.

   Why this is needed at all
     Sections 17/18 rename every class on the detail page, and the
     稿A draft (views/news/view_v2.php) is the source of the new
     names. A CLASS rename is self-consistent: the rule and the
     rule's consumer live in the same two files (the view and this
     sheet), and both are part of the migration. The article page
     has couplings where that is NOT true - where the consumer
     lives somewhere the migration never touches. Those fail with
     no error and no visual difference, the same failure mode as
     the sidebar tabs (section 9) and the early-brief style layer
     (section 17), one layer further out.

   The four ways something reaches this page (only one is safe)
     class selector   news-v2.js:51          .v2-article-body
     id selector      view.php:41            (inline, in the view)
     id selector      common1.js:390         articlebody
     name selector    view.php:27 and :70    (handler attributes)

     The last row is the one every earlier round missed.

   A. The class on the body element - safe, already owned by 17
     .v2-article-body is queried only by news-v2.js:51. That file
     leaves with layout/header.php and 稿A never loads jQuery, so
     it dies on migration exactly like the tabs. Section 17's
     .ga-body replaces it; nothing else needs to know.

   B. The id on the body element - MUST NOT be renamed (the rule)
       view.php:37      div class="v2-article-body" id="articlebody"
       view_v2.php:176  div class="art-body"        id="artbody"

     The draft renames BOTH the class and the id. Renaming the
     class is required. Renaming the id is a separate act with a
     separate consequence, and here the draft must NOT be followed.

     REASONING CORRECTED (design doc section 22). The reason this
     section gave until now was that articleFixes() stays faithful
     to '#articlebody > table' only while the id is kept. That is
     false: articleFixes() queries '.ga-body > table' and does not
     read the id at all, so renaming the id cannot change its match
     set. The rule survives, on narrower and checkable grounds:

       - The id has its consumers. common1.js:390 doZoom(size)
         reads getbyid('articlebody'). It is dead code today (no
         caller anywhere in the repo) and layout/header.php:28
         still loads its file on every old-layout page; the draft
         id 'artbody' would make it permanently unmatched. That is
         a weak reason on its own, because dead code is dead.
       - Do not ADD a name nobody asked for. Renaming the id buys
         nothing the class does not already provide (ids do not
         enter styling), and it is one more rename in a batch whose
         whole hazard list is renames that go silent. Cheap, not
         load-bearing.
       - The class is the part that must be right, and the failure
         that must not happen is losing it: dropping
         class="ga-body", or wrapping the tables in an extra
         element, breaks the strip with nothing to grep for. '>' is
         a direct-child combinator here exactly as it was in the
         original.

     RULE: the .ga-body element keeps id="articlebody". Do not copy
     the draft's id="artbody". It is still the one place where
     "follow the draft" (section 18's stated method) is wrong, but
     the strength is now "this rename buys nothing and the file it
     would break is already dead", not "the table strip depends on
     it". Anyone who finds a live consumer of the id should re-open
     Q10; anyone who finds only dead ones should treat this rule as
     a default, not a hazard.

   C. Font zoom - dies on migration, and the draft's fix is not free
     Three implementations of one feature exist today:
       view.php:27      href="javascript:v2Zoom(14|17|20)"
                        capital Z, defined at news-v2.js:50 inside
                        the jQuery closure, hung on window, setting
                        font-size on the CLASS .v2-article-body
       view_v2.php:166  href="javascript:v2zoom(15|17|20)"
                        lowercase, defined inline at view_v2.php:274,
                        setting fontSize on the ID artbody
       common1.js:390   doZoom(size) - a third, id-based, unused

     On migration the capital-Z pair leaves with news-v2.js, so
     those buttons become dead links unless the migrated view also
     brings the draft's inline script. Section 13 scoped gao-a.js
     to "the two behaviours above, nothing else" and left zoom out
     because the draft already implements it - that stays valid
     ONLY while the view carries its own script. So zoom is not
     this file's job, but it IS part of the batch-5 view contract,
     and it is why the zoom buttons must not be migrated alone.

     The two live implementations disagree on the hook (class vs
     id) and on the sizes (14/17/20 vs 15/17/20).

     C1. B and C CANNOT BOTH BE TAKEN VERBATIM - the id literal
     The sentence that used to stand here read "porting the draft's
     script keeps zoom working on the id", which makes B and C look
     as though they compose for free. They do not, and the failure
     is silent:
       view_v2.php:274   function v2zoom(px) does
                         document.getElementById('artbody').style
                         .fontSize = px + 'px'
     The draft's function takes px but NOT the element - it
     hardcodes the string 'artbody'. Rule B keeps that element's id
     as 'articlebody'. Copy the block unchanged and getElementById
     returns null, so every zoom button throws on .style and does
     nothing: no error the page owner would see, no visual change.
     Same failure shape as section 9's tabs, one layer further out.
     => Porting C means porting it WITH ONE EDIT: the id string
        inside the copied function becomes 'articlebody'. That edit
        is the entire cost of choosing B, and it belongs in the
        batch-5 view diff, not in gao-a.js.
     Corollary: if a future round reverses B and renames the id to
     the draft's 'artbody', this edit disappears and the string is
     copied verbatim instead. Either way the two must agree; what
     must not happen is copying the script and keeping the id, or
     the buttons go quiet with nothing to grep for.

     C2. The draft's script element holds TWO behaviours; only one
     view_v2.php:273-279 is a single script element containing
        (a) the v2zoom function above, and
        (b) an IIFE that switches the sidebar tabs by writing INLINE
            style.display on .rank[data-pane], keyed off #hottabs.
     Only (a) is "the draft's inline zoom script" that C means. Do
     NOT bring (b). After the section 9 rename its selectors
     (#hottabs, .rank[data-pane]) match nothing, and its job is
     gao-a.js's. Carrying it is worse than dead code: it is a
     second writer of the same visibility that writes inline style,
     while section 9 deliberately drives visibility from the .on
     class - so the two disagree about how a pane is hidden, and
     inline style beats the class. If a migration also keeps an id
     on the tab container, both writers fire on the same click.
     The usable rule: copy the FUNCTION, not the SCRIPT ELEMENT.

   D. Handler attributes - a dispatch layer with no file to search
     'javascript:fn(...)' in an href and 'onclick="fn(...)"' call a
     global BY NAME. They contain no jQuery and sit in no script
     block, so all four keys used in earlier rounds missed them:
       (1) independent script files - missed news-v2.js's
           selector dispatch
       (2) news-v2.js - missed the inline block in view.php
       (3) a business string, e.g. 'video-js' - missed the second
           line of that same block
       (4) '$(' or 'jQuery' - misses anything jQuery-free, and
           misses name dispatch entirely
     The key that finds them is the handler form itself. Run both
     over the 8 migration targets before each batch - and run them
     from the repo root with the newsite/ prefix, because there is
     no top-level views/ directory in this repository and the two
     lines as first published here said only views/, which greps
     nothing and exits non-zero:
       grep -rn 'javascript:[a-zA-Z_$]' newsite/views/
       grep -rnE 'on(click|submit|mouseover|change|error)="?[a-zA-Z_$][a-zA-Z0-9_$]*\(' newsite/views/
     CORRECTION, measured 2026-09-19 by actually running them.
     First key, javascript: - 8 occurrences in 3 files:
       news/view.php:27 x3        the three zoom links
       news/view_v2.php:166 x3    the draft's own lowercase v2zoom,
                                  i.e. the very implementation 20.C
                                  ports
       partials/onlineservice.php:49, :56
                                  javascript:void(0); - not a call at
                                  all. That partial is required only
                                  by views/home/index.php, a template
                                  with no route
     Second key, on...="fn( - 9 occurrences in 6 files, and the
     number is the point: this key does NOT match attributes only.
     Five of the nine are script bodies assigning a handler, which
     is the script doing its own binding, not name dispatch:
       attribute form, i.e. the dispatch this section means
         news/view.php:70        onclick="adclick(   dormant, see D
         news/view_v2.php:199    onclick="adclick(   the draft copies
                                 it, so following the draft does not
                                 fix it
         layout/header.php:68    onsubmit="if(      the news-v2 search
                                 form's inline guard, which 21.E
                                 already rules a no-op
         flash/index.php:20      onclick="alert(     bare alert, and
                                 that file has no route
       script-body form, i.e. over-matching
         news/view_v2.php:277, homev2/a.php:346, :348, :372,
         homev2/b.php:231        x.onclick=function(
     So a reader who runs the keys and expects two hits will see
     eleven and must not conclude the section is stale. Read the
     FORM of each hit, not its count. The key over-matches, which is
     the safe direction - a key that under-matched is what produced
     the four earlier rounds of misses.
     Inside the eight targets themselves the only hits are
     news/view.php:27 and news/view.php:70, so "the only two live
     ones are in news/view.php" still holds - but it holds by the
     enumeration above, not by the count alone.

     Ran for batch 5. The complete set of hits inside the 8 targets
     is exactly two, both in news/view.php:
       v2Zoom   view.php:27   provided by news-v2.js today -> dies
                              on migration, see C
       adclick  view.php:70   provided by common1.js:340, still
                              loaded today -> dies. Also present in
                              the draft at view_v2.php:199, so
                              following the draft does NOT fix it.
                              Currently UNREACHABLE: config('ads.28')
                              is '' (src/config.php:33), so the
                              wrapping if at view.php:69 never
                              renders the div. Dormant, not live.
                              If ads.28 is ever filled in, the click
                              beacon to batch.common.php with
                              action=adclick and id=28 silently
                              stops being sent - no error, no visual
                              change - because 稿A pages do not load
                              common1.js. Same shape as the missing
                              analytics in Q4.
     The repo-wide view scan also hits flash/index.php:20 (a native
     alert(), unaffected). That file is a dead template with no
     route, so it is listed for completeness only.

   E. The two rules this section exists to state
     1. KEEP THE NAME, KEEP THE BEHAVIOUR. If the migrated markup
        keeps the hook name the page uses today, there is nothing
        to do. This is why rule B keeps the id instead of adding a
        class-scoped replacement to gao-a.js.
     2. CHANGE THE NAME, RE-HOME IT. If the migration renames a
        hook that a handler attribute calls, the new global belongs
        in gao-a.js - the same file that already re-homes the tabs
        and the table fix. That is what that file is for.

     Concrete outcome for batch 5: nothing has to be ADDED to
     gao-a.js if the view keeps id="articlebody" (B) and carries
     the draft's zoom FUNCTION with its one id string edited to
     match (C1). That single string edit is the whole batch-5 view
     diff for zoom. Whether the buttons survive at all is what
     C1/Q10 decide; it is not something gao-a.js can settle.
     What gao-a.js does need from the view is the class, not the
     id, which is why it was never the thing keeping B alive - see
     the correction at 20.B.

   What this section does NOT decide
     Zoom's existence and the id's name are the same question asked
     from two sides, and it is Q10's for H123. This section only
     fixes the pairing: whatever id the body element keeps, the
     copied function's string must equal it.

   Verification and scope of this section
     Zero declarations were added: the only hunks this section
     contributes are inside comment bodies, which git diff shows
     directly. Do NOT try to prove it with a raw brace count - the
     counts are not equal and have not been since section 19,
     because comments in this file quote braces on purpose:
       header   the ':root{--navy:#0B3D91...}' example
       section 18   the two '.v2-ad img{max-width:100%}' quotes
       section 19   the ':root{--navy:#0B3D91' grep, twice
     Earlier rounds printed a raw pair as though equality meant
     "no rule changed". It does not, in either direction: adding a
     comment can move the count, and adding a rule can leave it
     unmoved if the same commit also drops a quoted brace. Read the
     diff, not the census.
     Nothing links this sheet, so the blast radius is still zero.
     This section is a contract, not an implementation - it fixes
     no behaviour by itself, and a migration that ignores it still
     produces a page that renders correctly. What it prevents is a
     page that renders correctly and quietly does less.
   ========================================================= */


/* =========================================================
   21. Cross-shell contract: what the news-v2 shell does that
       v2top.php does NOT  (batches 2-5)

   Why this section exists
     Sections 19 and 20 describe two ways a migration can lose
     behaviour ON THE WAY OUT: section 19 covers the variables the
     shared header asks the host page for, section 20 covers the
     hooks the article page must not rename. Both assume the
     migration keeps the same page and only swaps classes.
     Batches 2-5 are not that. They replace views/layout/header.php
     and views/layout/footer.php with partials/v2top.php - a
     different shell with a different menu and, more importantly, a
     disjoint set of class names. Measured: zero occurrences of the
     string v2- inside v2top.php and inside all 11 稿A templates,
     and zero occurrences of ga- inside news-v2.css or
     layout/header.php. Nothing is shared, so everything the old
     shell did outside its own markup is dropped WITHOUT AN ERROR.
     This section is the list of those things.

   The pending set, measured 2026-09-19 (read-only, no execution)
     Lines carrying the string v2-, per view. This is a complete test
     for which system a view belongs to:
       pending      news/view.php 47   news/channel.php 76
                    news/category.php 44   news/tag.php 18
                    news/tagid.php 23   news/_side_common.php 15
                    channel/index.php 37   expo.php 4
       already-稿A   0 in all 11 templates that carry the baseline
     Read the numbers as a set membership test, not a difficulty
     scale. A file in the first set is news-v2 markup.

   A. Public path mapping changes.
      v2top.php has 14 menu items and every one of them already
      lands on a 稿A page. The news-v2 menu has 11 and was NOT a
      superset of it: it has no 招投标 / 人物库 / 企业库 entry at
      all, it renames two channels (6G/5G-A versus 6G/5G, and
      物联低空 versus 物联·低空), and it places 资讯中心 second
      where v2top.php places it last. So switching the shell changes
      what a reader can reach from EVERY page, including pages that
      are not themselves being migrated. That is a product decision.
      It is recorded here so that it is not discovered after the
      fact as a "missing menu".

   B. The head counter. layout/header.php loads a Baidu tongji
      snippet from hm.baidu.com in head (header.php lines 32-40).
      No 稿A template carries it. Placement matters: the snippet
      inserts itself at document.getElementsByTagName("script")[0],
      so where the line sits in the document decides the marker it
      attaches to. Section 15 above already records the footer gap
      (ICP filing number, sitemap, and the Matomo tag that lives
      only in layout/footer.php lines 26-38); this is a SECOND and
      separate counter, in a different file, so a migration that
      adopts the 稿A footer still loses it. Do not let Q4 (Matomo)
      be read as covering this one.

   C. Canonical. layout/header.php emits a link rel=canonical
      element from the $canonical view variable (header.php lines
      18-20). v2top.php emits no head element of any kind. All 8
      pending routes are indexable: the only noindex is host-level
      (bootstrap.php lines 59-64) and does not fire on the
      production host, unlike the /v2 preview routes in section 20.
      A migrated view must re-emit canonical itself, or the
      controller filling the view data must. A missing canonical is
      invisible to a reader either way.

      CORRECTION, measured 2026-09-19: do not read "this migration
      loses canonical" as uniform across the 8 - it is a per-view
      variable, emitted by header.php only when !empty
      (header.php:18). Measured against each view's controller:
        emits today   expo.php          ExpoController:24
                      channel/index.php  ChannelController:63,:101
                      news/channel.php   NewsChannelController:65
                      news/view.php      NewsController:217
        none today    news/category.php  CateController:108 (null)
                      news/tag.php       TagController:101 (null)
                      news/tagid.php     TagController:188 (null)
      i.e. the 5 routes above have one and the 3 below do not, and
      the eighth file is a partial - see tail note 2. Take the
      page's own $canonical; never copy a sibling's.

   D. The noindex meta element. Same origin and same fix
      (header.php lines 21-23). This one fails OPEN rather than
      closed, so it is cheaper than C: the cost appears only on the
      day the host stops being the production host.

   E. Three script assets, two of which carry live behaviour.
      jquery.lazyload.js matters only if a migrated view still
      carries class v2-lazy - 8 occurrences across 5 pending files
      (channel/index.php 1, news/category.php 1, news/channel.php 4,
      news/tag.php 1, news/tagid.php 1); the other 3 pending files
      have none. Those classes are being rewritten anyway, so the
      right target is the native loading attribute, not a port of
      the plugin.
      popwin.js plus the footer click handler (footer.php lines
      42-46) implement the login popup: the news-v2 shell renders
      登录/注册 as an anchor carrying class w_lg, and jQuery binds a
      window to it. v2top.php renders the same two links as PLAIN
      links straight to club.mscbsc.com. Nothing errors and nothing
      looks broken; the interaction is simply gone.
      The news-v2 search form guards an empty query with an inline
      submit handler raising an alert (header.php line 68); v2top.php
      instead marks the input required (v2top.php line 85). What the
      browser does is the same - an empty query never reaches
      /search - so only the wording differs. That one is a no-op and
      needs no port.

   Two hooks this section deliberately does NOT import
     The news-v2 menu has a mobile toggle: a button carrying class
     v2-nav-toggle plus a v2-nav-list that is hidden below 768px
     (news-v2.css lines 314-317) and switched by news-v2.js lines
     6-7. 稿A has no equivalent and does not need one: scrolling the
     menu horizontally on the narrow tier is its standard answer.
     The rule is a .nav selector carrying overflow-x auto, it sits
     in section 16 of this sheet, and it is repeated in most 稿A
     templates (a.php line 142 is the only one that also scrolls
     .tabs). Porting the toggle would put a second navigation
     model into a shell that already solves the narrow case, and the
     two would fight over the same element. Do not port it - and do
     not import its 768px scale point either, see 21.1.

   21.1 The breakpoint mismatch, and why a 768px check is not a
        sufficient acceptance shot
     The two shells change layout at DIFFERENT widths:
       news-v2.css   1199 / 992 / 768   (three blocks)
       homev2/a.php  1200 / 520         (two blocks)
     The 稿A narrow tier is 520px, not 768px. A page can therefore
     look right at 768 on one shell and wrong at 768 on the other.
     The batch 2-5 acceptance set must include 520 alongside the
     1440 / 768 / 390 shots used so far. 390 still matters - it is
     narrower than 520 and sits inside that tier - but 520 is the
     boundary the shell actually has, and it is the one a 768-only
     check steps over.
     This is the SHELL's boundary only. It is not the point at
     which every rule changes, and applying it to a column-count
     rule is a visible regression - see section 23, which is the
     per-rule companion to this paragraph.

   What this section does NOT decide
     - Whether the migrated footer becomes a full footer. That is
       section 15 plus Q4, and it predates this migration.
     - Where canonical, the noindex element and the head counter
       should be emitted FROM. A view can hardcode them, but the
       cleaner fix is a shared partial, and choosing that is a
       file-ownership question belonging to the same handover
       request as the view files themselves.
     - Which menu wins. This section records the difference; it
       does not pick.

   Verification and scope of this section
     Zero declarations added, and unlike section 20 this section
     quotes no brace characters at all, so a raw brace count is
     unmoved by it. That is a convenience, not a proof - sections 19
     and 20 already broke that count on purpose, so the proof is
     still the diff. Every number above is a read-only grep or wc
     over the working tree, reproducible per file.
     Nothing links this sheet, so the blast radius is still zero.
     This section is a contract, not an implementation: it changes
     no behaviour by itself, and a migration that ignores it still
     produces a page that renders correctly. What it prevents is a
     page that renders correctly while reporting nothing, showing
     nothing in search results, and unable to reach a third of the
     site.
   ========================================================= */


/* =========================================================
   22. Tab panes: pane visibility is a CLASS, not an inline
       style  (migration hazard, batches 3 and 4)

   Why this section exists
     Section 9 defines .ga-tab-body as display none plus an .on
     variant, so the FIRST pane must carry class="ga-tab-body on"
     and the others must carry the bare class. That contract is
     correct and it is not what this section is about. This
     section is about the one attribute that makes it fail, and
     that no earlier section can catch: sections 20 and 21 are
     about renamed hooks and about the shell swap, and this is
     neither. It is a single inline style inside the views being
     migrated. Section 20.C found the same failure mode for the
     zoom function; this is the same rule in a second place.

   Measured 2026-09-19, over the 5 files that hold a tab group
     tabs        news/_side_common.php:10   news/view.php:107
                 news/channel.php:171      news/category.php:62
                 channel/index.php:92
     All five are the same two-pane "this week / this month"
     block, with the active tab always marked class="active".
     The five files are reached by EIGHT routes, so the style
     attribute below sits on eight public URLs: news/view.php
     by two (/info/Nw... and /viewnews-N.html), news/category.php
     by two (/category-N.html and /cate.php), news/_side_common.php
     by two (/tag-*.html and /tags.php), news/channel.php by one
     (/news) and channel/index.php by one (/channel/{en}).
     Pane state, same five files:
       first pane    no style attribute anywhere        (5 of 5)
       second pane   style="display:none;"              (5 of 5)
     i.e. _side_common.php:19, view.php:116,
     channel.php:180, category.php:71, channel/index.php:97.

   The failure, step by step
     An inline style outranks every stylesheet rule that is not
     marked important, so a pane carrying display none stays
     hidden whatever this sheet says - and adding class="on" to
     it, which is the part everyone remembers, changes nothing.
     After a mechanical class swap the three states are:
       pane 1  class="ga-tab-body on"  no style   visible, right
       pane 2  class="ga-tab-body"     no style   hidden, right
       pane 2  class="ga-tab-body on"  no style   STILL hidden
     The middle line is what the markup actually produces, so
     the FIRST rendering of a straight swap is correct. That is
     what makes it dangerous: there is no visual symptom to
     review, and the only thing that changed is what happens
     after a click. Clicking then does the opposite on the two
     panes - gao-a.js removes .on from pane 1, which hides it,
     and adds .on to pane 2, which does nothing - so the tab
     group reads "this month" and shows an empty card. Half the
     group works and half of it erases the card.
     Generalised: any hook driven by a class can be killed by an
     inline style on that same element, and a diff that only
     compares class names cannot see it.

   The instruction, for the five files above
     Delete the whole style attribute when swapping the classes;
     do not translate it and do not carry it over.
       before  <ul class="v2-list v2-list-num v2-tab-body" style="display:none;">
       after   <ul class="ga-rank ga-tab-body">
     and the first pane becomes
       <ul class="ga-rank ga-tab-body on">
     Do NOT put display block inline on the first pane either:
     .ga-tab-body.on already supplies it, and hardcoding it in
     the markup would make pane 1 un-hideable by the switcher.
     Tab labels: .active becomes .on (section 9).
     The 稿A draft (view_v2.php:233) does the opposite - second
     pane marked with data-pane plus inline display none, and
     switched by inline styles. Section 20.C2 already rules
     that draft's script element must not be carried over; the
     same applies to its pane markup, which is a third model
     that belongs to none of .ga-* classes here.

   Why no declarations are added
     The obvious next thought is a rule giving .ga-tab-body a
     padding of its own, so a pane outside a card body is not
     flush to the edge. Checked before writing it, and it is
     not needed: section 4's guard is written
     .ga-card-bd > .ga-list, .ga-card-bd > .ga-rank, so it only
     ever matches a list that is a direct child of .ga-card-bd.
     With that wrapper present the wrapper owns the inset and
     the guard zeroes the list; with no wrapper the guard does
     not match at all and the list keeps its own padding
     (.ga-list 6px 16px 12px, .ga-rank 8px 16px 12px). Both
     structures are defined already, so the compensating rule
     that seemed missing would have been dead code.
     Worth recording for anyone who does add one later: a
     .ga-rank.ga-tab-body rule is (0,2,0), which ties section
     4's guard and sits LATER in this file, so it would win the
     tie and silently restore the double padding the guard
     exists to prevent. If a pane-specific padding is ever
     needed, give it (0,1,0) and let the guard keep winning.

   Verification and scope of this section
     Zero declarations added, so nothing changes how this sheet
     parses and nothing changes on any page. Every count above
     is a read-only grep over the working tree and reproducible
     per file. Nothing links this sheet, so the blast radius is
     still zero, and this section is a contract rather than an
     implementation: a migration that ignores it still renders
     correctly at load. What it prevents is a page that looks
     right in every screenshot and whose sidebar tab empties the
     card the first time a reader clicks it.
   ========================================================= */


/* =========================================================
   23. Breakpoints: 520 is where the SHELL changes, not where
       every rule changes  (migration hazard, batches 2-5)

   Why this section exists
     Section 21.1 corrected a real misconception - the narrow
     tier of a 稿A page is 520px, not 768px - and it is easy to
     over-apply that correction. It governs the parts of a page
     that the shared header carries. It does NOT govern a
     two-column content grid, and the two numbers disagree on
     purpose. Acting on 21.1 alone can delete a working 768px
     rule from a page being migrated, or add a 520px one that
     makes a two-column grid go single-column 248px earlier than
     it does today. Both are visible changes, and neither is a
     fix.

   The inventory, measured 2026-09-19 over all of newsite/
     Thirty-one media at-rules: thirty width tiers, plus one
     prefers-reduced-motion block at assets/admin.css:474.
     Nothing else in the tree is conditional - no supports rule
     and no container query - so "breakpoint" below always means
     a width tier.
     That is a bare substring count over .css and .php, and it
     holds only while no comment spells the at-rule name out.
     This paragraph deliberately does not, for the same reason
     sections 19 and 20 broke the brace count: a token quoted in
     prose stops being countable, and someone re-measuring later
     will see one extra hit per mention. Subtract those rather
     than "correcting" the number.
     Shell, one breakpoint pair, shared:
       partials/v2top.php          1200 / 520
       homev2/a.php                1200 / 520
     Already 稿A (preview route, not in the migration set) and the
     working proof that a 1200-only tier is enough for a full page:
       news/view_v2.php            1200 only
     Pending migrations, driven by the shared layout - and these
     seven files contain NO media query of their own, because
     layout/header.php supplies the whole news-v2 system and its
     1199/992/768 tiers come from news-v2.css:
       news/view.php, news/channel.php, news/category.php,
       news/tag.php, news/tagid.php, news/_side_common.php,
       channel/index.php    ->  remove the layout require and
       these seven files lose their only source of responsive
       behaviour.
     Pending migration, carries its own:
       expo.php                    768    - the single exception
     Already 稿A, all 1200 with one narrow exception:
       bidding/list.php, bidding/detail.php, company/list.php,
       company/detail.php, person/list.php, person/detail.php,
       search/index.php, flash/v2.php   -> 1200 only
       flash/detail.php            1200 / 520
       homev2/b.php                1200 only  (稿B draft,
                                   noindex, not migrated)
     Adjacent, out of scope: assets/admin.css is 1200 / 768 and
     is an internal tool, not a public page. Do not merge the two
     inventories; the number coincidence is not shared intent.

   The rule
     The SHELL gets the 520 tier because .topbar and .nav are
     shared chrome whose geometry must stay constant across
     pages, and v2top.php already owns that block (section 19).
     A content column does not: a 1200px two-column layout goes
     single-column at 1200, which is exactly what every migrated
     page does, and that is earlier and not later than 768.
     .ga-layout is the copy of that pattern - 820 + 360 at wide,
     one column at 1200 - so a migrated page needs NO new
     breakpoint for its columns. What it may still need is a
     768 or 520 tier for ROW-level geometry, which is how this
     sheet itself is built: section 16 uses 1200 for columns and
     768 / 520 for .ga-media row sizes and .ga-grid2.

   Two live examples, both deliberate
     flash/detail.php has BOTH numbers and they do different
     jobs: 1200 for .main and .nav, 520 for .article padding and
     .crumbs. It is the model to copy.
     expo.php (batch 2, the first page to be migrated) is the
     exception - it is the only pending file that declares a
     breakpoint of its own. Do not read its 768 as a leftover
     from news-v2.css that must be rewritten to 520: that rule
     is in the view, and the file has no other media query, so
     changing it would change today's rendering rather than
     restore it. Whether it should join the 1200/520 pattern is
     a layout decision that belongs with the batch-2 rewrite,
     not a correction to make on the way past.

   The hazard, stated as a mistake I could make
     Reading 21.1 and "correcting" the seven layout-driven views
     by adding a 520px tier in place of the 768px behaviour they
     have today. At 520 exactly, the current page has already
     collapsed its columns but still shows full-size media rows;
     the corrected version would show narrow ones. The 21.1
     acceptance requirement is a screenshot at 520 - it is not
     an instruction to move every 768 in the codebase to 520.

   What this section does NOT decide
     - Whether .ga-grid2 should get a tier between 768 and 520.
       It is two columns until 768 and then one, and nobody has
       asked for an intermediate step.
     - Any new breakpoint. This section records what exists; it
       does not add a tier to any sheet or view.

   Verification and scope of this section
     Zero declarations added. It quotes no brace characters, so
     unlike sections 19 and 20 a raw brace count is unmoved by
     it - a convenience, not a proof; the proof is the diff.
     Every number above is a read-only grep over the working
     tree and reproducible per file. Nothing links this sheet,
     so the blast radius is still zero. Like section 21 this is
     a contract: a migration that ignores it renders correctly.
     What it prevents is a page whose columns break at the
     right width and whose content inside them breaks at the
     wrong one.
   ========================================================= */


/* =========================================================
   24. The head element: the part of the shell swap that
       section 21 does not cover  (batches 2-5)

   Why this section exists
     Section 21 lists what the outgoing shell does that
     v2top.php does not, but its subject is the shell swap and
     it is organised around the BODY - the menu, the two
     counters, the login popup, the lazyload fallback. Sections
     19, 22 and 23 are about the host page and the class swap.
     Two head elements do get named, as 21.B and as 21.C/21.D,
     but nothing covers the head AS A WHOLE, and that is the
     part of the swap with the widest silent blast radius.
     Measured 2026-09-19: v2top.php emits no head element of
     any kind - it opens with a style block followed by markup
     - and section 19 already states that the head stays with
     the host page. The head is therefore not something a
     migration inherits. It is something the migration has to
     create.

   The measurement that makes this a batch-level item rather
   than a per-page detail
     A read-only grep for the doctype, for a head tag, and for
     the charset, viewport, title, keywords and description
     lines across the whole of newsite/ returns exactly one
     shared file that emits any of them: layout/header.php.
     ALL EIGHT pending views open by requiring it on their
     first line and contain no head of their own:
       expo.php, channel/index.php, news/channel.php,
       news/category.php, news/tag.php, news/tagid.php,
       news/_side_common.php, news/view.php
     Eight of eight - not "most of them", and not "the ones
     without an inline style block". expo.php is the clearest
     case and is worth reading before the first migration
     starts: 54 lines, no head, and its style block sits at
     line 42, which is at the END of the body, immediately
     above the footer require. Its 768px tier is a body-level
     rule even though section 23 lists it as that page's own
     breakpoint.
     Consequence for batch 2: this is not "add one link tag to
     an existing head". The head is written from scratch - and
     the lines a reader thinks of as the top of the page
     currently come from a file that is about to be dropped.

   H-1. What the outgoing head carries
     layout/header.php lines 10-41, in document order:
       doctype and an html lang attribute
       a content-type meta declaring charset=gbk
       a viewport meta
       title
       keywords and description metas
       the canonical link, guarded by !empty
       the noindex meta, guarded by the noindex() helper
       the news-v2 stylesheet link
       six script tags: jquery, ajax, common1, lazyload,
         news-v2.js and popwin
       an inline Baidu tongji snippet (already 21.B)
     Of these, only the middle block is per-view data. The
     doctype, the html element, the charset and the viewport
     are structural and are not view variables.

   H-2. Canonical: the two precedents disagree, and the one a
        migrator would copy from is the wrong one
     A page that re-emits canonical for itself must use the
     guarded form:
       layout/header.php:18   wrapped in !empty, so a null or
                              missing variable emits nothing
       homev2/a.php:13        NOT wrapped - it emits the link
                              unconditionally
     Copying the second into a route whose controller passes
     null produces a link whose href is an empty string. That
     is worse than emitting no link at all - it nominates the
     site root as the page's canonical URL - and it is not an
     error, so nothing reports it.
     Measured over the six controllers behind the eight views,
     there are eight canonical entries: five non-null and
     three explicitly null.
       non-null  ExpoController:24, ChannelController:63 and
                 :101, NewsChannelController:65,
                 NewsController:217
       null      CateController:108, TagController:101,
                 TagController:188
     The three nulls are the category, tag and tagid routes, so
     the guard is not defensive coding here: on three of the
     seven routed pages it is the whole difference between the
     intended and the unintended behaviour.
     (21.C states this as "5 of the 8 routes". The split above
     is the same facts per file; ChannelController supplies
     two entries for one route.)
     Where the line should live, in the view, in a shared
     partial or in the controller, is an ownership question and
     belongs with the same handover request as the view files
     themselves, which 21.C already says. What this section
     fixes is the FORM of the line, not its home.

   H-3. Two static lines, and one of them decides whether the
        acceptance screenshots mean anything
     Charset: a view cannot inherit it, because there is no
     head to inherit it from. Every 稿A template states it
     statically, as a meta charset element whose value is gbk
     (homev2/a.php:8 and the other ten), which is what the
     renderer and the response header already assume. A
     migrated view must carry the same static line.
     Viewport: also static, and also carried by all eleven 稿A
     templates - and by none of the eight pending views, which
     is the point. Its value is width=device-width with an
     initial scale of 1, and it is the line that tells a phone
     this page has a 520px narrow tier instead of a wide
     desktop layout to be scaled down.
     The acceptance consequence is concrete and easy to miss.
     2.4 requires screenshots at 520 and at 390. Without this
     line a mobile rendering falls back to a layout viewport
     of roughly 980px and scales the result down: the
     narrow-tier rules never apply, the shot looks small but
     not broken, and the branch being accepted was never
     tested. So 2.4 evidence is conditional on the migrated
     page carrying BOTH the viewport line and the charset
     line. That could not be verified here - see the
     verification note below - so it is recorded as a
     precondition on how the evidence is captured, not as an
     observed defect.

   H-4. The last two lines are the footer's, not the header's
     layout/footer.php ends with the closing body and html
     elements (lines 47-48), and all eight pending views end by
     requiring it - expo.php:54 and channel/index.php:120 among
     them. That file also closes the container div its header
     counterpart opened, at its line 4, which is a structural
     pair no single file owns.
     A migrated view that stops requiring the footer therefore
     loses the document's closing tags along with its content.
     Trivial to fix and completely silent when missed: a
     browser closes an unterminated document without complaint.

   H-5. What is NOT at risk, so that it is not double-counted
     title, keywords and description are ordinary view
     variables and all eight controllers already supply them,
     with empty strings for keywords and description on the
     category, tag and tagid routes, which is what
     layout/header.php prints today. Losing the head does not
     lose the data; it loses the four lines that print it.
     The Baidu tongji snippet is 21.B, the Matomo tag and the
     ICP filing number are section 15, and the login popup is
     21.E. Those are body and footer items and are counted
     there. This section adds the head itself and the document
     closing tags, and nothing else.

   Verification and scope of this section
     Zero declarations added. It quotes no brace character, so
     unlike sections 19 and 20 a raw brace count is unmoved by
     it - a convenience, not a proof; the proof is the diff.
     Every count above is a read-only grep over the working
     tree and reproducible per file: the viewport and head
     sweep is a whole-tree grep for the doctype and for each
     meta line, and the canonical split is one grep for the
     array key across the six controllers.
     NOT verified: no browser was run here, so the 980px
     fallback in H-3 is stated from how a mobile browser is
     specified to behave when a document declares no viewport,
     not from a screenshot. Treat it as a requirement on how
     2.4 evidence is captured.
     Nothing links this sheet, so the blast radius is still
     zero. Like sections 21 to 23 this is a contract: a
     migration that ignores it renders correctly in a desktop
     browser and fails quietly everywhere else.
   ========================================================= */


/* =========================================================
   25. The third clearfix: the one float in the migration set,
       and the shell swap that un-contains it  (batch 5)

   Why this section exists
     Sections 21 to 24 list things a migration LOSES by
     swapping the shell. This one is about the thing the swap
     makes visibly wrong while every class name still looks
     right. It is also a correction: the site it concerns is
     the one the existing notes do not name.

   The measurement that reframes the existing note
     The design doc's section 9 names two .clearfix sites -
     channel/index.php:38 and news/tagid.php:21 - and section
     4.4 of the 2.4 checklist carries the second of those.
     Neither is the one that matters. Counted 2026-09-20 over
     the eight migration targets, by one grep for the class:
       channel/index.php:38   class="clearfix"  + inline flex
       news/tagid.php:21      class="clearfix"  + inline flex
       news/view.php:89       class="clearfix"  (no style at all)
     Three sites, not two. And news/view.php is batch 5, so
     the checklist item that names tagid.php:21 sits in the
     batch-4 section and never reaches it.

   Why view.php:89 is the only one of the three that needs a
   decision
     It is the only float in all eight files. One grep for a
     float declaration across the eight targets returns
     exactly one hit, and it is inside this wrapper:
       view.php:89     <div class="clearfix">
       view.php:90       <img width=140 height=140
                            style="float:left;margin:4px 14px 4px 0;">
       view.php:91-97    <div> the reply-code list </div>
     The other two sites each carry inline flex with
     flex-wrap, so no float exists inside them and their
     ::after is an empty flex item - they do not need
     clearing. This one does: it is a bare block whose only
     containment is the clear.

   Why the clear stops working, and why nothing replaces it
     .v2 .clearfix::after (news-v2.css:15) is scoped to a body
     carrying class v2, which layout/header.php:42 emits. All
     of the 稿A templates open with a bare body element -
     homev2/a.php:164, bidding/list.php:93, flash/detail.php:73,
     news/view_v2.php:145, search/index.php:43 among them - so
     after the swap the selector cannot match. This sheet
     declares no clearfix rule, and that omission is a
     decision, not an oversight: design doc section 9 declined
     one on the grounds that 稿A lays out with flex and grid.
     That reasoning holds for the two flex sites. It does not
     hold for this one. Nor is the float contained by any
     ancestor, and this is the part that is easy to assume:
     the chain is .ga-card-bd (padding, min-width, section 4),
     then .ga-card (background, radius, shadow, min-width),
     then the page. None of the three establishes a block
     formatting context - none sets overflow, float,
     flow-root or position - and news-v2.css does not give
     one to .v2-card or .v2-card-bd either.
     Consequence on a migrated article page - and the
     condition it hangs on. Whether it is visible depends on
     the reply-code list at :92-:97 being SHORTER than the
     float's 148px box (140 plus its 4px top and bottom
     margins). While a float is contained the wrapper's height
     is the larger of its in-flow content and its floats;
     once the clear is gone it is the in-flow content alone.
       list >= 148px   the wrapper is tall enough on its own,
                       the image sits inside the card and
                       nothing looks wrong
       list <  148px   the wrapper collapses to the list and
                       the 140x140 image hangs below it, out
                       of the green border, with the card's
                       bottom edge above its own content
     At 13px over a 1.8 line-height a row is about 23px, so
     seven rows clear 148px. A long reply list therefore
     hides this completely. That is why it is worth writing
     down rather than obvious: the defect is data-dependent,
     it appears on some articles and not others, and BOTH
     states are silent. It cannot be settled from this file -
     what $zl holds is a database question, and this round
     ran no database and no browser.

   The sheet already contains the answer, and this section did
   not see it  (added 2026-09-20)
     Every state above rests on one unstated premise: that the
     migration KEEPS a block wrapper around the pair. It does
     not have to, and the mapping does not ask it to. The
     component table maps this block - view.php:86-100, the
     green-bordered WeChat card, the range the wrapper at :89
     sits inside - to .ga-wx. That name is not borrowed from
     news-v2; it exists only in this sheet, and it declares
     display:flex.
     A float inside a flex container is not a float. Flexbox
     ignores the float property on a flex item, and the flex
     container establishes the formatting context the ancestor
     chain above does not - so the 140px box cannot leave it,
     short reply list or long, and the two-state table earlier
     in this section has no second state on that path.
     The mapping is a rename, not an extra layer:
       div.clearfix   becomes div.ga-wx ITSELF. Section 18.4
                      selects .ga-wx > img by CHILD, so the
                      wrapper must BE .ga-wx, not sit inside one.
       the img at :90 stays its direct child. float:left goes -
                      it is dead in a flex container either way,
                      but the margin in the same inline
                      declaration is not, and section 18.4 fixes
                      the box at 120 rather than 140. Those two
                      numbers are the whole of what this mapping
                      changes by eye.
       the reply list becomes a ul, which section 18.4 styles.
     So the decision this section declines to take has already
     been taken - by this sheet, on the path that uses the
     sheet. What the escape analysis still describes is the
     other path: carrying the class names across onto card
     wrappers, v2-card and v2-card-bd to ga-card and
     ga-card-bd, which leaves this block a plain block and
     reproduces the escape exactly as written above. Both paths
     are silent at both states, so no screenshot tells them
     apart; what tells them apart is which component the
     migration reaches for.
     Why this section missed it, recorded because that is the
     reusable part: the containment sweep below was run over
     section 4 and over news-v2's two card rules, which are the
     wrappers this block has TODAY and not the component it is
     being mapped TO. Section 18.4 is a handful of lines, and
     this section already names .ga-wx ul at the end of it,
     under the list-style heading, without asking what .ga-wx
     itself is. The negative claim was true of everything it
     covered, and false as a statement about this site.

   Whose call this is
     Design doc section 9 recommended deleting the class or
     replacing it locally. For this site that means either
     dropping the float and laying the pair out with flex, or
     clearing it with a 稿A rule. Both are view-level edits to
     news/view.php, a file this sheet does not own, so this
     section records the hazard and does not pick. Declaring a
     .clearfix rule here instead would put a v2-era name into
     the 稿A system for a single call site, which is exactly
     what section 9 already refused.

   The second loss: list-style is reset by a descendant rule
     Counted 2026-09-20 over the eight migration targets, one
     grep for an opening ul tag gives 36, one per line:
     channel/index.php 4, news/channel.php 11,
     news/category.php 8, news/view.php 9,
     news/_side_common.php 4, and none at all in expo.php,
     news/tag.php or news/tagid.php. Thirty-five of the 36
     carry class v2-list, which section 5 maps to .ga-list, so
     those trees stay marker-free through the class swap. The
     36th is view.php:47, which carries no class.
     Separately, one li is not inside any of them. The parent
     of view.php:94 is the div opened at :91; the nearest ul
     above it opens at :77 and closes at :81, before that card
     begins. So the chain there is div > li with no list
     element in it at all.
     Both sites rest on the same rule, and it is the
     DESCENDANT selector .v2 ul, .v2 ol, .v2 li
     { list-style:none } at news-v2.css:12, which dies with
     the body class for the same reason the clearfix above
     does. This sheet resets list-style only for .ga-list,
     .ga-rank, .ga-rel ul, .ga-wx ul and .mn-pts li - and
     neither of these two sites is inside any of them. A
     classless ul still generates list items, and a bare li
     keeps display:list-item from the user agent whatever its
     parent is, so the expected result is a marker at both
     sites where there is none today.
     Framed against the design doc: section 17.5 lists
     ".v2 ul,.v2 ol,.v2 li" as the first of three classes of
     dying rule and observes that section 9 wrote down only
     .clearfix of them. These two lines are where that class
     is observed to bite rather than merely to exist.
     Recorded, not fixed - both fixes are view edits in the
     same file as the paragraph above.
   One thing this section does NOT claim
     Each of the two flex sites gets an extra empty flex item
     from the ::after today. Whether that affects where those
     rows wrap has not been measured and is not asserted here.

   Verification and scope of this section
     Zero declarations added. It quotes no brace character, so
     unlike sections 19 and 20 a raw brace count is unmoved by
     it - a convenience, not a proof; the proof is the diff.
     Every count above is a read-only grep over the working
     tree and reproducible per file: the three sites are one
     grep for the class across the eight targets, the single
     float is one grep for a float declaration, and the
     containment question is a grep for overflow, float,
     flow-root and position over section 4 and over
     news-v2.css's two card rules.
     ⚠️ That last one is the sweep that was run over the wrong
     object set, and the subsection above is the repair. Run it
     over section 18.4 as well - one grep for display:flex over
     this sheet returns .ga-wx, and the mapping table's own row
     for this block names that component. The rule that would
     have caught it is the one this repository already wrote
     down: a claim of the form "no X exists" or "X is the only
     one" must be preceded by a full query with its hit count
     written out. The float count got that treatment here ("one
     grep returns exactly one hit"); the containment sweep did
     not, and it swept the wrappers the block has today instead
     of the component it is being mapped to.
     ⚠️ This section writes that class name many times over, so
     a later repo-wide count of it must subtract these lines;
     same caveat as section 23 records for at-rule names.
     Deliberately no count is given here, and that is worth a
     sentence of its own: the number cannot be written down
     stably, because the sentence that states it is itself one
     of the occurrences. Measured twice while drafting this
     block it read 9, then 11 - and the second reading was
     taken after the paragraph below was written, so the rise
     was caused by this section's own prose, not by the
     source. Any figure here would be stale on arrival.
     Sections 19 and 20 have
     the same self-reference problem with the tokens they
     quote, which is why section 23 tells the next reader to
     subtract mentions rather than to "correct" the figure.
     NOT verified: no browser was run here, so the escape
     described above is reasoned from CSS 2.1 containment and
     from the absence of a formatting context in the ancestor
     chain - not from a screenshot. Nothing links this sheet,
     so the blast radius is still zero. Like sections 21 to 24
     this is a contract: a migration that ignores it renders
     correctly in a desktop browser for every card but one.
   ========================================================= */


/* =========================================================
   Tail note: the census printed in section 19 cannot be
   inherited  (appended 2026-09-20; zero declarations)

   Section 19 prints one measured pair for this sheet - the
   two-line census applied to a file that is not a page. It
   read "6 and 37". The first number holds. The second did not
   describe the file it shipped in.

   The second number, per commit, re-run with section 19's own
   verbatim line-2 grep:
     20   4cfd320   the commit section 19 first landed in
     37   791fedc   first commit to reach 37
     40   0ac60c9   the commit that WROTE "37" into the prose
     41   aaebd35   the commit that wrote design section 43.2
     41   today     (7f58a1c, the last commit to touch this
                     file, leaves the count alone)
   So the shipped number was 40, not 37. Whether 37 ever
   described a tree its author could see is not recoverable
   from this repository: 37 is exactly the count at the two
   commits before 0ac60c9, so the likeliest story is that it
   was measured correctly and then invalidated by the very
   paragraph written to publish it - the three mentions of the
   partial in that paragraph are about the size of the rise.
   That is a guess and is labelled one. What is NOT a guess is
   the part that matters: the number in the commit does not
   match the commit. It was not re-run after the prose that
   carried it was written.
   Line 1 moved as well, and once: 4 at 4cfd320, 6 from
   791fedc onward, and 6 today because nothing since has added
   a page to that set. Both are re-run at the start of any
   round that quotes them.

   Why it drifted, and why it will drift again: this file is
   not a host of the partial section 19 is about, it is the
   document that DISCUSSES it. The census is a question about
   hosts; pointed at a file that names the partial in prose,
   it measures prose. Every later documentation round that
   names the partial - including a round written only to
   correct earlier prose, which is what 0ac60c9 was - adds a
   hit. A figure of this kind is therefore not merely due to
   go out of date later. It is out of date the moment the
   paragraph around it is finished, and the paragraph is part
   of what changed it.

   Three consequences, all of which were already true:
     1. A page must never be checked against the magnitude in
        section 19's paragraph. Section 19's own two greps are
        the check; that paragraph's numbers are illustration.
     2. The property the pair demonstrates - "meaningless
        here" - does not move, and never needed the number.
     3. Section 25 records this same hazard for its own class
        name and answers it by declining to give a figure at
        all. That is the right shape, and section 19 is the
        worked proof that it is not a style preference: the
        figure that WAS given is already wrong in the commit
        that published it.

   Placement, on purpose: this block is appended after the
   last line of the file rather than set beside the sentence it
   corrects, and that sentence was amended in place without
   changing its line count. Any insertion above it moves every
   anchor below the insertion point - this repository has had
   to repair that class of damage once already, and the repair
   note is in the design doc. Appending at EOF moves none.
   ========================================================= */


/* =========================================================
   Tail note 2: the canonical split in section 21
   (appended 2026-09-20; zero declarations)

   Section 21.C printed, under a table of seven view rows:
     "i.e. 5 of the 8 routes already have one and 3
      deliberately do not."
   Both halves are true - five routes do carry one and three
   are an explicit null - and the sentence is still wrong,
   because it counts ROUTES while sitting under a table of
   VIEWS. Seven rows are listed. The eighth pending file is a
   partial with no controller of its own, and by this same
   paragraph's own rule it belongs in neither column: it is
   rendered inside two hosts, and both of those hosts sit in
   the "nothing today" group, so it emits nothing. A reader
   who counts seven rows and then meets a sentence about eight
   cannot tell which of the two numbers is the wrong one - and
   the row such a reader is likeliest to doubt is the one that
   gives /news its canonical today. That is the same shape as
   the wrong figure the first tail note repairs: not a false
   count, a count whose unit is not the unit of the table
   under it.

   The sentence now names the partial instead of counting it.

   Section 21 is the one section here that is a census rather
   than a plan, so its readings were re-run against the
   working tree this round. All of them reproduced: the
   per-file counts for the eight pending files, in both
   directions of the two-prefix membership test (the prefixes
   are deliberately not spelled out here, for the reason the
   first tail note gives); the eleven-template set,
   both ways round; the three variant groups, including the
   token only one of the eleven declares and both body
   line-height exceptions; all seven controller anchors; the
   canonical split itself, five against three; the fourteen
   selectors behind section 19's bare-name paragraph; the
   thirty-one at-rule count and its one non-width entry;
   the five tab-group files with all ten of their line
   anchors; and the three clearfix sites, only one of which
   carries a float. Nothing else in section 21 changed.

   One measurement in this file still has no stated unit: the
   image count in the opening notes. It is not a defect today,
   because the two candidate rulers agree at 24 on the current
   tree and the four per-file anchors printed under it sit on
   distinct lines. It stops being harmless the first time two
   of those elements share a line - the total and the four
   anchors would still agree with each other, and disagree
   with the page.

   Placement, as in the first tail note: appended after the
   last line, and section 21's sentence was rewrapped to the
   same line count, so that no anchor below it moved.
   ========================================================= */


/* =========================================================
   Tail note 3: the counting axis of the section 21 census
   (appended 2026-09-21; zero declarations)

   Section 21 opens its census with the word "Occurrences".
   The eight numbers printed under it - 47 / 76 / 44 / 18 /
   23 / 15 / 37 / 4 - are not occurrences. They are lines.

   Both axes, re-run over the same eight files and the same
   two-character prefix on the same day:
     lines       47  76  44  18  23  15  37   4
     occurrences 51  84  49  19  24  19  45   4
   They disagree in seven of the eight files and always in
   the same direction - a line can carry the prefix more
   than once, never fewer than once. The one file where the
   two agree is the one carrying it once in total.

   Why every later re-run inherited the word: the numbers
   were chosen as a set membership test - a file is news-v2
   markup or it is not - and both axes answer that question
   identically (all eight non-zero, all eleven templates
   zero). The re-runs re-measured the numbers and never the
   axis, so the axis was never the subject of a measurement.

   The section 20 keys printed earlier in this file are the
   contrast and are worth reading next to this note: each
   names its own unit. One of them counts occurrences and is
   also numerically safe today, because no line carries that
   key twice on the current tree. The neighbouring figure is
   not safe: it reads 8, and only four lines in the whole
   views tree carry that class - so on this file's third
   axis, elements, the two per-file anchors under it and the
   figure above it do not describe the same seven classes.
   The first tail note already ruled that no count of that
   key may be carried into a later round. Its unit is
   written down, which is the one thing the census lacked, so
   a reader who disagrees can at least see the disagreement.

   Section 19 counts a third way: its opening figure is an
   element count printed above per-file line anchors. That
   pairing is recorded in the design notes and holds only so
   long as one line carries one element. The census here is
   the mirror image of that hazard - an element count over
   line anchors there, a line count called occurrences here.

   The word is corrected in place, and this note was appended
   after the last line so that no anchor below section 21
   moved. The eight numbers are unchanged and stay correct:
   on the line axis they were and are exact, and as a set
   membership test they were and are exact. What changes is
   only what a reader is told they measure.
   ========================================================= */


/* =========================================================
   Tail note 4: the structural check nobody had run, and the
   section 25 inventory re-taken  (appended 2026-09-22;
   zero declarations)

   Tail note 3 corrected a counting axis. This note answers a
   different question, and it is the one the repository's
   static checker was written for and never ran: does this
   sheet parse at all? No browser and no CSS parser was
   available here, so what follows is a structural count
   rather than a parse. It rules out the class of error that
   stops a stylesheet dead on arrival. It says nothing about
   whether the rules are right.

   Block delimiters: 67 openers against 67 closers. Balanced.
   Brace characters: 233 open against 230 close. The gap is
   three, and it is not a defect - it decomposes exactly into
   the places this file quotes its own subject matter.
     prose open braces   4
       line 224 quotes the news-v2 rule at the date row
       line 812 quotes the root token plus an open brace
       line 866 the same token, without the leading colon
       line 1234 names that same grep
     prose close braces  1
       line 224 again, closing the same quotation
   Subtract them and the sheet stands at 229 against 229.
   The other direction is clean as well: exactly six lines
   carry a close brace with no open one on the same line, at
   377, 384, 402, 552, 761 and 777, and all six are the
   closers of the six media blocks opened at 372, 378, 385,
   541, 758 and 762.
   That is why this note names the prose sites instead of
   "correcting" the raw figure: the imbalance is deliberate,
   and a later reader re-running the count must subtract it
   rather than chase it - the same instruction section 23
   gives for at-rule names and tail note 3 gives for its own.

   Section 25's inventory, re-run over the working tree
   2026-09-22. All of it reproduces:
     list elements across the eight targets         36
       channel/index.php 4, news/channel.php 11,
       news/category.php 8, news/view.php 9,
       news/_side_common.php 4, and zero in expo.php,
       news/tag.php and news/tagid.php
     of those, carrying the class section 5 maps     35
     the one that does not                     news/view.php:47
     the orphan list item's parent         the div at line 91
     nearest list element above it         opens 77, closes 81
     clearfix sites                                    3
     float declarations inside them                    1
     list-style resets in this sheet                   5
       at 212, 250, 515, 694 and 708 - the five the
       section itself names
     the descendant rule that dies         news-v2.css:12
   Worth separating from the rest: that 36 is both 36 lines
   and 36 occurrences. The section does not claim it, and a
   future edit putting two list tags on one line would break
   the agreement silently - the same exposure tail note 2
   records one paragraph below for the image count.

   Tail note 2's predicted failure, tested and not fired
     Tail note 2 records that the image count in the opening
     notes has no stated unit, and that its two candidate
     rulers would agree with each other and disagree with the
     page the first time two image tags share a line. Tested
     2026-09-22: they still agree, per file as well as in
     total. Per-file lines read 2/4/6/3/4/1/2/2 and sum to 24;
     per-file occurrences read those same eight numbers and
     sum to 24. No line carries two. The four anchors printed
     under the figure - news/channel.php lines 29, 81, 111 and
     146 - all reproduce, and each is one construct: an anchor
     wrapping a single image whose missing-thumbnail fallback
     is the one shared placeholder path. The hazard stands as
     a prediction and has not happened yet, so the figure is
     still safe to quote and still not safe to inherit blindly.

   One wording matter, recorded and not corrected
     The same paragraph says the failed-image attribute is
     "Counted repo-wide ... exactly twice" and names both
     sites: partials/v2top.php line 78 and homev2/a.php line
     257. Both anchors reproduce exactly, and the eight pending
     views carry none, which is what the paragraph is arguing.
     The count is true. The adjective is the loose part: it is
     a count over this site's own source tree, not over this
     git repository, which also holds the old site and picks up
     many more. Nothing moves and the member set is printed
     beside the figure, so this is a note for the next
     re-runner rather than a defect - but a reader who takes
     the adjective literally will get a far larger answer and
     may conclude the figure drifted.

   Verification and scope of this section
     Zero declarations added. Like sections 23 and 25, the
     body of this note quotes no brace character and no block
     delimiter, so each raw count moves by exactly the pair
     that opens and closes it: delimiters go from 67 to 68,
     and the brace pair is unmoved at 233 and 230. The body
     also declines to spell out two tokens it discusses: it
     says "block delimiters" and "failed-image attribute"
     rather than writing them, so a later count of either is
     unmoved by this note. That is deliberate and it is the
     opposite of what a first draft of this paragraph claimed
     - the draft asserted the note added a mention, then
     measured itself and found it did not. Recorded because
     the same self-measurement is what tail note 3 did, and
     because a reader who wants to count those tokens will
     find this note does not stand in the way.
     Every figure above is a read-only grep over the working
     tree, and the per-file lists are printed so a reader can
     re-run them one file at a time. Nothing links this sheet,
     so the blast radius is still zero, and this section
     changes no behaviour.
   ========================================================= */
