/* THE PRODUCT-PAGE LAYER.
 *
 * WHAT THIS FILE USED TO BE. Seven rules /ctrl-shift and /publol had
 * byte-identical between them -- `.hero .herohead`, `.hero .sublogo`,
 * `.hero h1 a`, `.hero p`, `.hero .meta`, `.item h3 a`, `.item:last-child`
 * -- extracted on 2026-08-16 (docs/FRONTEND-ASSET-CONSOLIDATION.md,
 * section A) because two pages agreeing by hand is two chances to stop
 * agreeing.
 *
 * THEY WERE DELETED ON 2026-08-17, when the last page that used them was
 * ported. Both pages are on the .pp-* layer below and neither renders a
 * `.hero` or an `.item` any more, so those seven rules styled nothing while
 * still shipping to every reader of a product page. Three other pages do
 * use a class called `.hero` -- index, article, site_access -- and none of
 * them links this file; leaving the rules here would have meant a
 * stylesheet whose only remaining effect was to look like it had one.
 *
 * External file for the CSP reason edition-states.css and auth-forms.css
 * give: `style-src 'self'` covers a same-origin stylesheet in BOTH the
 * enforced and the report-only policy (frontend_app/main.py:_build_csp,
 * re-checked 2026-08-16).
 */


/* ==========================================================================
   THE PRODUCT-PAGE LAYER -- ingested from the approved mockups, 2026-08-17
   ==========================================================================

   SOURCE: design/mockups/Daily Click-through.dc.html and
   design/mockups/pubLOL.dc.html, both edited and approved by Sudeep. Every
   value below is READ OFF THOSE FILES. Nothing here is invented, and where
   the two mockups disagreed the difference became a variable rather than a
   judgement call.

   WHY THIS EXISTS. Put the two mockups side by side and they are the same
   page: eyebrow, subtitle, a tinted lead panel with a 52px product logo and
   a 420px hero, a section rule, a list of stories, an ad slot, a yellow
   promo box. What actually differs is THREE THINGS:

     1. the accent colour        -- yellow on /daily, pink on /publol
     2. the lead panel's tint    -- ivory #FFFFF0 vs pink #FCE4EE
     3. how stories are listed   -- a 3-up card grid vs numbered rows

   Only (3) is structure. (1) and (2) are two custom properties. So the
   layer is written once and each page declares its identity in two lines,
   which is the difference between a design system and four pages that
   currently agree.

   HOW A PAGE OPTS IN:

       <div class="pp" style="--accent:#EE2B6C;--pp-lead-bg:#FCE4EE;
                              --pp-lead-border:#F3C7D9">

   --accent DEFAULTS TO YELLOW and the tint to ivory, which is /daily's
   treatment -- so a page that declares nothing gets the house style rather
   than an unstyled one. Same discipline as the vertical underlines in
   homepage.css: the default must be the common case, because the day
   somebody adds a fifth product page they will forget the variable.

   ADDITIVE ON PURPOSE. Every class here is new (.pp-*). Nothing above this
   comment changed, so /ctrl-shift and /publol render exactly as they did
   until their markup asks for these classes. A stylesheet that restyles a
   live page the moment it is linked cannot be reviewed one page at a time.
   ========================================================================== */

/* THE CONTENT COLUMN'S PADDING, and it belongs here because it went
   MISSING otherwise. Every product page wrote `.pad{padding:26px 30px 40px}`
   in its own <style> block; all four blocks were deleted in the port and
   nobody moved this rule, so the eyebrow and dateline rendered flush to the
   viewport edge while the lead card -- which has padding of its own -- did
   not. Sudeep, 2026-08-17, on /daily: "Alignment and top eyebrow?"

   It is on `.pp` rather than on `.pad` deliberately. `.pad` is a generic
   name several other pages also use with their own values; scoping the
   padding to the product-page layer means the layer carries everything a
   product page needs and cannot lose a rule to a deletion somewhere else
   again. */
.pp{padding:26px var(--space-3xl) 40px}
@media(max-width:640px){.pp{padding:18px var(--space-lg) 30px}}

/* ===== EACH PRODUCT'S IDENTITY, DECLARED HERE AND NOWHERE ELSE =====
 *
 * Sudeep, 2026-08-17: "Dont make page level hard coded changes please."
 *
 * These four blocks used to be inline `style="--accent:#EE2B6C;..."` on each
 * page's wrapper. That was the layer's original design and it was wrong in
 * the way that matters: it put four brand colours in four files, so "what
 * colour is pubLOL" had four possible answers and changing one meant
 * knowing which page to open. A page now says WHICH PRODUCT IT IS --
 * `class="pp pp--publol"` -- and this file says what that means. The page
 * carries no hex at all.
 *
 * THE DEFAULTS ARE /daily's, on `.pp` itself, so a fifth product page looks
 * like us before anyone has chosen its colour.
 *
 * --pp-eyebrow-color IS SEPARATE FROM --accent ON PURPOSE, and the reason is
 * the bug that prompted this. The two mockups disagree:
 *
 *     Daily Click-through.dc.html   color:#001234   (navy)
 *     pubLOL.dc.html                color:#EE2B6C   (its accent)
 *
 * and reading `var(--accent)` for both made /daily's eyebrow YELLOW on a
 * near-white page -- about 1.5:1, which is not a contrast failure so much as
 * invisible text. The rule underneath the disagreement is not "these two
 * products differ", it is "the accent is only usable as TEXT when it is dark
 * enough to be read". Navy is the default because it is always safe; a
 * product whose accent is legible opts in. */
.pp{--accent:var(--yellow);--pp-lead-bg:var(--ivory);--pp-lead-border:var(--line);
    --pp-eyebrow-color:var(--navy)}

/* /daily -- the defaults above, named so the markup can say what it is
   rather than relying on absence to mean something. */
.pp--daily{}

/* /publol. Pink accent, soft pink panel with its own slightly warmer
   border. --pp-eyebrow-case:none IS LOAD-BEARING: "pubLOL" is stylised
   brand casing and the layer uppercases the eyebrow by default, which
   would render it "PUBLOL" and destroy the mark (DESIGN.md section 7).
   Its lead headline is 25px where /daily's is 21px. */
