/* THE COMPONENT LAYER -- BUTTONS, MODALS, NUDGES.
 *
 * Published definition: docs/DESIGN-SYSTEM.md, "Components".
 * Enforced by frontend_app/tests/test_component_layer_is_single_source.py.
 *
 * Sudeep, 2026-08-16: "Where are the buttons, CTAs, Modals, and Nudges in
 * the design system?" And, the same day: "The design system once
 * established will carry the tfc brand forward. it is important to get
 * right and verify with WWW and App development standards of both iOS and
 * Android too. use components." And: "we may launch an app later too."
 *
 * WHY THIS FILE EXISTS
 * --------------------
 * Before 2026-08-16 there were NINETEEN button implementations for about
 * five buttons. base.html carried .btn/.btn-primary/.btn-secondary plus
 * .navuserbtn and .navreviewbtn; pages/account.html separately declared
 * .actionbtn, .tcta, .danger-cta, .btn-outline, .btn-linkedin and
 * .signout-cta; edition-states.css had .es-btn; base.html's four banners
 * each had their own .*-action; and FIVE different pages each declared a
 * class literally called `.cta`.
 *
 * Those five were not duplicates of each other. They were a NAME
 * COLLISION -- four were buttons and the fifth, in
 * pages/research_article.html, was `display:inline-flex;gap:10px`, i.e. a
 * LAYOUT CONTAINER holding two buttons. One name, two kinds of thing, and
 * nothing anywhere said which you were looking at.
 *
 * Same diagnosis as the type scale a few hours earlier: it is what happens
 * when there is no place to put the answer, so every rule answers for
 * itself. This is the place.
 *
 * THIS IS STRUCTURE, NOT RESTYLING. Every colour below is lifted verbatim
 * from the rule it replaces. There is no colour scale here and there must
 * not be one -- that is a separate pass after launch, and
 * docs/DESIGN-SYSTEM.md says so.
 *
 * THE HIT AREA IS 44px, AND IT IS NOT A STYLE PREFERENCE
 * ------------------------------------------------------
 * iOS Human Interface Guidelines say 44x44pt. Android Material says
 * 48x48dp. WCAG 2.5.5 (Target Size, Enhanced) says 44x44 CSS px; WCAG
 * 2.5.8 (Minimum, AA) says 24x24. 44 is the number all three agree is at
 * least acceptable, so 44 is the floor here and 48 is what --lg takes.
 *
 * This matters NOW rather than later because a button defined today at
 * 30px tall is what gets copied into a Swift and a Kotlin implementation
 * if the app happens, and the platform review catches it after it has
 * shipped. Getting it right in the shared definition is nearly free today
 * and expensive in eight months.
 *
 * WHY AN EXTERNAL FILE AND NOT base.html's INLINE <style>
 * --------------------------------------------------------
 * pages/invite.html is one of the six STANDALONE auth documents that do
 * not extend base.html. It had a `.cta` of its own for exactly that
 * reason. A component layer living inside base.html could never reach it,
 * so the collision would have survived in the one place a new reader meets
 * the product. Same reasoning auth-forms.css already documents.
 *
 * That is also why every var() below carries a LITERAL FALLBACK: on those
 * six documents :root has no --fs-* tokens and no --sand, and a bare
 * var(--fs-caption) would resolve to nothing, taking the whole declaration
 * with it. The fallback is always the value the token actually holds --
 * test_type_scale_is_single_source.py checks the two agree, so they cannot
 * drift apart silently.
 *
 * `style-src 'self'` covers a same-origin stylesheet in both the enforced
 * and the stricter report-only CSP (main.py:_build_csp), so this adds
 * nothing to the inline inventory tfc-70 is shrinking.
 */

/* ============================================================
   1. BUTTON  --  one component, five variants, two sizes,
                  five states, three modifiers.

   VARIANTS   primary | secondary | outline | danger | brand-linkedin
              (+ ghost, which is a borderless text action)
   SIZES      default (13px / 44px) | --lg (15px / 48px)
   STATES     rest | hover | focus-visible | active | disabled
   MODIFIERS  --block (full width) | --icon (square, icon only)

   LEGACY NAMES RESOLVE TO THE COMPONENT rather than being deleted --
   the same treatment base.html gives .h1-serif/.body-serif/.meta in the
   type scale. Every name below is listed in the selector group of the
   base and of its variant, so there is exactly ONE definition of a
   button on this site and no markup had to be rewritten to get it. A
   page stylesheet that redeclares any of them fails the guard test.
   ============================================================ */

/* THE BASE. Everything a button is regardless of colour.

   font-family:inherit is REQUIRED and is registered as the one family
   exception in test_type_scale_is_single_source.py: a <button> element
   does NOT inherit font-family from its ancestors the way a <div> does --
   the UA stylesheet gives it a system font. Every rule this file replaces
   said `font-family:var(--sans)` for that reason. Saying `inherit` is the
   same fix expressed once, and it also works on the six standalone auth
   documents, where --sans does not exist.

   min-height, not padding-to-44: padding would have to differ per size to
   land on the same floor, and the floor is about the HIT AREA rather than
   about how the button looks. inline-flex + centring is what lets a
   44px box hold a 13px label without the label drifting off centre. */
.btn,
.btn-primary, .btn-secondary, .btn-outline, .btn-linkedin,
.actionbtn, .tcta, .danger-cta, .signout-cta, .es-btn,
.navuserbtn, .navreviewbtn, .nudge-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  padding: 10px 18px;
  border: 1px solid transparent;
  border-radius: 8px;
  font-family: inherit;
  font-size: var(--fs-caption, 13px);
  line-height: var(--lh-caption, 1.4);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  transition: opacity 200ms ease-in-out, background 200ms ease-in-out,
              border-color 200ms ease-in-out;
}

/* THE FOCUS RING, ONCE, FOR EVERY BUTTON ON THE SITE.
   Before this, five of the nineteen implementations had a :focus-visible
   rule and fourteen had nothing at all -- so most of this site's buttons
   were keyboard-reachable and invisible once reached.

   --btn-focus rather than a fixed colour: a navy ring is right on white
   and INVISIBLE on the navy masthead and the navy nudge banners. Those
   surfaces set the variable instead of writing a second focus rule, which
   is what stops the ring drifting the way the buttons themselves did. */