.pp--publol{--accent:#EE2B6C;--pp-lead-bg:#FCE4EE;--pp-lead-border:#F3C7D9;
  --pp-eyebrow-case:none;--pp-eyebrow-color:var(--accent);--pp-leadsize:25px}

/* /ctrl-shift. Blue accent on a soft blue panel, and a 2px lead border
   where the other three use 1px -- that weight is this product's own. */
.pp--ctrl-shift{--accent:#2E6FB8;--pp-lead-bg:#E7EAF3;--pp-lead-border:#2E6FB8;
  --pp-lead-bw:2px;--pp-eyebrow-color:var(--accent)}

/* /feature. Takes the house defaults -- it has no tinted lead panel of its
   own, being one long article rather than a list. */
.pp--feature{}

/* The dateline. Mono, and in the PRODUCT'S accent rather than in navy --
   it is the one place per page where the product says which product it is,
   and on /publol the mockup makes it pink. */
/* text-transform is a VARIABLE because one product's name is not plain caps.
   "pubLOL" is stylised brand casing and forcing it through uppercase renders
   it "PUBLOL", which destroys the mark. A page whose name must survive sets
   --pp-eyebrow-case:none. See design/DESIGN.md section 7 -- never tidy away a
   brand element to make a rule uniform. */
/* COLOUR IS A VARIABLE BECAUSE THE TWO MOCKUPS DISAGREE, and I collapsed
   them into one rule the first time round -- which is exactly the
   improvisation the port was meant to avoid.

     Daily Click-through.dc.html   color:#001234   (navy)
     pubLOL.dc.html                color:#EE2B6C   (its accent, pink)

   Reading `var(--accent)` for both made /daily's eyebrow YELLOW on a
   near-white page: roughly 1.5:1, which is not a contrast failure so much
   as invisible text. The default is now navy -- the safe, legible case --
   and a product whose mockup gives its eyebrow the accent asks for it by
   name. Sudeep, 2026-08-17: "Alignment and top eyebrow?" */
.pp-eyebrow{font:var(--type-label);letter-spacing:.1em;
  text-transform:var(--pp-eyebrow-case,uppercase);
  font-weight:600;color:var(--pp-eyebrow-color);margin:0 0 var(--space-3xs)}
/* The standfirst under it stays --muted on both mockups, NOT the accent and
   not the caption grey: it is a full sentence of cadence copy, not meta. */
.pp-sub{font:var(--type-caption);font-family:var(--mono);color:var(--muted);margin:0 0 var(--space-md)}
/* "· Edition 7", AND THE ONE RULE IT HAS TO OBEY: never start a line.
   The cadence line is longer than a phone column, so it wraps -- and at
   375px it wrapped on the space before this last separator, leaving
   "· Edition 7" alone on a third line beginning with a dangling middot.
   Sudeep reported exactly that on 2026-08-23 on /daily.
   nowrap keeps the label and its number together; the non-breaking space
   the macro emits in front of the separator is the other half, and stops
   the separator being able to lead a line at all. Both are needed -- see
   pp.header in _product_page.html, which explains why each alone is not
   enough. No font declaration: it is the same line, in the same type. */
.pp-sub-ed{white-space:nowrap}

/* BACK / FORWARD THROUGH THE PUBLISHED RUN (2026-08-21). Sits directly
   under the dateline it pages, because the thing being changed is the date
   on the line above -- put at the foot of the page it would read as
   pagination through the CURRENT edition's stories instead.
   The whole nav is absent when there is fewer than two published issues, so
   there is no empty-state styling to write.

   IT IS ONE CONTROL, NOT THREE THINGS IN A ROW (2026-08-23). Sudeep, on
   /daily on a phone: the pager "shows a hyperlink" and sits on a line of
   its own under the tagline. Two separate faults, and the first was not
   this file's -- base.html's `nav a` rules were repainting the arrows in
   masthead ink and growing a yellow underline under them on hover, because
   this pager is a <nav> and so was the masthead. That selector is anchored
   to #mainNav now.

   What IS this file's is the second half: three loose boxes with a 10px gap
   between them read as a row of items, and on a phone a row of items that
   is not obviously a control reads as links. So the pieces are drawn INSIDE
   one pill -- the border and the ground belong to the nav, the arrows and
   the position sit flush within it, and hairlines divide them. The same
   segmented-control vocabulary .hp-chip uses on the homepage: mono label,
   999px, hairline on paper.

   display:inline-flex, not flex. As a block it stretched the full column
   and its own edges were invisible, which is most of why it did not read as
   a control; now it hugs its three parts and is unmistakably one object.
   It still sits on its own line -- there is nothing beside it to sit next
   to, the dateline being a separate macro -- but a 132px pill on a line is
   a control, where a full-width row of loose glyphs was not.

   NOT overflow:hidden to clip the arrows to the pill's radius, tempting as
   that is: an outline is painted outside the border box, so clipping the
   children would clip the focus ring off the two elements on this page most
   likely to be reached by keyboard. The arrows carry their own radius
   instead. */
.pp-arch{display:inline-flex;align-items:stretch;margin:0 0 var(--space-md);
  border:1px solid var(--line);border-radius:999px;background:var(--surface)}
/* 36px, not the 44px touch floor: these are secondary navigation sitting
   beside body text, and the pair plus its label already spans a comfortable
   target area. The hit area is the whole square, not the glyph. */
/* Takes a type ROLE rather than a hand-picked pixel size. 17px looked right
   for the glyph and would have been a seventeenth font size on a page that
   has eight roles -- which is the exact drift
   test_no_page_stylesheet_declares_a_font_size_outside_the_scale exists to
   stop. The lead role is the nearest thing the scale has to "a little
   larger than body", and it reads identically here. */
/* border:0 is load-bearing, not tidiness: the arrows used to carry the
   pill's outline themselves, and leaving it would draw a box inside a box. */
.pp-arch-btn{display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:36px;border:0;border-radius:999px;
  font:var(--type-lead);line-height:1;color:var(--navy);text-decoration:none;
  background:transparent}