.btn, .btn-primary, .btn-secondary, .btn-outline, .btn-linkedin,
.actionbtn, .tcta, .danger-cta, .signout-cta, .es-btn,
.navuserbtn, .navreviewbtn, .nudge-action { --btn-focus: var(--navy, #001234); }

.btn:focus-visible,
.btn-primary:focus-visible, .btn-secondary:focus-visible,
.btn-outline:focus-visible, .btn-linkedin:focus-visible,
.actionbtn:focus-visible, .tcta:focus-visible, .danger-cta:focus-visible,
.signout-cta:focus-visible, .es-btn:focus-visible,
.navuserbtn:focus-visible, .navreviewbtn:focus-visible,
.nudge-action:focus-visible {
  outline: 3px solid var(--btn-focus);
  outline-offset: 3px;
}

/* ACTIVE. One shared press response. Deliberately geometric rather than a
   colour change: a colour change would have to be written five times, once
   per variant, which is how the hover states ended up disagreeing. */
.btn:active,
.btn-primary:active, .btn-secondary:active, .btn-outline:active,
.btn-linkedin:active, .actionbtn:active, .tcta:active, .danger-cta:active,
.signout-cta:active, .es-btn:active, .nudge-action:active {
  transform: translateY(1px);
}
@media (prefers-reduced-motion: reduce) {
  .btn, .btn-primary, .btn-secondary, .btn-outline, .btn-linkedin,
  .actionbtn, .tcta, .danger-cta, .signout-cta, .es-btn,
  .navuserbtn, .navreviewbtn, .nudge-action { transition: none; }
  .btn:active, .btn-primary:active, .btn-secondary:active,
  .btn-outline:active, .btn-linkedin:active, .actionbtn:active,
  .tcta:active, .danger-cta:active, .signout-cta:active, .es-btn:active,
  .nudge-action:active { transform: none; }
}

/* DISABLED. Sand ground, muted ink -- pages/account.html's .tcta[disabled]
   treatment, which was the only one of the four that carried a real
   colour rather than an opacity. `opacity:.6` and `opacity:.55` said
   "faded" where this says "not available", and a faded navy button on
   white still reads as pressable.

   [aria-disabled] as well as [disabled], because an <a> cannot carry
   `disabled` at all and the two must look the same or a disabled link is
   a live-looking dead end. */
.btn:disabled, .btn[disabled], .btn[aria-disabled="true"],
.btn-primary:disabled, .btn-secondary:disabled, .btn-outline:disabled,
.btn-linkedin:disabled, .actionbtn:disabled, .tcta:disabled,
.tcta[disabled], .danger-cta:disabled, .danger-cta[disabled],
.signout-cta:disabled, .es-btn:disabled, .nudge-action:disabled {
  background: var(--sand, #F1EFE8);
  color: var(--muted, #6b7080);
  border-color: var(--sand, #F1EFE8);
  cursor: not-allowed;
  transform: none;
  opacity: 1;
}

/* ---- VARIANTS. Colour only. Every value lifted verbatim. ---- */

/* PRIMARY -- navy ground, yellow ink. base.html's .btn-primary, and what
   four of the five `.cta` rules were each independently spelling out. */
.btn--primary, .btn-primary, .tcta, .signout-cta {
  background: var(--navy, #001234);
  color: var(--yellow, #F0D62F);
  border-color: var(--navy, #001234);
}
.btn--primary:hover, .btn-primary:hover, .tcta:hover, .signout-cta:hover { opacity: .9; }

/* SECONDARY -- yellow ground, navy ink. 12.6:1, the highest-contrast
   pairing the brand has. */
.btn--secondary, .btn-secondary, .es-btn {
  background: var(--yellow, #F0D62F);
  color: var(--navy, #001234);
  border-color: var(--yellow, #F0D62F);
}
.btn--secondary:hover, .btn-secondary:hover { opacity: .9; }

/* OUTLINE -- white ground, navy ink, hairline border. account.html's
   .btn-outline and .actionbtn were the same button with different padding
   and a one-pixel difference in type size. */
.btn--outline, .btn-outline, .actionbtn {
  background: var(--surface, #fff);
  color: var(--navy, #001234);
  border-color: var(--line, #E4E4E0);
}
.btn--outline:hover, .btn-outline:hover, .actionbtn:hover { border-color: var(--navy, #001234); }

/* DANGER -- pink ground, white ink. Used in exactly one place and it must
   stay that way: account closure, after the fold is opened AND the
   confirmation phrase is typed. account.html's own note is the rule --
   "colour at the point of action is a warning; colour on a closed
   accordion row is decoration". */
.btn--danger, .danger-cta {
  background: var(--pink, #EE2B6C);
  color: #fff;
  border-color: var(--pink, #EE2B6C);
}
.btn--danger:hover, .danger-cta:hover { opacity: .9; }

/* BRAND -- LinkedIn. NOT part of the palette and deliberately outside it:
   the member is about to be handed to LinkedIn and the button has to look
   like where it goes. Its hover is LinkedIn's own darker blue rather than
   this file's opacity shift, which is the one hover that carries meaning
   rather than convention. */
.btn--brand-linkedin, .btn-linkedin {
  background: #0A66C2;
  color: #fff;
  border-color: #0A66C2;
}
.btn--brand-linkedin:hover, .btn-linkedin:hover { background: #004182; border-color: #004182; }
.btn--brand-linkedin svg, .btn-linkedin svg { flex-shrink: 0; fill: #fff; }

/* GHOST -- a text action that is still a button. Replaces the inline
   `style="background:none;color:var(--muted)"` on the homepage vertical
   picker's "Skip for now", which was a sixth variant written in an
   attribute where no stylesheet could see it. */
.btn--ghost {
  background: none;
  color: var(--muted, #6b7080);
  border-color: transparent;
}
.btn--ghost:hover { color: var(--navy, #001234); }

/* ---- SIZES ---- */

/* LARGE. The primary action of a whole page or a whole dialog, not of a
   card or a row. 48px is Material's number and these four are the places
   the layout already had room for it. */
.btn--lg, .btn-linkedin, .signout-cta, .es-btn {
  min-height: 48px;
  padding: 13px 24px;
  font-size: var(--fs-body-sm, 15px);
  line-height: var(--lh-body-sm, 1.55);
}

/* ---- MODIFIERS ---- */

/* BLOCK. Full width. This was pages/invite.html's entire reason for
   declaring its own `.cta` (`display:block;width:100%`) and
   account.html's reason for `.tier .tcta{width:100%}` plus a second rule
   re-teaching an <a> how to be one. A width is a modifier, not a
   different button. */
.btn--block, .tcta, .btn-linkedin {
  display: flex;
  width: 100%;
  box-sizing: border-box;
}

/* ICON-ONLY. A square hit area for a control whose label is a glyph.
   Every one of these was under the floor before today -- the modal close
   was ~34x34 and the nudge dismiss ~24x28 -- which is the failure mode
   WCAG 2.5.5 exists for: the smaller the visible mark, the more likely
   the control is the only way out of something. */
.btn--icon {
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  gap: 0;
}

/* ---- LAYOUT: A ROW OF BUTTONS ----
   THE OTHER HALF OF THE `.cta` COLLISION. pages/research_article.html's
   `.cta` was never a button: it was `display:inline-flex;gap:10px`
   holding two anchors. It is renamed here to something that says out loud
   that it is a container, because a name that means both a button and the
   box around buttons is worse than two names that mean nothing. */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
/* The centred form. `.ra-missing` is `text-align:center`, which centred
   the old INLINE-flex container for free; a block-level flex container
   fills its line and has to be told. Without this the two buttons in the
   "Story not available" panel go left while the heading above them stays
   centred. */
.btn-row--center { justify-content: center; }

/* ============================================================
   2. MODAL

   Lifted out of pages/account.html, which owned every rule and every line
   of behaviour for all three of this site's dialogs. It was not a
   component; it was a section of one template.

   PARTS      backdrop | dialog | close | title
   VARIANTS   default | --square (the claim dialog's 1:1 ratio)
   BEHAVIOUR  is in /static/js/modal.js and is PART OF THE COMPONENT.
              A modal that looks unified and drops focus management is a
              regression, not a refactor -- see that file's header for the
              seven behaviours and which of them each dialog had before.
   ============================================================ */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 18, 52, .55);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 200;
  padding: 20px;
}
.modal-dialog {
  background: var(--surface, #fff);
  border-radius: 14px;
  max-width: 420px;
  width: 100%;
  padding: 24px;
  position: relative;
  max-height: 90vh;
  overflow-y: auto;
}

/* THE CLOSE CONTROL. Was `font-size:22px;padding:6px` -- about 34x34, and
   on two of the three dialogs it is the ONLY visible way out. Now a 44px
   square, positioned so the glyph sits where it always did rather than
   the box growing down and right into the heading.

   It takes .btn--icon's geometry but NOT a .btn variant: it is
   deliberately unfilled and unbordered, because a filled 44px square in
   the corner of a dialog competes with the dialog's own primary action.
   The claim dialog's bordered circle below is the documented exception,
   and it exists because UAT 128 removed that dialog's second dismiss
   control and left the × carrying the job alone. */
.modal-close {
  position: absolute;
  top: 8px;
  right: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  background: none;
  border: none;
  border-radius: 8px;
  font-family: inherit;
  /* 22px, A LITERAL, and unchanged from the rule this replaces. Not text:
     it is the drawn height of a single x glyph inside a 44px square, i.e.
     icon sizing -- the same argument homepage.css's .hp-caro-nav arrow
     already carries in test_type_scale_is_single_source.py's
     _SIZE_EXCEPTIONS, and registered there for this file too. At the
     caption role's 13px the only way out of a dialog would carry a barely
     visible mark. The BOX grew to 44; the MARK did not move. */
  font-size: 22px;
  line-height: 1;
  color: var(--muted, #6b7080);
  cursor: pointer;
}
.modal-close:hover { color: var(--ink, #1a1c22); }
.modal-close:focus-visible { outline: 3px solid var(--navy, #001234); outline-offset: 2px; }

/* The heading takes focus on open (tabindex="-1"). Browsers draw a ring on
   it, which is correct for a keyboard reader and noise for everyone else. */
.modal-dialog h2[tabindex="-1"]:focus { outline: none; }
.modal-dialog h2[tabindex="-1"]:focus-visible {
  outline: 3px solid var(--navy, #001234);
  outline-offset: 3px;
}

/* ============================================================
   3. NUDGE  --  the fixed corner banner.

   FOUR of them shipped in base.html with four copies of the same
   geometry: Add to Home Screen, the generic Ad-Free referral nudge, the
   Ad-Free milestone banner and the Black-tier Share Premium banner.

   THIS UNIFIES PRESENTATION ONLY. Which banner appears, and whether any
   does, is decided server-side in app/nudge_eligibility.py and
   app/ad_free_milestone.py, and the milestone banner's copy is built from
   the server's own counts. None of that is touched here and none of it
   may move into CSS.

   PARTS      nudge | -text | -title | -sub | -action | -close
   VARIANTS   --navy (three of the four) | --yellow (Black tier only)
   DOCKS      --top (the member nudges) | --bottom (Add to Home Screen)
              They dock in OPPOSITE corners on purpose: the three member
              nudges are mutually exclusive and share one slot, and the
              install prompt must never be able to collide with them.
   ============================================================ */

.nudge {
  position: fixed;
  left: 12px;
  right: 12px;
  z-index: 59;
  border-radius: 12px;
  padding: 12px 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, .25);
}
.nudge--top { top: 12px; }
/* Above the member nudges, because it is the one a reader can dismiss
   permanently and the one that appears on the anonymous site too. */
.nudge--bottom { bottom: 12px; z-index: 60; }

.nudge--navy { background: var(--navy, #001234); color: #fff; }
.nudge--navy .nudge-sub { color: #c9ccd6; }
.nudge--navy .nudge-close { color: #8b93a3; }
.nudge--navy .nudge-close:hover { color: #fff; }
/* Yellow ground, navy ink -- the Black tier's banner, and the only one
   that is not the Ad-Free offer. A Black member seeing a nudge should
   never have to wonder whether it is the upsell they already bought past.
   Its dismiss is muted by opacity rather than by a lighter grey: on
   yellow a grey × falls below contrast, and tinting navy keeps it in
   palette. */
.nudge--yellow { background: var(--yellow, #F0D62F); color: var(--navy, #001234); }
.nudge--yellow .nudge-sub { color: #3a4560; }
.nudge--yellow .nudge-close { color: var(--navy, #001234); opacity: .5; }
.nudge--yellow .nudge-close:hover { opacity: 1; }
.nudge--yellow .nudge-action {
  background: var(--navy, #001234);
  color: var(--yellow, #F0D62F);
  border-color: var(--navy, #001234);
}

.nudge-icon { width: 40px; height: 40px; border-radius: 9px; flex-shrink: 0; display: block; }
.nudge-text { flex: 1; min-width: 0; }
.nudge-title {
  font-size: var(--fs-caption, 13px);
  line-height: 1.3;
  font-weight: 600;
}
/* The milestone banner's title goes yellow-on-navy where the generic
   Ad-Free nudge's stays white. Emphasis, not hue: this banner always
   carries a real number about the reader's own progress and the generic
   one never does. */
.nudge-title--accent { color: var(--yellow, #F0D62F); }
/* WAS 11.5px, WHICH IS NOT ON THE SCALE and could not be: the sans roles
   are 16/15/13, and 11px is the MONO label role -- "never a sentence".
   This is a sentence, so it takes the caption role and is separated from
   the title by weight and colour rather than by size. The banner is a few
   pixels taller on a phone as a result. */
.nudge-sub { font-size: var(--fs-caption, 13px); line-height: 1.4; margin-top: 2px; }

/* The action inherits the whole button component (it is listed in the
   base selector group above) and only states what is local to a banner:
   it must not wrap, and it must not shrink when the copy is long. */
.nudge-action { flex-shrink: 0; white-space: nowrap; }
/* Both grounds are dark or saturated; a navy ring on either is invisible. */
.nudge--navy .nudge-action { --btn-focus: var(--yellow, #F0D62F); }
.nudge--yellow .nudge-action { --btn-focus: var(--navy, #001234); }

.nudge-close {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0;
  flex-shrink: 0;
  background: none;
  border: none;
  border-radius: 8px;
  font-family: inherit;
  /* 20px, a literal, unchanged. Icon sizing -- same argument as
     .modal-close above, and registered in the same exception list. */
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.nudge--navy .nudge-close:focus-visible {
  outline: 3px solid var(--yellow, #F0D62F);
  outline-offset: 2px;
}
.nudge--yellow .nudge-close:focus-visible {
  outline: 3px solid var(--navy, #001234);
  outline-offset: 2px;
}

@media (min-width: 640px) {
  .nudge { left: 20px; right: auto; max-width: 380px; }
  .nudge--top { top: auto; bottom: 20px; }
  .nudge--bottom { left: auto; right: 20px; bottom: 20px; }
}

/* ============================================================
   4. CARD  --  the bounded content container.

   Sudeep, 2026-08-16, on being told cards and forms were not yet
   components: "They need to be components; which is why I asked for a
   design system not a template."

   WHY THIS ONE IS DIFFERENT FROM THE BUTTON
   -----------------------------------------
   The button had nineteen implementations of ONE thing, so unifying it
   meant deleting eighteen. The card has twenty-one implementations of a
   thing that genuinely comes in three sizes and five grounds, and the
   grounds carry EDITORIAL meaning. Collapsing them would look like a
   successful consolidation and would destroy a brand distinction.

   So the card is expressed as a base plus KNOBS rather than as a base
   plus eighteen deletions:

     --card-radius   4px editorial | 10px list | 12px panel (default)
     --card-pad      the one number a card is still allowed to decide
     --card-border   the hairline
     --card-rail     the accent colour of the top rail, and
     --card-rail-w   its thickness

   --card-pad is the honest part. Twenty-one cards use fifteen different
   paddings -- 11/13, 12/14, 13/16, 14/15, 15/16/17, 16/17, 16/18, 18,
   20, 26/20, 36, 40, 44/30, clamp(22,3vw,34), clamp(22,3.4vw,40) -- and
   several are fluid on purpose. Flattening them to one number would be
   a REDESIGN of every page at once, which this pass explicitly is not.
   So padding stays local and is stated as ONE declaration through the
   knob, instead of being restated as part of a whole box. The guard test
   enforces exactly that: a page may set --card-pad; it may not set
   `padding` on a card the component owns.

   THE STREAM DISTINCTION IS A VARIANT, NOT AN EXCEPTION
   -----------------------------------------------------
   Stream A (curated third-party: our 35 words about somebody else's
   story) sits on COOL #fffafa. Stream B (original First Click writing)
   sits on WARM #F5F3F0. design/DESIGN.md section 5 lists the pair among
   the four things that must never be removed, and it is the one a
   component pass is most likely to remove BY ACCIDENT -- the difference
   is about 2% of luminance and reads, correctly, as one design rather
   than two. A card component with a single surface colour would have
   looked like the tidiest possible outcome.

   They are therefore two NAMED GROUNDS below, asserted against each
   other by test_card_and_field_are_single_source.py, and the tint is
   never the only signal: .hp-stream prints the word as well.

   PARTS      card | (the page owns what goes inside it)
   GROUNDS    --surface | --stream-a | --stream-b | --sunk | --dark
   SIZES      --editorial (4px) | --list (10px) | default (12px)
   MODIFIERS  --rail | --flush | --dashed | --raised | --interactive
   STATES     rest | hover | focus-visible | current | above
   ============================================================ */

/* THE BASE. Border, radius, padding, box model -- and deliberately NO
   background: a card that declares no ground today (.acard, .tier,
   .wb-card, .hp-card) sits on whatever is behind it, and handing all of
   them var(--surface) would have been a restyle wearing a refactor's
   clothes. A ground is asked for; it is not a default.

   Legacy names resolve to the component rather than being deleted --
   the same treatment .btn-primary / .tcta / .es-btn get above, and the
   reason no card markup had to be rewritten to gain the shared box. */
.card,
.hp-lead, .hp-vcard, .hp-card, .hp-col, .hp-empty, .es,
.acard, .tier, .earnbox, .closebox, .databox, .signoutbox, .arch-card,
.ct-card, .wb-card, .tile,
.rs-item, .rs-vcard, .rs-ent, .rs-pending, .ra-missing, .errp {
  box-sizing: border-box;
  border: 1px solid var(--card-border, var(--line, #E4E4E0));
  border-radius: var(--card-radius, 12px);
  padding: var(--card-pad, 20px);
}

/* ---- GROUNDS. Colour only; every value lifted verbatim. ---- */

.card--surface, .hp-vcard, .hp-col, .ct-card, .tile,
.rs-item, .rs-vcard, .rs-ent { background: var(--surface, #fff); }

/* STREAM A -- cool. Curated third-party content. DO NOT MERGE WITH
   STREAM B: see the header, and design/DESIGN.md section 5. */
.card--stream-a, .hp-lead, .hp-empty, .es, .errp, .hp-card--a { background: #fffafa; }
/* STREAM B -- warm. Original First Click writing. */
.card--stream-b, .es--warm, .hp-card--b { background: #F5F3F0; }

.card--sunk, .rs-pending, .ra-missing { background: var(--sunk, #F4F3EE); }

/* DARK -- pages/claim.html's card, and the only dark surface on the
   site. Named here rather than left as a page's own rule so the
   BLACK-tier claim document sits inside the system rather than beside
   it; whether the dark treatment is a deliberate variant or drift is
   still the open question auth-forms.css's header records. */
.card--dark { background: #000; --card-border: #22242c; }

/* ---- SIZES. The three radii that genuinely exist. ---- */

/* EDITORIAL -- 4px. The daily board and the edition states: panels that
   ARE the page's content rather than a container around it. A 12px
   radius on a full-bleed lead panel reads as a widget. */
.card--editorial, .hp-lead, .hp-vcard, .hp-card, .hp-col, .hp-empty, .es, .errp {
  --card-radius: 4px;
}
/* LIST -- 10px. A row in a list of like things. */
.card--list, .ct-card, .rs-item, .rs-vcard, .rs-ent, .rs-pending,
.closebox, .databox, .signoutbox { --card-radius: 10px; }
/* PANEL -- 12px, the default, so it has no modifier. */

/* ---- THE ONE KNOB EACH CARD STILL SETS ----
   Padding, and nothing else. See the header for why it was not
   flattened. .acard and .wb-card are absent because 20px IS the
   default. */
.hp-vcard, .hp-card { --card-pad: 15px 16px 17px; }
.hp-col { --card-pad: 14px 15px; }
.hp-empty { --card-pad: clamp(22px, 3vw, 34px); }
.es, .errp { --card-pad: clamp(22px, 3.4vw, 40px); }
.tier, .ct-card { --card-pad: 16px 17px; }
.earnbox { --card-pad: 16px 18px; }
.closebox { --card-pad: 13px 16px; }
.databox, .signoutbox, .tile { --card-pad: 18px; }
.rs-item { --card-pad: 15px 17px; }
.rs-vcard { --card-pad: 12px 14px; }
.rs-ent { --card-pad: 11px 13px; }
.rs-pending { --card-pad: 26px 20px; }
.ra-missing { --card-pad: 44px 30px; }

/* ---- MODIFIERS ---- */

/* FLUSH. A card whose media runs edge to edge, so the padding belongs
   to an inner body element rather than to the card. The homepage lead
   panel and its carousel slides are the only ones; overflow:hidden is
   what keeps a 1200x630 photograph inside a 4px corner. */
.card--flush, .hp-lead { --card-pad: 0; overflow: hidden; }

/* THE RAIL. A thick accent border along the top carrying the vertical's
   palette colour. It is a BLOCK of colour and never the only signal --
   four of the five vertical accents fail 4.5:1 on white, so the
   vertical's NAME appears in navy text below every rail on this site.

   Width is a knob because the three that exist are deliberate: 6px on a
   panel that IS the page, 5px on a card, 4px on a column. */
.card--rail, .hp-lead, .hp-vcard, .hp-col, .hp-empty, .es, .errp {
  border-top: var(--card-rail-w, 5px) solid var(--card-rail, var(--navy, #001234));
}
.hp-lead, .hp-empty, .es, .errp { --card-rail-w: 6px; }
.hp-col { --card-rail-w: 4px; }
/* --vc is set per card as a style attribute: it is DATA (one of five
   palette entries chosen per article), which is why it cannot be a
   class. The navy fallback is what an unclassified card gets. */
.hp-lead, .hp-vcard, .hp-col { --card-rail: var(--vc, #001234); }

/* DASHED. "Nothing here yet", drawn as an outline rather than a filled
   panel, so an empty state cannot be mistaken for content. */
.card--dashed, .rs-pending, .ra-missing { border-style: dashed; }

/* RAISED. A card that floats on the page rather than sitting in a
   column -- the standalone auth documents. */
.card--raised { box-shadow: 0 1px 3px rgba(0, 0, 0, .05); }

/* ---- STATES ----
   INTERACTIVE is where this half of the pass pays for itself. Of the
   five cards that are themselves a link or a button, .hp-card had a
   focus ring and .ct-card / .tile / .rs-vcard / .rs-ent had a hover and
   NO FOCUS RING AT ALL -- the same failure the button component found
   on fourteen of nineteen buttons, one layer up. */
.card--interactive, .hp-card, .ct-card, .tile, .rs-vcard, .rs-ent {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: border-color 200ms ease-in-out, background 200ms ease-in-out;
}
.card--interactive:hover, .hp-card:hover, .ct-card:hover,
.rs-vcard:hover, .rs-ent:hover { --card-border: var(--navy, #001234); }
/* .tile and .ct-card also warm their ground on hover; both predate the
   component and a tile has no other affordance. */
.tile:hover, .ct-card:hover { background: var(--sunk, #F4F3EE); }
.card--interactive:focus-visible, .hp-card:focus-visible,
.ct-card:focus-visible, .tile:focus-visible,
.rs-vcard:focus-visible, .rs-ent:focus-visible {
  outline: 3px solid var(--card-focus, var(--navy, #001234));
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .card--interactive, .hp-card, .ct-card, .tile, .rs-vcard, .rs-ent { transition: none; }
}

/* CURRENT. The card a reader is already on. Navy, doubled, sunk AND
   railed -- four signals, because a border-colour change alone is colour
   as the only signal. account.html's `.tier.current` is the one user.

   THE YELLOW RAIL WAS ADDED 2026-08-17, and it is the answer to a review
   note rather than decoration. Sudeep, on his own BLACK account:
   "Shouldnt the current plan be highlighted instead of looking duller
   than those that are below your plan?" Navy-doubled-and-sunk is a QUIET
   distinction -- it competes with three sibling cards each carrying a
   filled navy button, and it loses. The rail is the loudest mark in the
   row and it is spent on the one card that is his.

   It reuses the card's own --card-rail knob rather than inventing a
   border: the rail already means "this card is marked out" everywhere
   else on the site (the homepage lead and the vertical columns take the
   vertical's colour through the same knob), so a reader meets a familiar
   signal in a new place rather than a new signal.

   THE GROUND WENT NAVY LATER THE SAME DAY, and the first attempt is worth
   recording because it failed in an instructive way. Sand ground + a thin
   yellow rail + a pale label was still a QUIET card wearing one loud
   stripe. Sudeep: "The old styling looked nice/sharp due to the navy
   button. Now new looks like a bald man with an egg on his skull. PLease
   sharpen it. Use dark background if required."

   He is right about the cause. The complaint that started this was that
   his own plan looked DULLER than the plans beneath it, and the fix
   swapped a filled navy button for a pale panel -- which removed the
   darkness that was doing the work and replaced it with an outline. The
   rail alone cannot carry a card; it can only trim one.

   So the card is now REVERSED: navy ground, white ink, the yellow rail
   kept as a crown on top of it. It is the darkest thing in the row by a
   wide margin, which is what "this one is yours" should be.

   IT DOES NOT COMPETE WITH THE `above` CARD, and the reason is the same
   one the pair was built on: these are different SHAPES, not different
   intensities. `current` is a solid dark panel with no control in it;
   `above` is an ordinary light card carrying one filled navy button. A
   reader finds their plan by the block of colour and their next step by
   the button, and neither reading is available for the other.

   --card-ink is the knob a caller reads to tint its own contents (the
   tier name, price, bullets and status marker in account.html). Declared
   here rather than left to each caller so a second reversed card cannot
   invent a second white. */
.card--current, .tier.current {
  --card-border: var(--navy, #001234);
  --card-ink: #fff;
  border-width: 2px;
  background: var(--navy, #001234);
  color: var(--card-ink);
  border-top: var(--card-rail-w, 5px) solid var(--card-rail, var(--yellow, #F0D62F));
}

/* ABOVE. A card the reader has NOT reached yet -- the upgrade path, and
   the only card in a ladder with an action worth taking.

   The pair above/current answers one complaint and must not collapse
   back into one treatment while doing it. "Highlighted" means two
   different things in the same row and they are deliberately different
   SHAPES, not different intensities of the same shape:

     current -> the CARD is marked (rail, doubled border, sunk ground)
                and its action slot holds a quiet text marker. STATUS:
                you are here. Nothing to press.
     above   -> an ordinary card that carries the one FILLED button.
                INVITATION: go here. The emphasis is on the control.

   So the eye finds the reader's own plan by the card and the next step
   by the button, and neither reading is available for the other. If these
   two ever look alike, the row has stopped answering "which one am I
   on", which is the exact defect this pair was added to fix.

   The navy hairline is all the card itself takes: enough to separate an
   available plan from one already behind the reader, not enough to argue
   with the rail. */
.card--above, .tier.above { --card-border: var(--navy, #001234); }

/* ============================================================
   5. FIELD  --  one labelled control.

   ACCESSIBILITY IS THE COMPONENT, NOT A LAYER ON IT. A form component
   that only unifies appearance is half-built, and the half it leaves
   out is the half that decides whether the form can be used at all. So
   the definition below includes the markup contract, and the guard test
   asserts the markup as well as the CSS:

     1. Every control has a real <label for>. A placeholder is not a
        label: it disappears the moment the reader types, and it is not
        an accessible name on most screen readers. THREE CONTROLS ON
        THIS SITE HAD NOTHING ELSE -- the waitlist gate's email field,
        the research search box and the contributors search box.
     2. An error is ASSOCIATED, not merely coloured red. Colour alone
        fails WCAG 1.4.1. `.form-error` carries role="alert", so a
        server- or fetch-rendered message is announced rather than
        silently painted; a hint is wired with aria-describedby.
     3. A required field says so in text. `required` alone is announced
        by a screen reader and shown to nobody else.
     4. autocomplete on email / name / current-password / new-password.
        Password managers depend on it and so does the OS keyboard.
     5. The right `type` / `inputmode`. type="email" is what gives a
        phone the @ key -- which matters more once there is an app.
     6. 44px minimum hit area, the same floor as the button, per
        iOS 44pt / Material 48dp / WCAG 2.5.5. EVERY TEXT INPUT ON THIS
        SITE WAS UNDER IT: 14px type in 10px of padding is about 41px.
     7. focus-visible through the SAME mechanism as the button, not a
        second one.
     8. Errors must not depend on JavaScript. Nothing below is a
        scripted state; :invalid, [aria-invalid] and [disabled] all
        paint from server-rendered markup.

   PARTS      field | label | control | field-hint | field-req |
              form-error | form-success
   VARIANTS   default | --dark (pages/claim.html's BLACK-tier card)
   MODIFIERS  --inline (a control that sits in a row, not full width)
              --mono (a control holding a URL or a typed phrase)
   STATES     rest | hover | focus-visible | invalid | disabled |
              readonly
   ============================================================ */

/* The visually-hidden utility. It lives HERE rather than in base.html
   because rule 1 above is unsatisfiable without it on the six
   standalone auth documents -- base.html's .sr-only cannot reach them,
   which is precisely why /site-access shipped a bare placeholder. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* THE ROW. 16px (base.html) rather than 18px (auth-forms.css): the
   shared rule is the incumbent, and it is the one seen on more
   surfaces. site_access still sets 0 and spaces its single field
   through the control's own margin; that predates this file. */
.field { margin-bottom: var(--field-gap, 16px); }

/* THE LABEL. 600 and navy.
   WAS 600/var(--muted) in base.html, 500/var(--navy) on login, register
   and set_password, 600/var(--navy) on about, 500/var(--yellow) on
   claim. Navy at 600 is the majority weight and the higher-contrast
   colour -- --muted #6b7080 is 5.4:1 on white against navy's 16.7:1,
   and a form label is not secondary text. claim keeps yellow through
   the --dark variant below. */
.field label, .field-label {
  display: block;
  font-size: var(--fs-caption, 13px);
  line-height: var(--lh-caption, 1.4);
  font-weight: 600;
  color: var(--field-label, var(--navy, #001234));
  margin-bottom: 6px;
}

/* THE CONTROL.
   font-family:inherit for the same reason the button says it: the UA
   stylesheet gives <input>, <select> and <textarea> a system font, and
   `inherit` is the one spelling that also works on the six standalone
   auth documents where --sans does not exist.

   min-height:44px is the hit area, set the way the button sets it --
   on the box, not by inflating the padding, so nothing had to be
   redesigned to clear the floor.

   NOTE THE ABSENCE OF appearance:none. It is what a component author
   reaches for to make a <select> match an <input>, and on a <select> it
   deletes the disclosure arrow -- leaving a control that looks exactly
   like a text box and cannot be typed into. */
/* :where() so the three exclusions cost NO specificity -- written as
   plain :not() this selector would outrank every page rule that might
   legitimately add a margin or a width to one field, and a component
   nobody can adjust locally is one people copy instead. */
/* .rs-entfoot input: the Find box beside Load more on /research. Named
   here rather than styled on the page so both of that page's search
   boxes come from these declarations -- see cp-334. */
.field input:where(:not([type="checkbox"], [type="radio"], [type="hidden"])),
.field select,
.field textarea,
.field-control,
.ct-searchbar input,
.rs-controls input, .rs-entfoot input, .rs-controls select,
.cd-month-filter select,
select.tz,
.reflink input,
.closebox input[type="text"] {
  width: 100%;
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid var(--field-border, var(--line, #E4E4E0));
  border-radius: 6px;
  font-family: inherit;
  font-size: var(--fs-body-sm, 15px);
  line-height: var(--lh-body-sm, 1.55);
  background: var(--field-bg, var(--surface, #fff));
  color: var(--field-ink, var(--ink, #1a1c22));
}
.field textarea, .field-control--multiline { resize: vertical; min-height: 100px; }

/* MONO -- a control whose content is a URL or a phrase to be typed back
   exactly. Family only; the size comes from the component, so the
   referral link and the closure confirmation no longer sit at 12px,
   which was the smallest type on the account page. */
.field-control--mono, .reflink input, .closebox input[type="text"] {
  font-family: var(--mono, ui-monospace, monospace);
}

/* INLINE -- a control that shares a row rather than filling it. The
   width comes from the flex parent; the component keeps the box. */
.field-control--inline, .rs-controls input, .rs-entfoot input, .rs-controls select,
.cd-month-filter select, select.tz, .reflink input,
.closebox input[type="text"] { width: auto; }
.ct-searchbar input { max-width: 520px; }

/* ---- STATES ----

   THE FOCUS RING, and it is a real fix rather than a rename. Every
   field on this site did `outline:none` and replaced it with
   `box-shadow:0 0 0 2px rgba(0,18,52,.08)` -- navy at EIGHT PER CENT,
   which against white is a contrast ratio of about 1.05:1. It passed
   the "did you replace the outline" guard and was, in practice, not
   visible. One field (.rs-controls input) replaced it with a
   border-colour change and nothing else.

   This is the button's mechanism, variable and all, so the two cannot
   drift: --field-focus is set by the surface, not by a second rule. */
.field input:focus-visible, .field select:focus-visible,
.field textarea:focus-visible, .field-control:focus-visible,
.ct-searchbar input:focus-visible,
.rs-controls input:focus-visible, .rs-entfoot input:focus-visible, .rs-controls select:focus-visible,
.cd-month-filter select:focus-visible,
select.tz:focus-visible, .reflink input:focus-visible,
.closebox input[type="text"]:focus-visible {
  outline: 3px solid var(--field-focus, var(--navy, #001234));
  outline-offset: 2px;
  border-color: var(--field-focus, var(--navy, #001234));
}

/* INVALID. [aria-invalid] as well as :user-invalid, and NOT :invalid --
   :invalid paints a field the reader has not reached yet, and a form
   that is red before it is touched teaches the reader to ignore red.
   :user-invalid fires only after the reader has interacted or
   submitted, which is what it exists for.

   NOTHING SETS aria-invalid TODAY, and that is stated rather than
   implied. The five auth documents' inline scripts write their message
   into .form-error and mark no field; so the attribute selector below
   is the state the component OFFERS, and wiring the scripts to it is a
   behaviour change this structural pass deliberately did not make.
   :user-invalid is what carries the state meanwhile, including with
   JavaScript unavailable, which is the case that matters most. */
.field [aria-invalid="true"], .field-control[aria-invalid="true"],
.field input:user-invalid, .field select:user-invalid,
.field textarea:user-invalid {
  --field-border: var(--pink, #EE2B6C);
  --field-focus: var(--pink, #EE2B6C);
}

.field input:disabled, .field select:disabled, .field textarea:disabled,
.field-control:disabled, .rs-controls input:disabled, .rs-entfoot input:disabled,
.rs-controls select:disabled {
  background: var(--sand, #F1EFE8);
  color: var(--muted, #6b7080);
  cursor: not-allowed;
}
.field input[readonly], .field-control[readonly], .reflink input[readonly] {
  background: var(--sunk, #F4F3EE);
}

/* ---- THE MESSAGES ----
   .form-error is declared in auth-forms.css for the six standalone
   documents and in base.html for the other twenty-seven; both are left
   where they are because they differ in alignment for a reason (an auth
   card centres its one message under a full-width button). What this
   layer adds is the two parts neither of them had. */

/* THE HINT. The password rule under a password box, which shipped as an
   inline `style="font-size:11.5px"` on four documents -- a size that is
   on no scale, in an attribute no stylesheet could see, and associated
   with nothing. It is now caption-sized and wired to its input with
   aria-describedby, so a screen reader reads the rule before the reader
   invents a password that breaks it. */
.field-hint {
  font-size: var(--fs-caption, 13px);
  line-height: var(--lh-caption, 1.4);
  color: var(--field-hint, var(--muted, #6b7080));
  margin-top: 5px;
}

/* REQUIRED / OPTIONAL, IN WORDS. WCAG 1.3.1 and 3.3.2: the `required`
   attribute is announced to a screen reader and shown to nobody else,
   and an asterisk on its own is a convention rather than a label. The
   minority is what gets marked -- on a form where one field of four is
   optional, marking that one is quieter and less error-prone than
   marking three. */
.field-req, .field-optional {
  font-weight: 400;
  color: var(--field-hint, var(--muted, #6b7080));
}

/* ---- THE DARK VARIANT ----
   pages/claim.html. The BLACK-tier claim card is the only dark surface
   on the site; it sets the four knobs and writes no second definition
   of a field. Note --field-focus: on a #0a0c12 ground a navy ring is
   invisible, which is the same argument --btn-focus carries above. */
.field--dark {
  --field-label: var(--yellow, #F0D62F);
  --field-border: #2b2e38;
  --field-bg: #0a0c12;
  --field-ink: #fff;
  --field-focus: var(--yellow, #F0D62F);
  --field-hint: #7d8291;
}

/* ---- CHOICE: a checkbox or a radio and its label ----
   The usual offender for the 44px floor, and it was one here:
   account.html's vertical picker was a 40px row and the research
   filter pills are 31px. The row is the target, not the 13px box
   inside it, so min-height goes on the label. */
.field-choice, .vpick label {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 9px 12px;
  border: 1px solid var(--line, #E4E4E0);
  border-radius: 9px;
  cursor: pointer;
  font-size: var(--fs-caption, 13px);
  line-height: var(--lh-caption, 1.4);
  color: var(--ink, #1a1c22);
  transition: border-color 150ms, background 150ms;
}
.field-choice:hover, .vpick label:hover { border-color: var(--silver, #9aa0ad); }
/* The ring goes on the LABEL, driven by the input inside it, because
   the input is what receives focus and the label is what the reader
   sees. Without this the vertical picker was keyboard-operable and
   showed nothing at all. */
.field-choice:focus-within, .vpick label:focus-within {
  outline: 3px solid var(--field-focus, var(--navy, #001234));
  outline-offset: 2px;
}
.field-choice input, .vpick input { accent-color: var(--navy, #001234); }
.field-choice--picked, .vpick label.picked {
  border-color: var(--navy, #001234);
  border-width: 2px;
  background: var(--sunk, #F4F3EE);
}
@media (prefers-reduced-motion: reduce) {
  .field-choice, .vpick label { transition: none; }
}

/* ============================================================
   SECTION 8: THE PROMO BOX
   ============================================================

   The yellow strip that closes a reading page: "Unlock Ad-Free reading /
   Refer a friend ->" for a member who can still earn it, "Join thousands
   of busy executives / Become a Member ->" for a stranger. The markup is
   `promo()` in _product_page.html and there is only the one copy of it.

   MOVED HERE FROM product-page.css on 2026-08-17, verbatim and under the
   same class names. It lived in the product-page layer while /daily,
   /publol and /ctrl-shift were its only callers; design/mockups/Home.dc
   .html carries the identical box at the foot of the homepage, which does
   not load that stylesheet. base.html links this file on every page, so
   moving it costs the product pages nothing and gives the homepage the
   box rather than a second declaration of it.

   Yellow on BOTH product mockups whatever the product's accent is, which
   is why it reads --yellow outright and not --accent: it is the brand
   asking, not the product. That is also why it belongs in components.
   ============================================================ */
.pp-promo{background:var(--yellow);border-radius:12px;padding:var(--space-2xl) var(--space-2xl);
  margin:24px 0 4px;display:flex;gap:var(--space-lg);align-items:center;
  justify-content:space-between;flex-wrap:wrap}
.pp-promo-t{font-size:var(--fs-lead);font-weight:600;color:var(--navy)}
.pp-promo-s{font:var(--type-caption);color:#3a3a2a;margin-top:2px}
.pp-promo a{background:var(--navy);color:var(--yellow);font-weight:600;font-size:14px;
  text-decoration:none;padding:11px var(--space-2xl);border-radius:8px;white-space:nowrap}


/* ============================================================
   SECTION 9: THE HIGHLIGHT
   ============================================================

   A HIGHLIGHTER PEN ON PRINTED PAPER. That is the metaphor, in Sudeep's
   words on 2026-08-25: it "mimics the office vibe of using a highlighter
   on printed text." Navy ink on a solid yellow stripe, bold, uppercase,
   laid tight over a short phrase.

   THE METAPHOR IS ALSO THE SPEC, and it settles the questions that will
   otherwise be answered by taste:

     * NO border-radius. The stroke's shape IS the drawing -- straight
       along the top, one wave beneath, ends slanted the way a chisel
       tip leaves them. Rounding the box would put geometry back
       around a mark that exists to have none.
     * NO shadow, border, or gradient. Ink on paper sits IN the page, not
       above it. A shadow makes it a floating chip.
     * The stripe hugs the words and no more. Generous padding reads as a
       button and invites a click that is not there.
     * It goes OVER text, never around a block of it. Someone marking up
       a printout highlights a line, not a paragraph.

   ONE PER SCREEN. A person with a highlighter who marks every line has
   marked nothing, and the same is true here: this is loud by design, and
   its whole value is that the eye goes to it first. Two on a screen are
   worth neither.

   SHORT PHRASES AND BULLETS ONLY -- NEVER A PASSAGE THAT WRAPS. Sudeep,
   2026-08-25, on seeing it stretched over a two-line sentence: "This is a
   bit excessive... We should try never to use this highlight class for
   longer text passages; use it for shorter passages/bullets only."

   THAT IS A TECHNICAL LIMIT AS WELL AS A TASTE ONE, and it cannot be
   drawn around. The stroke is ONE SVG stretched to the element's box, so
   it knows nothing about line breaks: when the text wraps, the straight
   top edge runs through the middle of the first line instead of above it.
   A longer or repeated squiggle does not fix this. `background-decoration
   -break: clone` would, for a flat colour -- it repeats the background on
   each line fragment -- but not for a single stretched drawing, which is
   the thing that makes this look hand-made at all.

   So: a phrase, a badge, a bullet. If it wraps, it is the wrong component
   and the answer is to shorten the words, not to redraw the stroke.

   THE INK AND THE PAGE ARE BOTH NAVY, which is why the stroke must never
   fall short of a letter. A highlighter that misses on paper looks
   careless; here the uncovered part of the glyph is navy on navy and
   simply DISAPPEARS. Padding is deliberately asymmetric -- 8px above, 5px
   below -- so the stroke sits slightly high on the line the way a hand
   puts it, and the wave runs CLOSE beneath the letters without ever
   clearing them. Measured at 6px above the glyph box and 3px below.

   Sudeep proposed it for the homepage standfirst on 2026-08-25 and then
   asked for it to be reusable: "the class 'highlight' can be used in
   other templates too... like for COMING SOON which attracts viewer
   attention." So it lives here, in the sheet base.html links on every
   page, rather than in homepage.css where it started.

   WHAT IT IS FOR: a phrase that must be the first thing the eye lands on
   -- a standfirst, a COMING SOON, a status that matters. It is loud on
   purpose, which is also the constraint on it: two highlights on one
   screen are worth neither, and the moment it appears three times on a
   page it has stopped highlighting anything.

   THE COLOURS ARE INVERTED FROM EVERY OTHER YELLOW ON THIS SITE, and
   that is the SAFE direction rather than the risky one. Navy on yellow
   is about 11.9:1. The same yellow AS TEXT is 1.47:1 on white and fails
   outright -- which is why the site's standing rule is never to set
   yellow as label text. Here yellow is the GROUND, so the rule does not
   apply and the contrast is better than the body text around it.

   IT CARRIES NO MARGIN. Spacing is the caller's business: the homepage
   hero centres it with width:fit-content plus auto margins, and a
   COMING SOON badge sitting inline in a card wants neither. A component
   that ships its own margins is a component that has to be undone at
   every second use.

   width:fit-content RATHER THAN display:inline-block, so it can be
   centred by auto margins inside a flex column. The homepage kicker
   carries a long comment about exactly this: children that size to their
   content must centre their BOX, or they alone drift left while their
   siblings stay centred. That bug was reported four times before it was
   understood.

   12px: two less than the homepage kicker's 14px, Sudeep's instruction,
   and also a fit -- at 14px the homepage line overflowed a 320px phone
   and broke into a ragged double bar. Registered in the type-scale
   exception list with the measurements. */
.highlight{font-family:'Archivo Black',Impact,'Arial Black',sans-serif;
  font-weight:400;font-size:12px;line-height:1;letter-spacing:-.2px;
  color:var(--navy);text-transform:uppercase;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 40' preserveAspectRatio='none'%3E%3Cpath d='M2,38 L5,13 L198,7 L196,33 Q99,31 2,38 Z' fill='%23F0D62F'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-size:100% 100%;
  width:fit-content;padding:8px 12px 5px}

/* ==========================================================================
   SHOW PASSWORD (cp-303)

   IT LIVES IN THE COMPONENT LAYER AND NOT IN auth-forms.css, and that is
   not filing: the six standalone auth documents link auth-forms.css FIRST
   and components.css SECOND, so `.field input`'s `padding:10px 12px` here
   would win at equal specificity and the button would sit on top of the
   typed password. Same layer, later rule, no !important needed.

   Sudeep, 2026-08-29: "Show password button in the password field... on
   mobile too is missing."

   NO MEDIA QUERY ANYWHERE IN THIS BLOCK, deliberately. "On mobile too" was
   half the report, and the commonest way a control goes missing on a phone
   is a rule that hides it below some width to buy back space. The button is
   44px square at every width instead -- the touch target the design system
   asks for -- and the field's right padding makes room for it rather than
   the button overlapping the text.

   LITERAL SIZES, not var(--fs-*): the six documents that load this file are
   standalone <html> and have no :root type tokens. See the note on
   .form-error above. */
.pw-wrap{position:relative;display:block}
/* The padding is the button's width plus the gap either side of it, so a
   long password scrolls under the label rather than behind the button. */
.pw-wrap input{padding-right:66px}
/* width:auto IS THE WHOLE BUG FIX (cp-512, 2026-09-22). Sudeep, on a
   photograph of /register: "ShOW password styule is too complicated."

   IT WAS NOT COMPLICATED, IT WAS 398px WIDE AND SITTING ON TOP OF THE
   INPUT. Measured on the live page at a 1045px viewport: the input was
   398px, the wrap 398px, and the button 398px at the input's own left
   edge. The field read as one grey "Show" box with no visible place to
   type, which is exactly what he photographed.

   THE PAGE, NOT THIS FILE, WAS SIZING IT. All five documents that load
   password.js -- register, login, claim, set_password, site_access -- carry
   a BARE ELEMENT rule in their own <style>: `button{width:100%;...}`. It
   loses to `.pw-reveal` on every property this rule declares, so the
   colour, padding and border never leaked. Width was the one property
   nothing here declared, so the page's 100% was the only width declaration
   in the cascade and won uncontested. One missing line, every password
   field on the site.

   IT WENT UNCAUGHT BECAUSE EVERY GUARD ASSERTED A SHAPE AND NONE AN
   EFFECT. test_the_button_meets_the_touch_target pins min-width:44px --
   true, and no defence whatever against width:100%. And
   test_the_field_makes_room_rather_than_the_button_covering_the_text pins
   that `.pw-wrap input` has a right padding -- also true, and the button
   covered the text anyway. Both read the component layer in isolation,
   where it was always correct. The new guard in test_show_password.py
   reads the PAGES instead. */
.pw-reveal{position:absolute;top:0;right:0;height:100%;min-height:44px;min-width:44px;
  width:auto;display:inline-flex;align-items:center;justify-content:center;
  padding:0 12px;border:0;background:none;cursor:pointer;
  /* A <button> does NOT inherit the page face -- it takes the UA's own --
     so this one rule has to say so. Registered in
     frontend_app/tests/test_type_scale_is_single_source.py's
     _FAMILY_EXCEPTIONS: `inherit` is declining to choose a family, which
     is the opposite of the drift that guard exists to catch.
     var(--fs-caption, 13px) carries the fallback because the six
     standalone auth documents never see base.html's :root, and a bare
     var() there resolves to nothing and takes the declaration with it. */
  font-family:inherit;font-size:var(--fs-caption,13px);
  font-weight:600;line-height:1;
  /* --muted is not defined on these standalone documents either; the
     literal is the same slate the rest of the auth chrome uses. */
  color:var(--muted,#5b6070);border-radius:0 6px 6px 0}
/* THE ICON, NOT THE WORD (cp-512). Sudeep: "Use default stuff used by
   Microsoft and Google in their login modal." Both render an outline eye
   inside the field at the right, struck through once the password shows.
   20px is their size; the 44px hit area around it is ours and is unchanged.

   THE ACCESSIBLE NAME IS UNAFFECTED, which is the point of doing it this
   way. The button still carries aria-label="Show password" / "Hide
   password" and aria-pressed, so a screen reader announces exactly what it
   announced when the button spelled the word out; the SVG is aria-hidden.
   An icon-only control without that name would have been a downgrade. */
.pw-reveal svg{width:20px;height:20px;display:block;pointer-events:none}
.pw-reveal:hover{color:var(--ink,#1a1c22)}
.pw-reveal:focus-visible{outline:2px solid var(--blue,#2E6FB8);outline-offset:-2px}


/* ==========================================================================
   SECTION 10: THE CHIP
   ==========================================================================

   A chip is a tag: a short label that either filters the list it sits above
   or links to the thing it names. It is NOT a button and not a badge.

   PROMOTED HERE 2026-09-03 from frontend_app/templates/pages/vertical.html,
   where it lived as a page-local copy. Sudeep, on finding /research drawing
   the same data as full-width stacked cards: "We cannot have two different
   visual styles across vertical Page and Research Page". A component that
   exists twice is two components that agree until one of them is edited.

   `.pp .chip` in product-page.css IS NOT MERGED INTO THIS, deliberately.
   Product pages draw an uppercase --type-label chip at tighter padding, and
   test_uat_the_chip_the_line_and_the_hyperlink pins the picture credit as
   one of them. That rule keeps higher specificity, so those pages are
   unchanged by this section. The two styles genuinely differing is a
   finding, not something to fix by making one of them lose silently.

   BUTTONS ARE CHIPS TOO. /research's entity pills are <button> -- they
   filter in place rather than navigate -- so the reset below is load
   bearing: a button carries its own font and background from the UA
   stylesheet and would otherwise render 13px system-sans on grey. */
.chips{display:flex;flex-wrap:wrap;gap:8px}
/* --type-label IS mono at 11px, which is what this rule said by hand
   before the type-scale guard caught it. Same rendering, one source. */
/* A CHIP WRAPS RATHER THAN WIDENING THE PAGE (cp-456).
 *
 * Sudeep, 2026-09-11, on his own iPhone 13 mini: "Public Policy Section is
 * extended towards the right." And on why it matters now: "Majority of our
 * audience in the early days is going to check email on mobile or visit the
 * site by clicking a URL from LinkedIn. This needs to be clean."
 *
 * THE CONTAINER WAS NEVER THE PROBLEM. .chips is flex-wrap:wrap and wraps
 * BETWEEN chips correctly. What it could not do is stop a SINGLE chip being
 * wider than itself: white-space:nowrap with no max-width means one long
 * entity name sets the width of .chips, which sets the card, which widens
 * <body> -- which is the grey band down the right edge of his screenshots.
 * Measured on the live site at 375px: /vertical/public-policy overflowed by
 * 90px on a 368px chip reading "New York City Department of Consumer and
 * Worker Protection", and /daily by 74px on a 403px one. Injecting only
 * this rule took both to zero with no element exceeding the viewport, and
 * reverting it brought the overflow back.
 *
 * break-word, NOT anywhere: entity names have spaces, so ordinary word
 * wrapping is enough, and `anywhere` would split mid-word, which reads as
 * broken rather than wrapped. break-word still rescues the single token
 * longer than the line.
 *
 * NO ELLIPSIS, and this is the reason rather than a preference: on these
 * cards the entity name IS the information. A half-named regulator is worse
 * than a two-line chip -- truncation would fix the geometry by destroying
 * the thing the chip exists to say.
 *
 * align-items:center is KEPT, so a single-line chip renders byte-identically
 * to before and a wrapped one centres its .cdot against the whole pill. The
 * dot is a real sibling element (flex:none), not a ::before, so it cannot
 * be orphaned onto a second line -- and in practice the dot-bearing chips
 * are vertical names, which are short and do not wrap. The long ones are
 * .chip.ent, which carry no dot.
 *
 * .hp-chip on the homepage is deliberately NOT changed: its nowrap enforces
 * "All vertical names on ONE line -- never two" (review_notes 109), it holds
 * only short vertical names, and the homepage measured zero overflow.
 *
 * `.pp .chip` IS CHANGED, AND THIS PARAGRAPH USED TO SAY IT WAS NOT.
 * It said product-page.css's rule "stays where it is ... so the four product
 * pages are unchanged by this section" -- true as a description of
 * specificity, and wrong as a decision. That override carried
 * white-space:nowrap, so THIS fix never reached /daily, which is the page
 * Sudeep was reporting. He said so again on 2026-09-14: "still persists ...
 * DAILY PAGE ONLY". The wrapping declarations are now in both rules and only
 * the product page's look -- uppercase, tighter padding -- is still its own.
 *
 * THE LESSON IS NOT "CHECK SPECIFICITY". I did check it; I saw the override
 * and wrote it up as intentional. What was missing was asking whether the
 * REPORTED page was one of the ones the exception excluded. A documented
 * exception is not a checked one. */
.chip{display:inline-flex;align-items:center;font:var(--type-label);
  text-decoration:none;color:var(--navy);border:1px solid var(--line);
  border-radius:999px;padding:5px 12px;background:var(--sunk);
  max-width:100%;white-space:normal;overflow-wrap:break-word;
  transition:border-color .12s,background .12s}
/* A chip that filters in place is a <button>, which arrives from the UA
   stylesheet with its own font and grey ground. font: is repeated here
   rather than inherited because `font:inherit` on a button takes the
   surrounding body sans, not the chip's own role. */
button.chip{font:var(--type-label);cursor:pointer}
a.chip:hover,button.chip:hover{border-color:var(--navy);background:#EDECE6}
.chip.on{background:var(--navy);color:var(--yellow);border-color:var(--navy)}
/* An ENTITY chip is dashed -- it is a thing mentioned, not a section you
   can browse, and the dash says so without spending a second colour. */
.chip.ent{border-style:dashed}
/* THE pubLOL PILL. The product's own pink, carried by the dot the vertical
   chip already established, and a pink border so the chip reads as marked
   rather than as one more grey tag.

   NOT A PINK FILL, and the reason is measured rather than aesthetic:
   #EE2B6C is 4.04:1 behind white text and 4.44:1 behind navy, this chip is
   11px mono, and small text needs 4.5:1. Both fail. Darkening the brand
   pink to pass would be exactly the move note 153 warns about -- a brand
   element changed as a side effect of fixing something else -- so the
   colour stays and the pattern changes. See _chips.html for the whole of
   the argument, including what to do if he wants it louder.

   THE HOVER MATCHES .chip's own, not a pink one: a chip that changes its
   identity colour on hover reads as a state change rather than as a
   link. */
.chip.lol{border-color:#EE2B6C;font-weight:600}
.chip.lol .cdot{background:#EE2B6C}
/* THE RESEARCH PILL. Solid navy fill, so it reads as an ACTION beside the
   dashed entity tags rather than as one more tag -- the whole point of
   capping the list is that the reader notices where the rest went.
   Yellow-on-navy on hover: brand, and high contrast in both states (never
   yellow TEXT on white, which is 1.47:1).

   PROMOTED HERE 2026-09-04, from a page-local copy in pages/vertical.html,
   for the same reason `.chip` itself was promoted the day before: a third
   surface now emits it. _chips.html's entity_chips macro renders this pill
   on every card that has any entity, and pages/article.html joined the
   macro's callers when Stream B articles started carrying tags (migration
   100). Left where it was, article.html would have needed a fourth copy of
   four declarations -- and the drift guard in
   frontend_app/tests/test_entity_chips_are_capped.py exists because
   exactly that had already happened to the markup.

   `.pp .chip.more` in product-page.css is NOT merged into this and stays
   where it is: product pages draw a tighter, uppercase chip and that rule
   carries higher specificity, so the four product pages are unchanged by
   this section. Same documented exception as `.pp .chip` above. */
.chip.more{background:var(--navy);color:#fff;border-color:var(--navy);font-weight:600}
a.chip.more:hover{background:var(--navy);color:var(--yellow);border-color:var(--navy)}
.chip .cdot{width:6px;height:6px;border-radius:50%;margin-right:5px;flex:none}
/* The count inside a chip. Dimmer than the name, because the name is what
   is being chosen and the number is why. */
.chip .n{opacity:.6;margin-left:5px}


/* ==========================================================================
   10. THE ADVERTISING SLOT RESERVES ITS OWN HEIGHT (cp-458)
   ==========================================================================

   THIS BECAME LOAD-BEARING THE DAY THE BANNER MOVED UP. `.adsense-container`
   is an empty padded box until Google's script fills the <ins> inside it, so
   the page has always JUMPED at the moment an ad arrives. While the banner
   sat 98% down a 43-screen page, nobody was there to see it. cp-458 moves it
   to roughly one screen down, where the jump happens under the reader's
   thumb -- which is review_note 57's original complaint ("bad location, the
   fastest way to annoy readers/members") arriving by a different route -- and
   where it is a Cumulative Layout Shift regression on a page we want indexed.

   THE NUMBERS ARE ADSENSE'S, NOT A GUESS THAT LOOKS TIDY. The unit is
   `data-ad-format="horizontal"` (see templates/_ad_banner.html), and the
   tallest horizontal unit Google serves is 320x100 below tablet widths and
   728x90 at 728 and above. Reserving the MAXIMUM for each band is the choice
   that can never shift: a shorter unit leaves a little unused box, a taller
   one is not served for this format. The opposite trade -- reserving a
   typical height -- shifts the page every time a tall unit fills.

   ON THE <ins>, NOT ON THE CONTAINER. The container also holds the
   "Advertising" label and the independence note, which are policy furniture
   and already have their own height; putting the reserve on the container
   would double-count them and leave a permanent gap under every ad.

   WHAT IS NOT CONFIRMED, AND SAYING SO IS THE POINT: no real ad has ever
   served on this site -- AdSense has not approved the account -- so these are
   the documented format heights rather than a measured one. The first day a
   real unit fills, measure it and correct this rule if it disagrees.

   /research's `.rs-ad-dock` IS DELIBERATELY NOT TOUCHED. It is a different
   wrapper at a different height over the same slot, and whether that
   divergence is a bug or a deliberate choice for a research tool is an open
   question for Sudeep (cp-458). Flattening it here would settle it quietly. */
.adsense-container .adsbygoogle{min-height:100px}
@media (min-width:728px){
  .adsense-container .adsbygoogle{min-height:90px}
}

/* ==========================================================================
   11. THE ENTITY INDEX CONTROL -- CAPPED LIST, LOAD MORE, FIND (cp-497)
   ==========================================================================

   Sudeep, 2026-09-21, with a screen recording of a vertical on his phone:
   "The vertical pages still do not have entities folded like on research
   page. The scroll is tooooooo long and annoying." Then, when the first
   attempt invented a new control instead of reusing his: "I already
   provided example of research page where entities are folded and a button
   to load more provided or search entities. Just find it and use it on
   vertical pages too."

   MEASURED ON THE LIVE SITE BEFORE ANYTHING CHANGED, because "too long" is
   not a number. The "Also covered here" index on /vertical/technology drew
   460 pills ahead of 105 story rows; advertising-and-pr and
   marketing-and-design were the same shape one size down. A reader scrolled
   a wall of 460 to reach the first headline, on every visit.

   THESE RULES ARE NOT NEW. They are pages/research.html's own, moved here
   verbatim on 2026-09-21 because a second surface now draws them -- the
   same threshold that moved `.chip` out of pages/vertical.html on
   2026-09-03 and the Research pill on 2026-09-04. The class names are kept
   (`rs-` and all) rather than renamed: a rename would touch research.js's
   getElementById calls and its delegated click branch for no reader-visible
   gain, and this is a port, not a redesign.

   THE REASONING THAT TRAVELLED WITH EACH RULE, from the page that wrote
   them:
     - Load more and Find sit on ONE row, because Sudeep asked for either
       option to be available at the same point in the scroll rather than
       making a reader page through a cloud to reach a control.
     - The search box's BORDER, PADDING AND GROUND belong to the field
       component, not to any page. All that is set here is this control's
       share of the row, which is layout. test_card_and_field_are_single
       _source lists it as an exception with that reason.
     - .rs-entnote takes the full row under the button and the box; cp-338
       added it so that after the third Load more the control can offer
       Load all. */

.rs-entfoot{display:flex;flex-wrap:wrap;gap:10px;align-items:center;
  justify-content:center;margin-top:16px}
.rs-entfoot .rs-more{margin:0}
.rs-entfoot input[type=search]{flex:0 1 190px}
.rs-entnote{flex-basis:100%;text-align:center;font:var(--type-caption);
  color:var(--color-text-caption);margin:0}
/* TOKENISED ON THE WAY OUT OF THE PAGE, and the guards are why. Inside
   pages/research.html this rule said `font-family:var(--sans);font-size:13px`
   and no test could see it; in the shared layer
   test_type_scale_is_single_source rejects both on sight. --type-caption IS
   13px sans 400, so the button a reader sees is unchanged and the value now
   has a name. A literal that survives a move into the component layer is a
   seventeenth size waiting to happen. */
.rs-more{display:block;margin:16px auto 0;background:var(--sunk);border:1px solid var(--line);
  border-radius:8px;padding:10px 22px;font:var(--type-caption);cursor:pointer;color:var(--ink)}

/* ---- The server-rendered case (the vertical pages) ------------------
   /research builds its cloud from an API and pages it with entSkip; a
   vertical already holds every pill in the document, so the same control
   is driven over the rendered list instead of over a fetch.

   THE CAP IS APPLIED BY SCRIPT, NEVER BY THE SERVER, and that is the whole
   accessibility argument for this file rather than a Jinja slice: with no
   JavaScript there is no `is-capped` class, nothing is hidden, and the page
   behaves exactly as it did before today -- every entity present and
   reachable. A server-side [:24] would have made the other 436 unreachable
   for those readers and for anything that does not run scripts. */
/* `display:contents` so the pill itself stays the flex item of `.chips`
   and the row is pixel-identical to the hand-written loop this replaced.
   The wrapper exists only to carry `data-over`, which is this control's
   business and not something _chips.html's macro should know about. */
.entindex-cell{display:contents}
.entindex.is-capped .entindex-cell[data-over]{display:none}
.entindex .entindex-cell[hidden]{display:none}


/* ==========================================================================
   12. THE DISCLOSURE -- ONE MARKER, ONE DIRECTION, FOUR SURFACES (cp-500)
   ==========================================================================

   Sudeep, 2026-09-22, twice in one sitting: "Don't hand code anything. Use
   templates and design system please", and then "We are not hand coding and
   need to use templates; what do we have that expands like an accordion but
   looks like a card per month?"

   THE ANSWER WAS: WE ALREADY HAD IT, THREE TIMES OVER, IN THREE PLACES.
   /account's "Download your data" and account-closure boxes are registered
   card variants (SECTION 4) each wrapping a <details>; /about's FAQ is the
   same disclosure again. Every one of them drew its own copy of the same
   six rules, and account.html's own comment above its copy said why that
   was a problem:

       "control doing the same job, and a second copy is how the two
        triangles end up pointing different ways."

   The archive would have been the FOURTH copy. That is the threshold that
   moved `.chip` out of pages/vertical.html on 2026-09-03, the Research pill
   on 2026-09-04 and the entity index on 2026-09-21. So the rules are here
   and the three pages keep their own class names in the selector list,
   exactly as SECTION 4 does for the card variants.

   NOTHING ABOUT THE RENDERED CONTROL CHANGES. Same glyphs, same direction,
   same focus ring. This is a move, not a redesign -- the reasoning written
   against each rule travelled with it and is kept below.

   WHY A DRAWN MARKER AT ALL, from /about's copy: <details> is
   keyboard-operable and announced as a disclosure with no ARIA to get
   wrong, it survives JavaScript failing, and the content stays IN THE DOM
   while closed so Ctrl+F, print and crawlers still find it. The native
   triangle is replaced only so the three surfaces match. */

.ab-faq-item summary,
.closebox summary,
.databox summary,
.arch-card > summary{
  cursor:pointer;list-style:none;
}
.ab-faq-item summary::-webkit-details-marker,
.closebox summary::-webkit-details-marker,
.databox summary::-webkit-details-marker,
.arch-card > summary::-webkit-details-marker{display:none}

/* The glyphs, and the DIRECTION is the part that drifts: closed points
   right, open points down. \00a0 is a non-breaking space, so the marker
   cannot be left stranded on its own line. */
.ab-faq-item summary::before,
.closebox summary::before,
.databox summary::before,
.arch-card > summary::before{content:"\25B8\00a0";color:var(--navy)}
.ab-faq-item[open] summary::before,
.closebox details[open] summary::before,
.databox details[open] summary::before,
.arch-card[open] > summary::before{content:"\25BE\00a0"}

/* list-style:none plus a drawn marker otherwise leaves a summary that can
   be focused and gives no sign of it. */
.ab-faq-item summary:focus-visible,
.closebox summary:focus-visible,
.databox summary:focus-visible,
.arch-card > summary:focus-visible{
  outline:2px solid var(--navy);outline-offset:3px;border-radius:4px;
}

/* ---- The archive card ------------------------------------------------
   Registered as a card variant in SECTION 4 above, so its border, radius,
   ground and padding are the card's and not this section's. What is set
   here is only what a card that OPENS needs and a static one does not. */
.arch-card{margin-bottom:var(--space-sm)}
.arch-card > summary{
  font:var(--type-body);font-weight:600;color:var(--navy);
  display:block;
}
/* The rows clear the title when the card is open. A closed card has no
   body to space, so this cannot add height to the collapsed state --
   which is the whole reason the accordion was asked for. */
.arch-card[open] > .arch-dates{margin-top:var(--space-sm)}