.pp-arch-btn:hover{background:var(--sunk);color:var(--navy)}
.pp-arch-btn:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
/* The end of the run. Dimmed and NOT a link -- see the macro's note on why
   this is a span rather than a disabled anchor. */
.pp-arch-off{opacity:.32;background:transparent}
/* No font-family here: the label role IS mono (design system rule three --
   "mono is labels only"), so naming it again would be a page stylesheet
   restating the scale, which is what the type guard forbids.
   The hairlines are what make the three parts read as segments of one
   control rather than as a caption between two buttons, and nowrap stops
   "4 of 7" folding in the middle of the pill on a narrow phone. */
.pp-arch-pos{display:inline-flex;align-items:center;white-space:nowrap;
  padding:0 var(--space-sm);border-left:1px solid var(--line);
  border-right:1px solid var(--line);
  font:var(--type-label);color:var(--muted);letter-spacing:.08em}

/* THE EDITOR'S LEDE. /daily is the only product that writes one today, and
   it is NOT in the mockup -- the mockup was recreated from the live page
   before the field existed. Sudeep's ruling on exactly this shape ("Maintain
   the article level section# drop you created. That was really good; it may
   not be part of the mockup I shared") is why it is ported rather than
   dropped: absent from a mockup is not the same as rejected.

   It lives in the LAYER rather than in daily.html because the alternative is
   a <style> block on a ported page, and one surviving block is how the four
   product pages became four designs the first time. 60ch is a measure, not a
   width -- it stops the lede running the full content column at desktop,
   where a serif line of 100+ characters stops being readable. */
.pp-intro{font:var(--type-lead);color:#3a3a38;margin:0 0 var(--space-lg);max-width:60ch}

/* THE LEAD PANEL. The one tinted box on the page, and the tint is what
   makes a product page feel like that product before a word is read. */
.pp-lead{background:var(--pp-lead-bg);border:var(--pp-lead-bw,1px) solid var(--pp-lead-border);
  border-radius:12px;padding:var(--space-xl) var(--space-2xl);margin:0 0 26px}
.pp-leadhead{display:flex;align-items:center;gap:var(--space-md);margin-bottom:var(--space-md)}
/* 52px is the sublogo PNGs' NATIVE size -- see .hero .sublogo above. Same
   rule, same reason, and it must never be scaled up. */
.pp-sublogo{height:52px;width:auto;display:block;flex:0 0 auto}
.pp-kicker{font:var(--type-label);letter-spacing:.6px;
  text-transform:uppercase;color:var(--color-text-caption);margin:0 0 7px}
/* Serif 500 -- the ruling's "headlines serif weight 500". 21px is /daily's
   lead; /publol's is 25px and sets --pp-leadsize to say so. */
.pp-leadhl{font:500 var(--pp-leadsize,21px)/1.3 var(--serif);margin:0 0 7px;letter-spacing:-.3px}
.pp-leadhl a{color:var(--navy);text-decoration:none}
.pp-leadhl a:hover{text-decoration:underline;text-decoration-color:var(--accent)}
/* The hero: the ruling's fixed 420px, and radius 8 rather than the panel's
   12 so the picture sits INSIDE the box rather than tracing it. */
.pp-hero{width:100%;height:420px;min-height:420px;object-fit:cover;
  border-radius:8px;display:block;margin-bottom:var(--space-md)}
.pp-leadsum{font:var(--type-body);color:#3a3a38;margin:0}
/* THE EDITOR'S NOTE (cp-340 / tfc-208). Rendered by the lead and card
   macros directly after the summary, only when the article carries one.
   Values are the approved mock's (EM SPEC - Editor's Note.md, step 5). */
/* THE NOTE IS ONE STEP BELOW ITS SUMMARY, and sits on the steel ground with the
   6px yellow rule (cp-340). .pp-leadsum is --type-body, so the lead note
   is --type-body-sm; .pp-cardsum is 14px, so the card note is
   --type-caption at 13px. Before this it MATCHED the summary on the lead
   and the card and was LARGER than it on the vertical rows and homepage
   cards, so the two read as one block of equal weight -- Sudeep,
   2026-09-04: "I want the text size to not compete with the 35 word
   summary either." */
/* --steel IS --silver LAID OVER WHITE AT 28%, and the only COOL ground the
   site has. Tokenised 2026-09-19 (cp-346) as primitive.color.slate.100
   under the role color.surface.note; --steel is that role's legacy alias
   in base.html. The full note is in homepage.css beside .hp-note. */
.pp-leadnote,.pp-cardnote{font:var(--type-body-sm);font-weight:300;color:#3a3a38;margin:14px 0 0;
  padding:12px 16px;border-left:6px solid var(--yellow);background:var(--steel);
  border-radius:0 3px 3px 0;max-width:74ch}
.pp-note-label{display:block;font:var(--type-label);letter-spacing:.06em;text-transform:uppercase;color:#6b6b66;margin-bottom:4px}
.pp-meta{font:var(--type-label);color:var(--color-text-caption);margin-top:9px}

/* THE SECTION RULE takes the accent at 2px. Deliberately thinner than the
   homepage's 3px yellow section rule: a product page has many of these and
   the homepage has three. */
.pp-sec{font:var(--type-caption);font-weight:600;letter-spacing:.04em;text-transform:uppercase;
  color:var(--navy);margin:0 0 var(--space-sm);padding-bottom:var(--space-2xs);
  border-bottom:2px solid var(--accent)}
.pp-sec a{color:inherit;text-decoration:none}
.pp-sec a:hover{text-decoration:underline;text-decoration-thickness:2px;text-decoration-color:var(--accent)}

/* STORY CARDS -- /daily's shape, and its column ladder is the mockup's,
   not a rule of my own.

   THIS WAS auto-fit minmax(240px,1fr) UNTIL 2026-08-17 and that was drift.
   The reasoning attached to it -- "the mockup carries its own phone
   breakpoint, minmax solves every width instead of one" -- was wrong on the
   facts twice over: the mockup carries TWO breakpoints (820px to two
   columns, 640px to one), and inside this content column auto-fit resolves
   to FOUR cards across at desktop where the mockup and the live page both
   say three. A grid that quietly adds a column is not a more flexible port
   of a design, it is a different design.

   Sudeep: "DO NOT IMPROVISE OR RECREATE OR GET CREATIVE." The ladder below
   is design/mockups/Daily Click-through.dc.html lines 17-18 and 83. */
.pp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-sm)}

/* A SECOND DENSITY, and only because a section genuinely holds a different
   number of things. /ctrl-shift's columns row is "one per vertical" -- five
   short cards, never three -- so three fixed columns would leave two
   orphaned on a second line every fortnight. auto-fill is right HERE and
   wrong on .pp-grid above, and the difference is worth stating: /daily's
   grid renders a capped three stories per vertical and the mockup fixes it
   at three, while this one renders however many verticals exist. */
.pp-grid--wide{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}
@media(max-width:820px){.pp-grid{grid-template-columns:1fr 1fr}}
@media(max-width:640px){.pp-grid{grid-template-columns:1fr}}
.pp-card{box-sizing:border-box;background:var(--surface);border:1px solid var(--line);
  border-radius:10px;padding:13px var(--space-md)}
.pp-cardhl{font:500 16px/1.35 var(--serif);margin:0 0 var(--space-3xs)}
.pp-cardhl a{color:var(--ink);text-decoration:none}
.pp-cardhl a:hover{color:var(--navy);text-decoration:underline;text-decoration-color:var(--accent)}
/* Four lines, per the ruling. The browser draws the ellipsis. */
.pp-cardsum{font:var(--type-body-sm);font-size:14px;color:#4a4a48;margin:0;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;line-clamp:4;overflow:hidden}
.pp-cardnote{font:var(--type-caption);padding:10px 14px;margin-top:10px}
.pp-byline{font:var(--type-label);color:var(--color-text-caption);margin-top:9px}

/* THE VERTICAL'S BANNER, between a section rule and its cards
   (db/migrations/041). Capped rather than fixed-height like .pp-hero: this
   is a section divider, and five 420px images down one page would be a
   slideshow. Absent -> nothing renders; five empty frames is not a page.

   aspect-ratio RESERVES THE BOX BEFORE THE PICTURE ARRIVES, and that is
   what fixes Sudeep's 2026-08-23 report: "The Chip loads and then goes to
   another line". The chip is the "Picture credit" line, which this macro
   puts DIRECTLY UNDER this image (_product_page.html's section_hero).

   The old rule stated a ceiling and no height, so the used height was
   `auto` -- and an <img> with no width/height attributes and no ratio has
   NO intrinsic size until its bytes arrive. Measured at 375px on the
   rendered page: the banner box was 21px tall at first paint (one line of
   alt text) and 229px once loaded, so every credit under one dropped 207px
   and 416px down the page after load. In the window before that the credit
   sat immediately under the section rule with no picture above it, which
   is the second half of the same report -- it reads as a loose hyperlink
   rather than as a caption, because at that moment it IS one.

   1200/630 is not a number chosen here: it is the platform's declared hero
   spec, already carried by .hp-lead-img on the homepage, by .pp-heroslot
   below and by .pp-caro-img, and it is what the newsletter templates print
   in their editor-facing placeholder. Stated as a ratio rather than a
   pixel height ON PURPOSE -- a fixed height would reserve the wrong box at
   every width except the one it was measured at, and max-height:280px is
   still the ceiling review_notes 143 settled, so nothing changes at tablet
   or desktop where the cap is already what decides the height. Below about
   533px the ratio decides instead, object-fit:cover crops to it exactly as
   it already cropped, and the box is the same size before and after load. */
.pp-sechero{width:100%;aspect-ratio:1200/630;max-height:280px;
  object-fit:cover;border-radius:10px;
  display:block;margin-bottom:var(--space-md)}

/* THE MISSING-HERO PLACEHOLDER, and it is deliberately opt-in. The lead
   panel renders NOTHING for a missing image by default -- a dashed box
   announces a gap to a reader who cannot fix it. /daily asked for it
   explicitly (Sudeep, review_notes id 3) because its must-read card is the
   page's single focal point; it STOPPED on 2026-09-04 (F10 / tfc-208),
   because on the public page the frame read as "under construction" to an
   AdSense reviewer, and a lead without a picture now sets typographically
   like the homepage's. The same day (cp-344) the rule reached the rest:
   /ctrl-shift's lead no longer passes placeholder=True (review note 17,
   reversed), /feature's own else-branch frame is gone, and /article's
   local .heroslot copy with it. NO PUBLIC PAGE PASSES placeholder NOW.
   The rule and the parameter stay for a Control Panel preview that asks
   for the frame deliberately; nowhere else, and nowhere new without
   being asked. */
.pp-heroslot{width:100%;aspect-ratio:1200/630;border:2px dashed rgba(0,18,52,.35);
  border-radius:8px;display:flex;align-items:center;justify-content:center;
  font:var(--type-label);letter-spacing:.5px;
  color:var(--navy);margin-bottom:var(--space-md);background:var(--sand)}

/* ===== LANDING ON THE STORY THE READER CLICKED =====
 *
 * MOVED HERE FROM daily.html ON 2026-08-17, unchanged. Sudeep, on the
 * homepage deep links (review_notes 137/138): "it scrolls up and lands on
 * top for me to click again... Nobody likes to scroll again looking for a
 * needle in the haystack." design/DESIGN.md records the anchors and this
 * highlight as MUST-KEEPS through any port, which is why they moved into
 * the layer rather than being left behind with the page's <style> block.
 *
 * scroll-margin-top is not optional and was checked, not assumed:
 * base.html's .mast is position:sticky top:0, so without it the browser
 * scrolls the target to y=0 and the masthead covers it -- the reader lands
 * on the story and cannot see it, which reads as the WRONG story and is
 * worse than not scrolling at all. The mast is 22 + 72 + 22 = 116px on
 * desktop and 22 + 40 + 22 = 84px below 1024px where the logo shrinks
 * (base.html's own media query). 128/96 leaves ~12px of air above the card
 * so it reads as landed-on rather than jammed under the bar.
 *
 * :target is how the reader knows WHICH one -- ten cards look alike. Yellow
 * rather than an error colour: this is a signpost. It costs a reader who
 * arrived without a fragment nothing.
 *
 * No smooth scrolling and no script. A fragment link is native browser
 * behaviour: nothing here to fail, nothing to block, nothing added to the
 * inline inventory tfc-70 is clearing. */
.pp-lead,.pp-card{scroll-margin-top:128px}
@media(max-width:1024px){.pp-lead,.pp-card{scroll-margin-top:96px}}
.pp-lead:target,.pp-card:target{border-color:var(--navy);box-shadow:-3px 0 0 0 var(--yellow)}

/* NUMBERED ROWS -- /publol's shape. The number is the accent's other job. */
.pp-row{display:flex;gap:var(--space-lg);padding:15px 0;border-bottom:1px solid var(--line)}
.pp-row:last-of-type{border-bottom:none}
.pp-num{font:var(--type-caption);font-family:var(--mono);font-weight:600;color:var(--accent);
  padding-top:2px;min-width:20px}
.pp-rowbody{flex:1}

/* THE AD SLOT, identical on both mockups. The "i" is a real disclosure --
   "we never accept ads from competitors or let sponsors influence
   editorial decisions" -- so it is a control with a title, not decoration. */
.pp-ad{background:var(--sunk);border-bottom:1px solid var(--line);padding:var(--space-2xl) var(--space-3xl);
  text-align:center;border-radius:8px;margin:26px 0 0}
.pp-adlabel{font:var(--type-label);color:var(--muted);margin-bottom:var(--space-sm);
  display:flex;align-items:center;justify-content:center;gap:var(--space-3xs)}
.pp-adinfo{cursor:help;display:inline-flex;align-items:center;justify-content:center;
  width:16px;height:16px;background:var(--line);border-radius:50%;
  font-size:10px;font-weight:600;color:var(--navy)}

/* THE PROMO BOX MOVED TO components.css (2026-08-17).

   It was declared here because the three product pages were its only
   callers. design/mockups/Home.dc.html has the same box -- same title,
   same sentence, same "Refer a friend ->" -- at the foot of the homepage,
   and the homepage does not load this stylesheet. Copying ten rules into
   homepage.css would have made a second definition of a box whose whole
   point is that it is the brand asking in one voice on every page.

   The four rules moved VERBATIM under the same class names, so the three
   product pages are untouched: base.html links components.css on every
   page, product pages included. Look for `.pp-promo` there. */


/* CHIPS INSIDE THE LEAD PANEL AND THE ROWS. _chips.html emits the markup and
   has always required its caller to style it -- which is why five pages each
   grew their own near-identical copy. These are the mockups' values, declared
   once, so a page that uses this layer inherits them and deletes its own. */
/* CHIPS, scoped to the whole product page rather than to its three
   containers. They were `.pp-lead .chip, .pp-row .chip, .pp-card .chip` --
   one declaration listed three times, because at the time those were the
   only three places a chip could appear. /feature has a fourth (its article
   header), and the honest reading of "list every container" is that the
   list was standing in for "anywhere on a product page". A chip looks the
   same everywhere here; that is the rule, so it is written as the rule. */
.pp .chips{display:flex;flex-wrap:wrap;gap:var(--space-3xs);margin-top:9px}
/* THIS RULE IS WHY cp-456 DID NOT FIX THE PAGE IT WAS REPORTED ON.
   Sudeep, 2026-09-11: "Public Policy Section is extended towards the right",
   and again on 2026-09-14 after the fix deployed: "This frontend UI issue
   where the Public Policy section is wider than other sections including the
   header still persists ... DAILY PAGE ONLY".

   cp-456 gave `.chip` in components.css max-width:100%, white-space:normal
   and overflow-wrap:break-word, so one 368px entity name wraps instead of
   pushing the document sideways. That rule is specificity (0,1,0). THIS one
   is (0,2,0) and set white-space:nowrap, so on every page carrying `.pp` --
   /daily among them -- the fix was overridden and never applied.

   THE SECTIONS ARE NOT UNEQUAL, AND A FIX AIMED AT MAKING THEM EQUAL WOULD
   BE AIMED AT NOTHING. Measured live at 375 on /daily?date=2026-09-11: all
   five section headings are 343px, identical, and no section box exceeds
   the viewport. What actually happens is that ONE chip -- the entity "New
   York City Department of Consumer and Worker Protection", 58 characters,
   403px in a ~329px column -- pushes document.scrollWidth to 449 against a
   375 viewport. The page gains 74px of horizontal scroll, and the region
   holding the offender reads as wider than its neighbours. The next widest
   chip anywhere on that page is 231px and comfortably inside, so this is a
   single offender widening the document, not a layout floor and not an
   unequal grid. Sudeep's perception was right and his diagnosis of it was
   not; both are recorded because only one of them is actionable.

   IT WAS VERIFIED ON A PAGE THIS RULE DOES NOT REACH. The measurement that
   declared cp-456 fixed was taken on /vertical/public-policy, whose wrapper
   is `.pad` with no `.pp`, so the base rule won there and overflow really
   did go to zero. The Daily has a Public Policy SECTION and the vertical
   page IS Public Policy; the two read alike in a bug report and are
   different surfaces. Right answer, wrong page.

   AND THE EXCLUSION WAS WRITTEN DOWN AS DELIBERATE. components.css still
   carried a note saying `.pp .chip` "stays where it is ... so the four
   product pages are unchanged by this section" -- I saw this override while
   making the fix, reasoned about it, and filed it as a feature instead of
   asking whether the fault was on those pages too. A documented exception
   is not a checked one.

   THE UPPERCASE, THE TIGHTER PADDING AND THE PILL RADIUS ARE UNTOUCHED.
   Only the three wrapping declarations change, and they are invisible until
   a chip is wider than its container -- which is exactly the case that was
   breaking the page. */
.pp .chip{display:inline-flex;align-items:center;
  font:var(--type-label);text-transform:uppercase;text-decoration:none;color:var(--navy);
  border:1px solid var(--line);border-radius:var(--radius-pill);padding:3px 9px;
  background:var(--sunk);
  max-width:100%;white-space:normal;overflow-wrap:break-word}
.pp a.chip:hover{border-color:var(--navy);background:#EDECE6}
.pp .chip .cdot{width:6px;height:6px;border-radius:50%;margin-right:5px;flex:none}
/* An ENTITY pill is dashed -- it is a thing mentioned, not a section you can
   browse, and the dash is what says so without a second colour. */
.pp .chip.ent{border-style:dashed}

/* The beat whose only story ran as the lead. A sentence, not a card:
 * the section keeps its heading so the page still visibly covers five
 * beats, without printing the same story twice. */
.pp .pp-leadabove{margin:10px 0 4px;color:var(--muted);font:var(--type-body)}
.pp .pp-leadabove a{color:var(--navy);font-weight:var(--w-bold)}

/* THE RESEARCH PILL, which is the only chip here that is an ACTION.
 *
 * Sudeep, 2026-08-27: the cards were rendering every entity the extractor
 * found -- eighteen on one story -- so the tag block ran four to six rows,
 * taller than the 35-word summary above it. The list is capped at three
 * now (see _chips.html) and this is where the rest went.
 *
 * SOLID NAVY, NOT ANOTHER OUTLINE. It sits in a row of pills that all look
 * alike; if it looked like them too, capping the list would just quietly
 * hide twelve entities and offer nothing in their place. Filled is what
 * says "this one does something".
 *
 * YELLOW ON HOVER, NEVER YELLOW TEXT ON WHITE -- that is 1.47:1 and fails
 * every contrast rule we hold ourselves to. Navy behind it is 12.6:1. */
.pp .chip.more{background:var(--navy);color:#fff;border-color:var(--navy);font-weight:600}
.pp a.chip.more:hover{background:var(--navy);color:var(--yellow);border-color:var(--navy)}

@media(max-width:640px){
  .pp-lead{padding:var(--space-lg) var(--space-lg)}
  /* The mockup drops its lead headline to 21px on a phone. */
  .pp-leadhl{--pp-leadsize:21px}
}

/* ===================================================================
 * THE CONTRIBUTOR CAROUSEL (.pp-caro)
 *
 * Sudeep, 2026-08-23: "I do not want other authors to feel left out... a
 * carousel showcasing image of each article and all articles listed as a
 * card below too."
 *
 * BEHAVIOUR IS BORROWED, LOOK IS NOT. The markup drives
 * static/js/homepage-carousel.js through its [data-tfc-*] hooks, so
 * autoplay, hover-pause, prefers-reduced-motion, the dots and the toggle
 * are one implementation rather than two. The CLASSES are pp-*, because
 * reusing .hp-caro-* would have pulled the homepage's visual identity onto
 * a product page -- the exact thing this file exists to keep separate.
 *
 * PROGRESSIVE ENHANCEMENT, SAME CONTRACT AS THE HOMEPAGE. Without the
 * script this is a plain vertical stack of figures: every picture visible,
 * every headline a link, nothing hidden. The script's first statement puts
 * `tfc-js` on <html> and the stack becomes a snapping strip. That is why
 * ctrl-shift.html loads it WITHOUT `defer` -- deferred, the reader would
 * watch the section reflow after first paint.
 * =================================================================== */
.pp-caro{position:relative;margin:0 0 22px}
.pp-caro-track{display:grid;gap:14px}
.pp-caro-slide{margin:0;background:var(--pp-lead-bg);
  border:var(--pp-lead-bw,1px) solid var(--pp-lead-border);border-radius:10px;
  overflow:hidden}
/* 1200x630 is the frame the rest of this product's art is cut to, so the
   slide holds that ratio whatever the file's own dimensions are. A
   columnist's portrait and a wide illustration must not resize the strip
   between slides. */
.pp-caro-img{display:block;width:100%;aspect-ratio:1200/630;object-fit:cover;
  background:var(--sunk)}
/* THE HEAD ROW (cp-382). /daily's and /publol's lead shape, drawn on
   this page's slides: the product mark, then the headline, then the
   picture. Same .pp-sublogo at the same 52px and the same serif lead
   headline -- deliberately the existing two roles rather than a third
   size, because Sudeep's objection to the alternative fix was that a
   change here "cannot be special for Ctrl+Shift". */
.pp-caro-head{display:flex;align-items:center;gap:var(--space-md);
  padding:16px 16px 12px}
.pp-caro-cap{padding:14px 16px 16px}
/* IN THE HEAD ROW IT IS THE PAGE'S LEAD HEADLINE and takes the lead's
   type role -- 21px, exactly .pp-leadhl. It keeps the 16px card size
   nowhere else, since this macro has one caller and every slide has a
   head; the base rule stays as the size a caption would want if a future
   caller ever draws one under a picture again. */
.pp-caro-hl{font:500 16px/1.35 var(--serif);margin:0 0 6px}
.pp-caro-head .pp-caro-hl{font:500 var(--pp-leadsize,21px)/1.3 var(--serif);
  margin:0;letter-spacing:-.3px}
.pp-caro-hl a{color:var(--navy);text-decoration:none}
.pp-caro-hl a:hover{text-decoration:underline;text-decoration-color:var(--accent)}

/* Controls are meaningless without the script that drives them, so they do
   not exist until it says so. */
.pp-caro-ctl{display:none}

.tfc-js .pp-caro-track{display:flex;gap:14px;overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}
.tfc-js .pp-caro-track::-webkit-scrollbar{display:none}
/* flex-basis 100%, never a partial "peek": under 100% the last slide can
   never reach its own snap point and the carousel lurches on the final
   press. The arrows and dots are the affordance instead. */
.tfc-js .pp-caro-slide{flex:0 0 100%;min-width:0;scroll-snap-align:start}
.tfc-js .pp-caro-ctl{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:12px 0 0}

.pp-caro-arrow,.pp-caro-toggle{font:var(--type-label);background:var(--surface);
  color:var(--navy);border:1px solid var(--line);border-radius:999px;cursor:pointer;
  line-height:1;padding:0}
/* THE 19px IS NOT TEXT. It sets the height of a single arrow GLYPH
   inside a 34px round button -- nobody reads it, and the label role's
   11px would leave a barely visible mark in the middle of the control.
   The identical case homepage.css's .hp-caro-nav already argued and
   registered; this is the product-page layer's copy of the same
   control, so it is registered by name too, in
   tests/test_type_scale_is_single_source.py's _SIZE_EXCEPTIONS. */
.pp-caro-arrow{width:34px;height:34px;font-size:19px}
.pp-caro-toggle{height:34px;padding:0 15px;letter-spacing:.1em;text-transform:uppercase;
  margin-left:auto}
.pp-caro-arrow:hover,.pp-caro-toggle:hover{border-color:var(--navy);background:var(--sunk)}
.pp-caro-dots{display:flex;align-items:center;gap:2px}
/* 24x24 hit areas (WCAG 2.5.8) around a 9px mark -- the dot a reader sees
   is small, the target they press is not. */
.pp-caro-dot{width:24px;height:24px;padding:0;border:0;background:none;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center}
.pp-caro-dot::after{content:"";width:9px;height:9px;border-radius:50%;
  background:var(--line);transition:background .15s}
.pp-caro-dot[aria-current="true"]::after{background:var(--accent)}
.pp-caro-dot:hover::after{background:var(--navy)}


/* =====================================================================
   THE COLUMN WALL -- one hero and six cards, with the hero changing.
   Plan B, B2.3. Markup: _product_page.html's columns() macro.
   Behaviour: static/js/product-hero-rotate.js.

   THE ACCEPTANCE CRITERION IS A HEIGHT, NOT A LOOK. Sudeep, 2026-09-04:
   "This is to reduce page scroll to between two to three scrolls." Every
   number below is chosen against that budget, and the budget is won or
   lost on a phone -- seven cards stacked single-column exceeds three
   scrolls comfortably, which is why the small-screen rule is a TWO-UP
   grid with the hero as the only full-width element rather than the
   one-column collapse .pp-grid uses at 640px.

   ONE COMPONENT AT TWO SIZES. The hero and the six cards are the same
   .pp-card; .pp-col-hero and .pp-col-card only change its scale. That is
   what lets the promotion be a DOM move plus a class swap rather than a
   re-render -- see the macro's header for why a re-render is the wrong
   mechanism.

   NO TRANSITION ON THE SWAP, and that is deliberate rather than
   unfinished. Two elements exchanging places in a grid cannot be
   cross-faded without absolute positioning, and animating a card into a
   hero's box is precisely the sliding motion Sudeep ruled out ("I am not
   referring to hero with carousel"). An instant exchange is also the
   honest thing under prefers-reduced-motion, where the script does not
   rotate at all.
   ===================================================================== */
.pp-cols{margin:0 0 var(--space-lg)}
.pp-col-heroslot{margin-bottom:var(--space-sm)}
/* The sublogo and the kicker LABEL THE SLOT, so they sit above the column
   inside it and stay put while it changes -- see the macro's note on why
   they are outside the swapped element, and on the brand rule that keeps
   the mark on this page at all. Reuses .pp-sublogo and .pp-kicker from the
   lead panel unchanged; only the row they sit in is declared here. */
.pp-col-herohead{display:flex;align-items:center;gap:var(--space-2xs);
  margin-bottom:var(--space-2xs)}
.pp-col-herohead .pp-kicker{margin:0}
.pp-col-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-sm)}
/* FLAT MODE (cp-377): every column is a card, so the row has to fill
   itself. `repeat(3,1fr)` is right under a hero -- three cards beneath one
   big one -- and wrong when the cards ARE the block: a two-column edition
   would draw two thirds of a row and a hole where the hero used to be.
   auto-fit lets two cards be halves and five be a row of three and a row
   of two.
   DECLARED BEFORE THE BREAKPOINTS ON PURPOSE. The 900px rule below pins
   .pp-col-grid to two-up and must keep winning here -- a card narrower
   than ~260px on a phone is the layout the two-up ladder exists to
   prevent, and auto-fit would happily produce one. */
.pp-col-grid--flat{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
/* THE AVATAR BYLINE ON A CARD (cp-377). byline.css owns .avatar, .info,
   .name and .role and deliberately does NOT own `.byline` itself -- its
   header explains that /article and /feature disagree on the container's
   gap and padding and that converging them is an open design call. So this
   is the card layer's container, declared here like theirs, and the ONLY
   thing it overrides is the size: 32px against their 48px, because this
   sits inside a 260px card rather than under a 3,000-word headline.
   Everything else -- the circle, the sand fill for a contributor with no
   photograph, object-fit, the name and role type -- comes from the shared
   component and must keep coming from there. */
.pp-card .byline{display:flex;align-items:center;gap:10px;
  margin-top:var(--space-2xs)}
.pp-card .byline .avatar{width:32px;height:32px}
/* The hero's card, scaled up: a bigger headline, a picture that is allowed
   real height, and the summary released from the card's four-line clamp
   because at this size the clamp is what makes a hero look like a mistake. */
.pp-col-hero .pp-card{padding:var(--space-md) var(--space-lg)}
/* --fs-subhead IS A CLAMP -- 21px on a phone up to 28px on a wide screen --
   so the hero headline scales with the viewport by itself and this rule
   needs no breakpoint of its own. That is also why it is a token rather
   than the 26px it would otherwise have been typed as:
   test_type_scale_is_single_source refuses a hand-picked size, and the
   reason it refuses one is that a page that picks its own stops moving
   when the scale does. */
.pp-col-hero .pp-cardhl{font:600 var(--fs-subhead)/1.25 var(--serif);margin-bottom:var(--space-2xs)}
/* THE FOUR-LINE CLAMP IS RELEASED AT HERO SIZE. The clamp is what keeps
   six cards the same height in a grid; on the one column that is the
   page's focal point it is what makes a hero look like a mistake. */
.pp-col-hero .pp-cardsum{font-size:var(--fs-body);-webkit-line-clamp:none;line-clamp:none;display:block;overflow:visible}
.pp-col-hero .pp-cardimg{max-height:340px}
/* THE PICTURE, and the ratio is the platform's declared hero spec -- the
   same 1200/630 .pp-sechero, .pp-heroslot, .pp-caro-img and the homepage's
   .hp-lead-img all carry. Stated as a RATIO so the box is reserved before
   the bytes arrive: an <img> with no dimensions and no ratio has no
   intrinsic size until it loads, and the credit line directly beneath it
   would drop down the page after first paint -- Sudeep's 2026-08-23 report
   ("The Chip loads and then goes to another line"), which .pp-sechero
   already carries the full argument for. A card whose picture reflowed
   would be worse here than there, because on this block the layout is
   already moving every ten seconds. */
.pp-cardimg{width:100%;aspect-ratio:1200/630;max-height:180px;object-fit:cover;
  border-radius:8px;display:block;margin-bottom:var(--space-2xs)}

.pp-cols-foot{display:flex;align-items:center;gap:var(--space-sm);flex-wrap:wrap;
  margin-top:var(--space-sm)}
/* Same control as .pp-caro-toggle, and deliberately the same drawing: a
   reader who has learned the pause button on one block should not have to
   learn a different one here. It is NOT the same selector because that one
   carries margin-left:auto, which belongs to the carousel's own row. */
.pp-cols-toggle{font:var(--type-label);background:var(--surface);color:var(--navy);
  border:1px solid var(--line);border-radius:999px;cursor:pointer;line-height:1;
  height:34px;padding:0 15px;letter-spacing:.1em;text-transform:uppercase}
.pp-cols-toggle:hover{border-color:var(--navy);background:var(--sunk)}
.pp-cols-all{font:var(--type-label);color:var(--navy);text-decoration:none;
  letter-spacing:.06em;text-transform:uppercase;margin-left:auto;
  /* 44px of hit area on a link that is one line of 11px type -- the floor
     iOS, Material and WCAG 2.5.5 all agree on, and the same one
     components.css enforces for buttons. */
  display:inline-flex;align-items:center;min-height:44px}
.pp-cols-all:hover{text-decoration:underline;text-decoration-thickness:2px;
  text-decoration-color:var(--accent)}

/* THE LADDER, AND IT DOES NOT END AT ONE COLUMN.

   .pp-grid collapses to a single column at 640px, which is right for a
   section of three cards and wrong here: six single-column cards under a
   hero is five or six phone screens of scrolling, and the whole page is
   budgeted at two to three. So the cards stay TWO-UP all the way down and
   only the hero is ever full width. At 375px that is a ~165px card, which
   holds a headline, a clamped summary and a byline -- the picture is
   dropped below 520px rather than shrunk to a stamp, since a 165px-wide
   1200x630 crop is 87px tall and shows nothing a reader can read. */
@media(max-width:900px){
  .pp-col-grid{grid-template-columns:1fr 1fr}
  .pp-col-hero .pp-cardimg{max-height:260px}
}
/* NO font-size IN THE HERO'S BREAKPOINTS. --fs-subhead and --fs-body are
   both clamps, so the hero's type has already narrowed by the time these
   rules apply; restating a size here would pin it and undo that. What DOES
   change below is what the two-up cards can hold. */
@media(max-width:520px){
  .pp-col-hero .pp-cardimg{max-height:190px}
  .pp-col-card .pp-card{padding:11px 12px}
  .pp-col-card .pp-cardhl{font-size:var(--fs-body-sm)}
  .pp-col-card .pp-cardsum{font-size:var(--fs-caption);-webkit-line-clamp:3;line-clamp:3}
  .pp-col-card .pp-cardimg{display:none}
  .pp-cols-all{margin-left:0}
  /* THE PHOTO BUBBLE STACKS, AND THE NUMBERS ARE MEASURED ON THE LIVE
     PAGE AT 375px RATHER THAN REASONED ABOUT. Two-up gives a 166px card
     and a 140px byline row; a 32px face plus the 10px gap leaves 98px for
     the name, and "Sudeep Kumar Jain" renders 47px tall in it -- THREE
     LINES beside a 32px circle. Nothing overflows, so no guard catches
     it; it simply looks broken, on the surface Sudeep actually reviews.
     Stacked, the name gets 136px and comes out at 23px -- one line, with
     the role on one line under it.
     THE FACE IS NOT SHRUNK AND NOT DROPPED. The card picture goes at this
     width because a 165px-wide 1200x630 crop shows nothing readable; a
     face is legible at 32px and is the whole of what cp-377 added here --
     "IF you cant give them self promotion opportunity why would they
     contribute for free". Dropping it to buy a line of width would remove
     the feature to fix its layout. */
  .pp-card .byline{flex-direction:column;align-items:flex-start;gap:6px}
}


/* THE AD BANNER AS A GRID ROW (cp-458).

   Ctrl+Shift's banner sits halfway through its wall of column cards --
   Sudeep, 2026-09-11: "For Feature and Ctrl+SHift move Ad-banner to 50%
   too." Both walls are real CSS grids (.pp-col-grid on the flat path,
   .pp-grid--wide on the preview path), so a banner dropped between two cards
   would otherwise become a grid ITEM one column wide and stand in the middle
   of a row like a very odd card.

   1 / -1 SPANS WHATEVER THE GRID CURRENTLY HAS, which matters because both
   grids change their column count at 820 and 640 and .pp-col-grid--flat uses
   auto-fit. A hard span count would be right at one breakpoint and wrong at
   the others.

   A ROW RATHER THAN A SECOND GRID, and that is not a style preference. The
   alternative was closing the grid, emitting the banner, and opening another
   -- which would leave the page with two elements carrying
   data-tfc-card-grid, the attribute static/js/product-hero-rotate.js looks
   for. Inert today, because both callers pass flat=True and the script only
   runs on the rotating shape, but "inert today" is how the next person
   inherits a bug. */
.pp-grid > .pp-adrow,
.pp-col-grid > .pp-adrow{grid-column:1 / -1}
