/* ==========================================================================
   Insights Feed — presentation (Figma 10416:5424, "Blog / Thinking Landing")

   Design contract: .planning/phases/10-blog-index/10-INSIGHTS-BUILD-SPEC.md
     §2 geometry · §3 typography · §4 token map · §6 BEM classes · §7 motion
     recipe · §8 responsive plan (every JUDGMENT row) · §9 visual checklist.

   Behavior (toggle/filter/load-more/reveal/error+retry) lives entirely in
   tbl-insights-feed.js — this file is presentation only.

   BB owns section spacing (project convention): no OUTER row/column padding
   or min-height is set anywhere in this file — that stays entirely BB's.
   10-INSIGHTS-ASSEMBLY-CHECKLIST.md §2 places the whole module (hero + feed)
   into ONE BB row and explicitly promises the hero/feed dual-width tension
   (10-05's flagged "one module instance, one BB row, two different Figma
   widths") is "resolved inside the module's own CSS, not by splitting BB
   rows." 10-09 QA found that promise unmet — the live row measured ~1500px,
   not the feed's target 974px — and closed it here: .tbl-insights__feed and
   .tbl-insights__more self-constrain to a centered 974px INNER content width
   (an internal max-width, not outer row spacing), while .tbl-insights__hero
   stays unconstrained so it keeps the wider bleed its pill-bar edge-fade
   needs. This works regardless of whatever width the single shared BB row
   ends up at.

   Cascade layers: this stylesheet is enqueued unlayered (like every sibling
   TBL Sections module), so its plain declarations already outrank style.css's
   layered forms.css/reset.css without needing a forced-priority override
   anywhere in this file (project convention — an unlayered forced-priority
   declaration is the WEAKEST tier, not the strongest).
   ========================================================================== */

/* --- 1. root / feed container ------------------------------------------- */

.tbl-insights {
	/* No outer width, padding, or min-height here — BB row owns section
	   spacing (project convention). */
	position: relative;
}

.tbl-insights__feed {
	/* 10-09 QA fix: self-constrain to the SPEC's 974px feed column via an
	   INNER max-width, not outer row spacing (BB still owns the row's own
	   padding/min-height/bleed — see the file header). Without this the row
	   inherits whatever width the single shared hero+feed BB row happens to
	   be, which measured ~1500px live — 526px wider than the design, and
	   wide enough that the pill bar stopped overflowing by default at
	   desktop widths (SPEC §8 explicitly expects overflow "at most desktop
	   widths, not just narrow ones"). max-width (not a fixed width) so this
	   never fights the <1200px fluid/stacked breakpoints, which already
	   render inside a container narrower than 974px anyway. */
	max-width: 974px;
	margin-inline: auto;
	display: flex;
	flex-direction: column;
	/* 10.1 item 5: the filters -> first-post gap was incidental (no margin-top
	   existed). Ben asked for 76px scaling down to 36px, so this is a fluid clamp,
	   not a breakpoint step - it reaches 76px at >=1200 and lands on 36px near 390.
	   This is INTERNAL module spacing between two elements inside the module, not
	   outer section spacing, so it does not violate the BB-owns-section-spacing
	   convention (same reasoning that justified the 974px inner max-width above).
	   Pre-existing residual gap measured at 10.1-MEASUREMENTS.md M-7 (0px at
	   1600/1200/800/390, so the clamp is used exactly as derived, no substitution). */
	margin-top: clamp(36px, 16.74px + 4.94vw, 76px);
}

/* --- 2. hero: Articles/Press toggle -------------------------------------- */

.tbl-insights__toggle {
	display: flex;
	align-items: baseline;
	flex-wrap: wrap;
	/* Figma expresses the two words as ONE text run with 7.2px of
	   letter-spacing on the single inter-word space (SPEC §3) — not
	   reproducible across two separate <button> elements, so this
	   approximates the same visual gap with flexbox gap instead. */
	gap: 0.2em;
	margin: 0 0 5px;
	list-style: none;
	padding: 0;
}

.tbl-insights__toggle-btn {
	/* Real button reset — this file is unlayered, so a plain declaration
	   already outranks forms.css's layered button styling (project
	   convention: never reach for a forced-priority override here). */
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
	font: inherit;

	font-family: var(--header-font);
	font-weight: var(--header-font-weight);
	/* SPEC §3 flag: get_design_context reports 180px/320 line-height for this
	   text run, but its own bounding box (get_metadata) is only 172px tall —
	   a bespoke-oversized-heading mismatch, same class of issue already
	   flagged for the Contact page's "Book an intro call". Built as a fluid
	   clamp anchored near (not at) the literal figure, tight line-height
	   (not the reported 320, which would add a full extra line of vertical
	   whitespace the bbox itself doesn't have room for). */
	--tbl-insights-toggle-size: clamp(2.5rem, 1rem + 8vw, 9rem);
	font-size: var(--tbl-insights-toggle-size);
	line-height: 1;
	color: var(--Neutral-10-Base); /* inactive word — nearest token to Figma's hex e6e6eb; FLAGGED (SPEC §4, not an exact match) */
	/* R3-1 (Ben, 2026-07-29): --button-easing is ALREADY a complete transition shorthand
	   (variables.css:457) — qualifying it with a property name produced an invalid
	   declaration the browser silently dropped, so this control had NO hover transition
	   at all. Bare form restores it; see the file's other 4 identical fixes in this round. */
	transition: var(--button-easing);
}

.tbl-insights__toggle-btn.is-active {
	color: var(--Neutral-100); /* active word — exact match to Figma's hex 0f0f0f (SPEC §4) */
}

.tbl-insights__toggle-count {
	/* Fixed size relative to the ACTIVE word's font-size (SPEC: count is 20px
	   next to a 180px word, ~11% ratio) — calc() off the same clamp so the
	   superscript scales down with the headline at narrow widths instead of
	   staying a literal 20px next to a 40px word. */
	font-size: calc(var(--tbl-insights-toggle-size) * 0.111);
	line-height: 1.2;
	font-weight: var(--header-font-weight);
	vertical-align: text-top; /* SPEC §9 item 2: sits near the TOP of the word, not on its baseline */
	/* Pastel #6 (Chris) — text-top parked the count on the word's CAP line,
	   which at a 180px headline leaves it visually detached. Nudged down
	   toward the x-height. `position: relative` on an inline box offsets the
	   paint without touching line layout, and the offset is in the COUNT's own
	   em so it tracks the parent clamp at every width — the same reasoning as
	   the -0.5em margin below. */
	position: relative;
	top: 0.75em;
	/* 10.1 item 1: the markup gap is already zero - the visible space is the
	   <sup>'s own left side-bearing, magnified by the parent's clamp()ed
	   font-size. Expressed in em of the COUNT's own font-size so it tracks the
	   clamp identically at every width (a px value tuned at 1600 would collide
	   at 390). Derived from 10.1-MEASUREMENTS.md M-1 / M-6:
	   g_figma_em -0.766 vs g_live_em@1600 0.1622 -> raw pull -0.928em, clamped
	   to the -0.5em ceiling (the count is only ~11% of the word's size, so a
	   larger pull would slide it under the word's final glyph). */
	margin-left: -0.5em;
	/* No color declared — count inherits color:inherit from its parent
	   button, so the active tab's count is dark and the inactive tab's count
	   is pale automatically (SPEC §9 item 3), with zero extra rules. */
}

/* --- 3. hero: subtitle + rule --------------------------------------------- */

.tbl-insights__subtitle {
	margin: 0 0 19px;
	font-family: var(--body-font);
	font-size: var(--body-font-size-larger); /* exact match, SPEC §4: 20px */
	line-height: 30px;
	letter-spacing: 0.2px;
	color: var(--Neutral-100);
}

.tbl-insights__rule {
	margin: 0 0 21px;
	border: 0;
	border-top: 1px solid var(--Neutral-20); /* nearest token to Figma's hex e0e0e0 — FLAGGED (SPEC §4, not an exact match); distinct from the much lighter row divider below */
	width: 100%;
	height: 0;
}

/* --- 4. hero: filters (View: label + category pill bar) ------------------ */

.tbl-insights__filters {
	display: flex;
	align-items: center;
	gap: 21px; /* 10.1 item 3: re-measured on frame 10416:5424 - SPEC 2's 56px was wrong (M-2) */
}

.tbl-insights__filters-label {
	flex: 0 0 auto;
	margin: 0;
	font-family: var(--body-font);
	font-size: var(--body-font-size-smaller); /* exact match, SPEC §4: 14px */
	line-height: 24px;
	color: var(--Black);
	white-space: nowrap;
}

.tbl-insights__pills {
	/* JUDGMENT (D-17/SPEC §8, "any width" row): overflow is the DEFAULT
	   presentation here, not a narrow-width exception — there are 10 pills
	   (9 categories + "All insights"), not the frame's fictional 6, so
	   scrolling with an edge fade is expected at most desktop widths too. */
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 6px; /* SPEC §2: pill-to-pill gap */
	overflow-x: auto;
	overscroll-behavior-x: contain; /* a horizontal flick must not trigger browser back-nav */
	scroll-snap-type: x proximity;
	scrollbar-width: none;
	/* Flex child needs min-width:0 or it will refuse to shrink below its
	   content's natural width, silently defeating overflow-x — the classic
	   flexbox overflow gotcha. */
	min-width: 0;
	flex: 1 1 auto;
	/* Edge fade via mask, not a coloured gradient overlay, so it reads
	   correctly over any background (SPEC action). `black`/`transparent`
	   keywords are used deliberately instead of a hex literal — only the
	   alpha channel matters to a mask, and this file must carry zero raw
	   hex (project convention).

	   10.1 item 4: the fades are now CONDITIONAL on real scroll position. The
	   original unconditional mask ramped from transparent at 0 to opaque at 16px,
	   so "All insights" - always the first pill - was permanently faded at rest,
	   with nothing to fade toward. Both stops are driven by custom properties that
	   tbl-insights-feed.js flips via is-scrolled-start / is-scrolled-end; a 0px
	   stop collapses that end's ramp to zero width, i.e. no fade. The defaults
	   below ARE the at-rest, unscrolled, no-JS state: no left fade, right fade on. */
	--tbl-pills-fade-left: 0px;
	--tbl-pills-fade-right: 24px;
	-webkit-mask-image: linear-gradient(to right, transparent 0, black var(--tbl-pills-fade-left), black calc(100% - var(--tbl-pills-fade-right)), transparent 100%);
	mask-image: linear-gradient(to right, transparent 0, black var(--tbl-pills-fade-left), black calc(100% - var(--tbl-pills-fade-right)), transparent 100%);
}

/* Set by tbl-insights-feed.js on scroll/resize (rAF-coalesced).
   is-scrolled-start = content is hidden off the LEFT edge, so the left fade earns
   its place. is-scrolled-end = there is nothing more to the right, so the right
   fade switches off. Neither class exists without JS, which is exactly the at-rest
   presentation we want as the default. */
.tbl-insights__pills.is-scrolled-start {
	--tbl-pills-fade-left: 16px;
}

.tbl-insights__pills.is-scrolled-end {
	--tbl-pills-fade-right: 0px;
}

.tbl-insights__pills::-webkit-scrollbar {
	display: none;
}

.tbl-insights__pill {
	flex: 0 0 auto;
	white-space: nowrap;
	scroll-snap-align: start;
	background: none;
	border: 0;
	cursor: pointer;
	padding: 6px; /* SPEC §2: pill padding, all sides */
	border-radius: 5px; /* no exact token brackets this value — RAW, FLAGGED (SPEC §4) */
	font-family: var(--body-font);
	font-size: 10px;
	line-height: 17.78px;
	font-weight: var(--header-font-weight);
	letter-spacing: 1px;
	text-transform: uppercase; /* SPEC §9 item 7: pill labels always render uppercase regardless of authored copy */
	/* Pastel #7 (Chris) — the SPEC §4 "pick one and confirm" is now confirmed:
	   #F2EEF6, a touch lighter than the --Primary-10 (#e8ddf7) placeholder.
	   Still unbound — it falls between --Primary-10 and --Primary-Alt-10
	   (#f9f4ff) — so it stays FLAGGED rather than snapped to a neighbour.
	   Matches the case-studies index pills (Pastel #10), same round. */
	background-color: #F2EEF6;   /* FLAG: unbound */
	color: var(--Black);
	/* R3-1 — see .tbl-insights__toggle-btn's comment above */
	transition: var(--button-easing);
}

.tbl-insights__pill.is-active {
	background-color: var(--Black); /* exact match, SPEC §4 */
	color: var(--White); /* exact match, SPEC §4 */
}

/* --- 5. feed rows: grid + divider ------------------------------------------ */

.tbl-insights-row {
	position: relative;
	display: grid;
	grid-template-columns: 340px 522px; /* SPEC §2, ≥1200 */
	column-gap: 64px;
	/* 10.1 item 8: the media box (340x427) is the taller grid child; centring
	   the shorter text column against it reads correctly whether the title
	   wraps 1 or 3 lines. Inert below 900px (single-column, see §10 below). */
	align-items: center;
	/* Row inner content sits inset 24px each side (24 + 926 + 24 = 974 — SPEC
	   §2); the divider below is positioned against THIS box's edges so it
	   spans the full 974px column while the content stays inset — the exact
	   detail SPEC §9 item 17 calls out. */
	padding: 0 24px 60px; /* 60px: content-to-divider gap (SPEC §2's 60/60 split) */
	/* Bespoke, non-token color (SPEC §4: rgba(15,29,7,.7) doesn't match any
	   achromatic neutral family). Shipped as a locally-scoped module CSS var
	   (same treatment the Contact SPEC used for its NY timezone-dot cyan),
	   flagged here for Ben/Chris to confirm the tint is intentional rather
	   than stray export noise. */
	--tbl-insights-excerpt-color: rgba(15, 29, 7, 0.7);
}

.tbl-insights-row:not(:last-child) {
	margin-bottom: 60px; /* 60px: divider-to-next-row gap (SPEC §2's 60/60 split) */
}

.tbl-insights-row::after {
	/* 10.1 item 9a: the :not(:last-child) qualifier is gone - every row,
	   including the last, now draws the hairline. The last row's own
	   padding-bottom (60px, base rule above) was previously zeroed by
	   .tbl-insights-row:last-child, which put its divider flush against the
	   feed's own bottom edge instead of 60px below the content, same as
	   every other row. That override is removed below so the last row falls
	   back to the base 60px padding and its divider sits at the same offset. */
	content: '';
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 1px;
	background: rgba(0, 0, 0, 0.1);
}

/* --- 6. row media: thumbnail / brand-purple fallback ----------------------- */

.tbl-insights-row__media {
	width: 340px;
	aspect-ratio: 340 / 427; /* NOT a fixed pixel height — survives every fluid breakpoint below */
	border-radius: 10px; /* no exact token brackets this value — RAW, FLAGGED (SPEC §4) */
	overflow: hidden;
	background: var(--Neutral-10-Base); /* quiet placeholder tone behind a lazy-loading image */
}

.tbl-insights-row__thumb {
	display: block;
	width: 100%;
	height: 100%;
	/* This is what makes the ~156 pre-crop featured images (uploaded before
	   the tbl-insights-portrait crop was registered in 10-02) look correct:
	   they serve WordPress's full-size file into this box, and object-fit is
	   the only thing enforcing the 340x427 frame — the registered crop is
	   progressive enhancement, not the layout mechanism (SPEC §10.5, F-6). */
	object-fit: cover;
	object-position: center;
}

.tbl-insights-row__fallback {
	/* Renders for 11/167 posts with no featured image — a designed state
	   (D-11), not an error state, so it gets full deliberate styling, not a
	   bare placeholder look. */
	width: 100%;
	height: 100%;
	box-sizing: border-box;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 24px;
	text-align: center;
	background: var(--Primary-90); /* nearest token to the sampled hex 220c4a fallback card, SPEC §4 (flagged, not exact) */
}

.tbl-insights-row__fallback-title {
	font-family: var(--header-font);
	font-weight: var(--header-font-weight);
	font-size: 22px;
	line-height: 1.25;
	color: var(--White);
}

/* --- 7. row body: title / meta / excerpt ----------------------------------- */

.tbl-insights-row__body {
	display: flex;
	flex-direction: column;
	min-width: 0; /* grid item text column must be allowed to shrink/wrap */
}

.tbl-insights-row__title {
	/* No font-size/line-height/color override needed: a bare <h3> already
	   inherits the sitewide fluid H3 clamp (--h3-font-size, topping out at
	   var(--h3-max-font-size) = 38px, with --h3-line-height: 1.158 ≈ 44px)
	   plus var(--header-dark-color) — an exact match to SPEC §3's 38px/44px
	   Neutral-100 target with zero rules here (same reuse pattern 10-06 used
	   for the CTA band's H2 headline). */
	margin: 0 0 12px;
}

.tbl-insights-row__title-link {
	color: inherit;
	/* Underline drawn ALWAYS; only its color animates (Task 2). Never toggle
	   text-decoration-line on hover — it is a discrete property and cannot
	   tween (project memory: this is the exact bug already fixed sitewide
	   once). */
	text-decoration-line: underline;
	text-decoration-color: transparent;
	text-decoration-thickness: 1px;
	text-underline-offset: 3px;
}

.tbl-insights-row__meta {
	display: flex;
	align-items: center;
	gap: 8px;
	margin: 0 0 16px;
	font-family: var(--body-font);
	font-size: 15px;
	line-height: 25px;
	color: var(--Neutral-100);
}

.tbl-insights-row__asterisk {
	/* Ben supplied the exact 8-point star as inline SVG (2026-07-29), replacing
	   the typewriter `*` the display font was rendering. The glyph is now art,
	   not type, so the font-size/line-height sizing this rule used to do is
	   gone — the SVG carries its own 11x11 box.

	   `color` is retained because the SVG's path is fill: currentColor, so the
	   token below still drives it. Ben's asset ships #AF85EC, which IS
	   --Primary-Alt-50-Base exactly (variables.css:88) — kept as the token
	   rather than a literal so it tracks any future palette change. */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 11px;
	height: 11px;
	vertical-align: middle;
	color: var(--Primary-Alt-50-Base);
}

.tbl-insights-row__asterisk svg {
	display: block;
	width: 100%;
	height: 100%;
}

.tbl-insights-row__readtime {
	color: color-mix(in srgb, var(--Neutral-100) 50%, transparent); /* SPEC §4: rgba(15,15,15,.5), already token-expressible */
}

.tbl-insights-row__excerpt {
	margin: 0 0 24px;
	max-width: 469px; /* SPEC §2's independently-set excerpt wrap width, as a max-width so it releases on narrow screens rather than clipping */
	font-family: var(--body-font);
	font-size: 15px;
	line-height: 25px;
	letter-spacing: 0.15px;
	color: var(--tbl-insights-excerpt-color);
}

/* --- 8. row foot: CTA / byline / avatar ------------------------------------- */

.tbl-insights-row__foot {
	display: flex;
	align-items: center;
	justify-content: space-between;
	margin-top: auto; /* pins the foot to the bottom of the body column when the excerpt is short */
	gap: 24px;
}

#tbl-insights .tbl-insights-row__cta.btn.btn-secondary.btn-xs {
	/* SPEC §4 maps this button's fill to var(--Primary-90) (hex 200147 —
	   effectively exact vs. the pulled hex 200548, a 4-unit rounding delta) as
	   its RESTING state (SPEC §9 item 14: "small, dark[-purple]" at rest,
	   confirmed against the row screenshot). The theme's stock
	   .btn-secondary is BLACK (hex 0f0f0f) at rest with purple only on hover —
	   the opposite of what this design wants. row.php's markup already carries
	   the btn/btn-secondary/btn-xs classes (btn-sm until Ben's 2026-07-29
	   XSmall retarget), so rather than duplicating the whole button ruleset or
	   reaching for a forced-priority override on an unlayered file, this neutralizes
	   .btn-secondary's own custom-property overrides back toward the
	   button system's Primary hover language (resting purple, hover
	   lightens to --Primary-Alt-50-Base — the sitewide Primary button
	   convention) using the exact same custom-property mechanism
	   .btn-secondary itself uses. Only min-width/min-height reach for the
	   SPEC's 121.29 x 32.3 box; no transition is redeclared (--button-easing
	   already applies from the base .btn rule).

	   10-09 QA fix: prefixed with the #tbl-insights ancestor (frontend.php).
	   theme.css's shared button block opens with a selector list that
	   includes #wp-submit; CSS Nesting's &-resolution wraps that list in
	   :is(...), whose specificity is that of its most specific branch — the
	   ID — so EVERY nested rule in that block (including &.btn-secondary)
	   computes an ID-tier specificity that no class-only selector can ever
	   outrank, confirmed empirically. theme.css is fenced (cannot be
	   edited), so this rule borrows the same ID tier instead of reaching for
	   a forced-priority override (still banned in this file). See
	   frontend.php's docblock at the #tbl-insights id for the full trace. */
	--Light-button-Filled-Color: var(--White);
	--Light-button-Filled-Fill: var(--Primary-90);
	--Light-button-Filled-Outline: var(--Primary-90);
	--Light-button-Filled-Hover-Color: var(--Primary-90);
	--Light-button-Filled-Hover: var(--Primary-Alt-50-Base);
	--Light-button-Filled-Hover-Outline: var(--Primary-Alt-50-Base);
	--Light-button-Filled-Focused-Color: var(--White);
	--Light-button-Filled-Focused: var(--Primary-90);
	--Light-button-Filled-Focused-Outline: var(--Primary-Alt-50-Base);
	--Light-button-Filled-Pressed-Color: var(--White);
	--Light-button-Filled-Pressed: var(--Primary-90);
	--Light-button-Filled-Pressed-Outline: var(--Primary-90);
	/* Ben, 2026-07-29: retargeted from .btn-sm to .btn-xs against Figma
	   10429:7332 (Primary Button, size=XSmall). theme.css:23627's &.btn-xs
	   already maps --button-padding/--button-font-size to --btn-xs-padding
	   (9px 15px) and --btn-xs-font-size (13px); the frame's 13px type and
	   9.15px vertical padding land on those tokens, so no size values are
	   redeclared here — only the box floor below.

	   min-height stays 32px: the frame computes 9.15 + 13.775 + 9.15 = 32.075.
	   min-width is NOT carried over — the frame's XSmall box is content-sized
	   around the arrow (15px left / 12px right / 10px gap), and the old 121px
	   floor came from the btn-sm SPEC box that no longer applies. */
	min-height: 32px;

	/* Ben, 2026-07-29: label weight 410. Set through the button system's own
	   custom property rather than a bare `font-weight`, because theme.css:23351
	   resolves the button's weight as font-weight: var(--button-font-weight) —
	   overriding the token is the mechanism this ruleset already uses for its
	   colours, and it avoids a specificity fight with that ID-tier block.
	   Scoped to this button only; the sitewide --button-font-weight default
	   (variables.css:449) stays 400.

	   Ben's design spec called for 410. Sequel Sans is loaded as STATIC faces
	   at 300/400/500/600 (fonts.css:154-181), not a variable font, so a true
	   410 is not renderable: per CSS font matching a target between 400 and
	   500 searches upward first, so 410 resolves to the 500 MediumDisp face.
	   Measured 2026-07-29 — "Read Article" at 13px is 69.11px at 400 and
	   70.44px at BOTH 410 and 500, i.e. the two are indistinguishable.

	   Ben's call on being shown that: declare 500. Writing the value that
	   actually renders keeps this honest — a literal 410 here would read as an
	   intent the font stack cannot honour, and would silently change meaning
	   if the variable cut were ever licensed. Revisit if that happens. */
	--button-font-weight: 500;
}

.tbl-insights-row__external-icon {
	margin-left: 8px;
	font-size: 0.9em;
}

/* Figma 10429:7332's trailing arrow, shown on INTERNAL links only — external
   links keep the ↗ affordance above, so a row never renders two icons.
   The asset ships #B390E7; that sits ~4 units per channel off
   --Primary-Alt-50-Base (#AF85EC), the same order of rounding delta this file
   already treats as effectively exact for the fill (#200147 vs #200548), so it
   is expressed as the token via fill: currentColor rather than a literal. */
/* NOTE: `color` is deliberately NOT set here, and currentColor is deliberately
   NOT relied on. theme.css's shared button block contains

       & * { color: inherit !important; }

   which resolves to :is(#wp-submit, .btn, …) * — ID tier, !important, and
   therefore unbeatable by any `color` declaration at any specificity. Every
   descendant of a .btn is forced to the button's own text colour (white).
   Measured live 2026-07-29: a class-only rule computed white, and so did an
   #tbl-insights-prefixed one.

   The row meta star is unaffected by all this only because it sits outside a
   button, which is why the identical token works there.

   The way through is a different property, not more weight: that rule
   constrains `color`, so the arrow is painted via `fill`/`stroke` on the path
   instead. A CSS declaration also outranks the SVG's own presentation
   attribute, so the markup's currentColor is simply overridden. No !important
   is used — this file bans forced-priority overrides (and per the project's
   cascade-layer note an unlayered !important would be the weakest tier anyway). */
#tbl-insights .tbl-insights-row__cta-arrow {
	display: inline-flex;
	align-items: center;
	flex: 0 0 auto;
	/* Matches Ben's supplied asset's own 15x14 canvas (2026-07-29 v2). Sizing
	   the box to the earlier 14.29x13.281 would make preserveAspectRatio scale
	   his viewBox down ~5%, so the art would no longer render at native size. */
	width: 15px;
	height: 14px;
	margin-left: 10px; /* the frame's text→icon gap */
}

#tbl-insights .tbl-insights-row__cta-arrow svg {
	display: block;
	width: 100%;
	height: 100%;
}

/* #B390E7 is Ben's explicit value (and the Figma asset's own fill), NOT a
   token. An earlier pass substituted --Primary-Alt-50-Base (#AF85EC) as
   "effectively exact" — Ben corrected that on 2026-07-29, so the literal is
   deliberate here. It is intentionally not expressed via --nav-bolt-color /
   --footer-accent, which happen to carry the same value but are semantically
   unrelated to a button icon and would drag this along if either changed.

   Declared in CSS as well as on the path's presentation attribute so the
   painted colour stays greppable from the stylesheet and survives any future
   markup regeneration. */
#tbl-insights .tbl-insights-row__cta-arrow svg path {
	fill: #B390E7;
	stroke: #B390E7;
	transition: var(--button-easing);
}

/* Ben, 2026-07-29: the arrow must darken on hover "like before". Before this
   icon was painted via fill it inherited `color`, so theme.css's
   `& * { color: inherit !important; }` dragged it through the button's own
   hover inversion for free. Painting it with a fixed fill froze that, so the
   hover state has to be restored explicitly.

   Reads the button's OWN hover token rather than hardcoding --Primary-90, so
   the arrow tracks the label automatically if that token is ever retuned.
   Custom properties inherit, so the value set on the .tbl-insights-row__cta
   rule above is visible down here on the path.

   :hover ONLY, deliberately — this ruleset keeps the label WHITE on
   :focus-visible and :active (--Light-button-Filled-Focused-Color /
   -Pressed-Color, both --White), and the arrow should not diverge from the
   label it sits beside.

   transition above is the bare var(--button-easing) per project convention;
   its `all` covers fill and stroke, so the arrow eases in step with the
   button's own fill rather than snapping. */
#tbl-insights .tbl-insights-row__cta:hover .tbl-insights-row__cta-arrow svg path {
	fill: var(--Light-button-Filled-Hover-Color);
	stroke: var(--Light-button-Filled-Hover-Color);
}

/* Visually-hidden pattern for the "(opens in a new tab)" a11y text — mirrors
   the only existing precedent in this codebase (.mobile-menu__toggle
   .screen-reader-text), scoped to this button since there is no sitewide
   utility class for it. */
.tbl-insights-row__cta .screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	clip-path: inset(50%);
	border: 0;
	white-space: nowrap;
}

.tbl-insights-row__byline {
	display: flex;
	align-items: center;
	gap: 10px; /* SPEC §2: avatar→name gap */
	flex: 0 0 auto;
}

.tbl-insights-row__avatar {
	flex: 0 0 auto;
	display: block;
	width: 33.25px;
	height: 33.25px;
	border-radius: 50%; /* a true circle, SPEC §9 item 15 */
	object-fit: cover;
	background: var(--Neutral-10-Base);
}

.tbl-insights-row__avatar-initials {
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--Neutral-20);
	color: var(--Neutral-100);
	font-family: var(--body-font);
	font-size: 12px;
	font-weight: 600;
	text-transform: uppercase;
}

.tbl-insights-row__avatar--house {
	/* The house byline (79/167 posts, tbladmin — not an edge case). Markup
	   ships this as an EMPTY span (data.php), so the monogram is built
	   entirely in CSS — no new asset file. */
	position: relative;
	background: var(--Primary-90);
}

.tbl-insights-row__avatar--house::before {
	content: 'TBL';
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	font-family: var(--header-font);
	font-size: 8px;
	font-weight: 600;
	letter-spacing: 0.5px;
	color: var(--White);
}

.tbl-insights-row__author {
	font-family: var(--body-font);
	font-size: 13.5px;
	line-height: 22px;
	letter-spacing: 0.135px;
	color: var(--tbl-insights-excerpt-color);
	white-space: nowrap;
}

.tbl-insights-row__date {
	/* Author→date visual gap is 44.22px (SPEC §2) but the byline's own flex
	   gap (10px, shared with avatar→name) is uniform across all children —
	   this margin makes up the ~34px difference rather than introducing a
	   second nested flex container for a gap that differs by one pairing. */
	margin-left: 34px;
	font-family: var(--body-font);
	font-size: 13.5px;
	line-height: 22px;
	letter-spacing: 0.135px;
	color: var(--tbl-insights-excerpt-color);
	white-space: nowrap;
}

/* --- 9. load more / empty / error ------------------------------------------ */

[hidden] {
	display: none;
}

.tbl-insights__more {
	/* 10.1 item 9b: the last row's divider now sits at that row's bottom:0 edge,
	   so this reads as exactly 60px from the hairline to the link.
	   .tbl-insights__more is a SIBLING of .tbl-insights__feed (frontend.php:190),
	   not a child, so there is no margin-collapse partner to fight. */
	margin-top: 60px;
	text-align: center;
	background: var(--White);
}

.tbl-insights__more-link {
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
	font: inherit;
	font-family: var(--body-font);
	font-size: var(--body-font-size-larger); /* 20px, SPEC §4 */
	line-height: 1.545; /* 30.91/20, SPEC §3 */
	color: var(--Black);
	text-decoration-line: underline;
	/* 10.1 item 9c: underlined AT REST (currentColor, not transparent) - matches
	   the sitewide .underline-link pattern (theme.css:443). Hover then moves the
	   COLOUR only (see section 12 below) and the underline follows via
	   currentColor. NEVER toggle text-decoration-line here - discrete property,
	   cannot tween (AP-2, already fixed sitewide once at theme.css:416). */
	text-decoration-color: currentColor;
}

.tbl-insights__empty,
.tbl-insights__error {
	margin: 0;
	padding: 40px 24px;
	text-align: center;
	font-family: var(--body-font);
	font-size: 15px;
	line-height: 25px;
	color: var(--Neutral-100);
}

.tbl-insights__retry {
	/* JS-created retry button (tbl-insights-feed.js) shares .tbl-insights__more-link
	   for its look (same always-drawn underline treatment) and adds this
	   class only as a spacing/behavior hook.
	   10.1 item 9d: this button now inherits item 9c's "underlined at rest"
	   treatment too, since it shares .tbl-insights__more-link. Decision: KEPT,
	   no carve-out added - a retry affordance reading as a link is consistent
	   with the sitewide underline-link pattern, and an unnecessary exception
	   would be worse than the consistent treatment. */
	display: inline-block;
	margin-top: 12px;
}

/* --- 10. responsive (D-17 — no tablet/mobile frame exists; every call below
   is a judgment 10-09 must confirm against the rendered page, not against
   this file's numbers) ------------------------------------------------------- */

@media (max-width: 1199px) {
	/* JUDGMENT (D-17/SPEC §8, 900–1199 row): content column goes fluid and
	   the image scales proportionally rather than staying pinned at the
	   desktop's literal 340px; the gap tightens from 64px to 40px. */
	.tbl-insights-row {
		grid-template-columns: minmax(0, 300px) 1fr;
		column-gap: 40px;
	}

	.tbl-insights-row__media {
		width: 100%;
		max-width: 300px;
	}
}

@media (max-width: 899px) {
	/* JUDGMENT (D-17/SPEC §8, <900 row): the row stacks — image above text,
	   both full width, at the SAME aspect ratio (never a fixed height). The
	   foot (CTA + byline) STAYS horizontal per the spec's explicit call. */
	.tbl-insights-row {
		grid-template-columns: 1fr;
		row-gap: 24px;
	}

	.tbl-insights-row__media {
		width: 100%;
		max-width: none;
	}

	.tbl-insights-row__body {
		width: 100%;
	}
}

@media (max-width: 599px) {
	/* JUDGMENT (D-17/SPEC §8, <600 row): tighter type scale throughout; the
	   excerpt's wrap width releases to 100% instead of clipping at 469px. */
	.tbl-insights-row__excerpt {
		max-width: 100%;
	}

	.tbl-insights-row__meta,
	.tbl-insights-row__excerpt {
		font-size: 14px;
		line-height: 22px;
	}

	/* JUDGMENT (D-17/SPEC §8, the hero toggle's specific narrow-width risk):
	   two full words plus superscript counts are the single most likely
	   element to visually break before 600px (SPEC §8) — the toggle's own
	   clamp (declared once, above, not re-declared here) already reaches its
	   MIN of 2.5rem/40px well before this breakpoint, which is the
	   deliberate mitigation; this rule only tightens the inter-word gap so
	   two shrunk words don't crowd each other. */
	.tbl-insights__toggle {
		gap: 0.1em;
	}

	/* JUDGMENT (D-17/SPEC §8, <600 foot): the byline may wrap under the CTA
	   button rather than fight it for width at this measure. */
	.tbl-insights-row__foot {
		flex-wrap: wrap;
		row-gap: 12px;
	}

	.tbl-insights-row__date {
		margin-left: 10px; /* the wrapped-row case no longer needs the wide author→date gap the single-line layout used */
	}
}

/* --- 11. motion: row entrance (one-shot IntersectionObserver reveal) -------- */

/*
 * Mirrors tbl-at-a-glance's reveal recipe (VERIFIED at
 * modules/tbl-at-a-glance/includes/tbl-at-a-glance.css:537-552), scoped to
 * this module, EXCEPT for the transform: 10.1 item 6 dropped the translateY
 * rise - rows now fade in without moving vertically, opacity-only staggered
 * fade. Armed server-side (frontend.php ships is-entering), released by
 * tbl-insights-feed.js (adds has-entered) — never make the from-state the
 * plain default, or the BB editor and every no-JS path would render rows
 * permanently invisible.
 *
 * This is a one-shot IO reveal, NOT a scroll-scrubbed pin, so the project's
 * "scroll-scrub needs linear easing" rule does not apply here — the
 * cubic-bezier below is deliberate and correct for this recipe.
 */

.tbl-insights__feed.is-entering .tbl-insights-row {
	opacity: 0;
}

.tbl-insights__feed.is-entering.has-entered .tbl-insights-row {
	opacity: 1;
	transition: opacity 1s cubic-bezier(.25, .46, .45, .94) calc(0.05s + var(--i, 0) * 0.1s);
}

/* --- 12. motion: row hover, link underlines, pill/toggle hover -------------- */

.tbl-insights-row__thumb {
	/* R3-1 — see .tbl-insights__toggle-btn's comment above */
	transition: var(--button-easing);
	will-change: transform;
}

/* The hover zoom rides the INDIVIDUAL `scale` property, not `transform` — the
   parallax below animates `translate`, and the two only compose without fighting
   if they stay on separate properties. Writing either as a `transform` makes the
   scroll animation the sole author of the matrix and the hover stops easing. */
.tbl-insights-row:hover .tbl-insights-row__thumb,
.tbl-insights-row:focus-within .tbl-insights-row__thumb {
	scale: 1.03;
}

/* --- 12b. parallax on the row thumbnails (Pastel #5) ------------------------
   The image is overscanned 8% of the box at the top and the bottom and then slid
   across that slack as the row crosses the viewport, so it drifts inside a crop
   box that never moves. Symmetric, so the composition sits dead-centre when the
   row is centred — the drift is around the crop, not away from it. Same overscan
   and travel the expanding-image module already uses, deliberately.

   Travel is 5% of the IMAGE's own height (a percentage translate resolves against
   the element, not the frame) = 5.8% of the box, inside the 8% slack.

   Inside the motion guards only: with no animation-timeline the plain 100%-height,
   untranslated image above stands, which is exactly the pre-parallax rendering. */
@media (prefers-reduced-motion: no-preference) {
	@supports (animation-timeline: view()) {
		.tbl-insights-row__media {
			view-timeline-name: --tbl-insights-thumb;
			view-timeline-inset: 0;
		}

		.tbl-insights-row__thumb {
			position: relative;
			top: -8%;
			height: 116%;
			/* linear is mandatory — the animation shorthand defaults to ease, which
			   would accelerate the drift against a linear scroll input. */
			animation: tbl-insights-thumb-parallax linear both;
			animation-timeline: --tbl-insights-thumb;
			animation-duration: 1ms;
			animation-range: cover 0% cover 100%;
		}
	}
}

@keyframes tbl-insights-thumb-parallax {
	from { translate: 0 -5%; }
	to   { translate: 0 5%; }
}

/*
 * text-decoration-line is DISCRETE and cannot tween (project memory — the
 * exact sitewide bug already fixed once). The underline is drawn ALWAYS
 * (Task 1, section 7) and only text-decoration-color animates here. NEVER
 * add a rule toggling text-decoration-line on hover/focus.
 */
.tbl-insights-row__title-link {
	/* R3-1 — see .tbl-insights__toggle-btn's comment above */
	transition: var(--button-easing);
}

.tbl-insights-row__title-link:hover,
.tbl-insights-row__title-link:focus-visible {
	text-decoration-color: currentColor;
}

.tbl-insights-row__title-link:focus-visible {
	outline: 2px solid var(--Primary-Alt-50-Base);
	outline-offset: 2px;
}

.tbl-insights__more-link {
	/* R3-1 — see .tbl-insights__toggle-btn's comment above */
	transition: var(--button-easing);
}

.tbl-insights__more-link:hover,
.tbl-insights__more-link:focus-visible {
	/* 10.1 item 9c: underlined at rest (text-decoration-color: currentColor above);
	   hover moves the COLOUR and the underline follows, matching theme.css's
	   .underline-link pattern (theme.css:443). --a-hover-link-color resolves to
	   var(--primary-color) -> var(--Primary-90) (variables.css:113 and :144).
	   NEVER toggle text-decoration-line here - discrete property, cannot tween
	   (AP-2, already fixed sitewide once at theme.css:416). */
	color: var(--a-hover-link-color);
}

.tbl-insights__more-link:focus-visible {
	outline: 2px solid var(--Primary-Alt-50-Base);
	outline-offset: 2px;
}

.tbl-insights__pill:focus-visible,
.tbl-insights__toggle-btn:focus-visible {
	outline: 2px solid var(--Primary-Alt-50-Base);
	outline-offset: 2px;
}

.tbl-insights__pill:hover {
	/* Color/background move only, timed from the shared easing family — never
	   a hand-rolled transition list on this control (project convention). */
	background-color: var(--Primary-Alt-50-Base);   /* Pastel #7 — #AF85EC, exact token match */
}

.tbl-insights__pill.is-active:hover {
	background-color: var(--Black); /* stays put — the active pill has nothing to hover TO */
}

/* 10.1 item 2: the inactive word gets the established sitewide nav hover purple.
   --Primary-Alt-50-Base is #af85ec (variables.css:88) - the same value already
   bound to --nav-link-hover-color, not a new colour. The :not(.is-active) guard
   matters: the active word is --Neutral-100 and must not move, mirroring the
   .tbl-insights__pill.is-active:hover treatment directly above. The colour
   transition already exists on the base rule - never add a second (AP-5). */
.tbl-insights__toggle-btn:not(.is-active):hover {
	color: var(--Primary-Alt-50-Base);
}

/* --- 13. motion: filter / toggle swap handshake ----------------------------- */

/*
 * Handshake with tbl-insights-feed.js's swapTo(): the JS adds is-swapping,
 * waits for this 150ms fade (0ms under reduced motion — see section 14),
 * replaces .tbl-insights__feed's innerHTML, then removes is-swapping on the
 * next frame — which re-triggers section 11's staggered entrance on the new
 * rows because has-entered gets removed and re-added around that same swap.
 */
.tbl-insights__feed {
	opacity: 1;
	transition: opacity 150ms linear;
}

.tbl-insights__feed.is-swapping {
	opacity: 0;
}

/* --- 14. reduced motion — the whole contract in one block ------------------- */

@media (prefers-reduced-motion: reduce) {
	/*
	 * Resetting the is-entering selector itself (not only is-entering.has-entered)
	 * is what guarantees content is visible even if the observer never fires —
	 * this is the acceptance criterion ("reduced-motion renders everything in
	 * its final state, no animation").
	 */
	.tbl-insights__feed.is-entering .tbl-insights-row,
	.tbl-insights__feed.is-entering.has-entered .tbl-insights-row {
		opacity: 1;
		transition: none;
	}

	.tbl-insights__feed,
	.tbl-insights__feed.is-swapping {
		opacity: 1;
		transition: none;
	}

	.tbl-insights-row:hover .tbl-insights-row__thumb,
	.tbl-insights-row:focus-within .tbl-insights-row__thumb {
		transform: none;
		scale: 1;
	}

	.tbl-insights-row__thumb,
	.tbl-insights-row__title-link,
	.tbl-insights__more-link,
	.tbl-insights__pill,
	.tbl-insights__toggle-btn {
		transition: none;
	}
}

/* --- 15. no-JS safety net ---------------------------------------------------- */

/*
 * frontend.php ships is-entering server-side and tbl-insights-feed.js adds
 * has-entered once the IntersectionObserver fires. If JS never runs at all
 * (disabled, blocked, failed to load, or simply absent — e.g. reading this
 * markup outside WordPress), has-entered never arrives and section 11's
 * .is-entering rule would leave every row at opacity:0 forever. That is
 * unacceptable (T-10-07-05) — @media (scripting: none) is the CSS-only
 * detection for "no scripting support", and resets the from-state here
 * exactly like the reduced-motion block above. This is belt-and-suspenders
 * with tbl-insights-feed.js's own hard fallback (Task 3): if
 * IntersectionObserver itself is unavailable in a scripting-capable browser,
 * the JS removes is-entering outright rather than relying on this media
 * query alone.
 */
@media (scripting: none) {
	.tbl-insights__feed.is-entering .tbl-insights-row {
		opacity: 1;
		transition: none;
	}
}

/*
 * BB editor: is-entering is shipped server-rendered directly on this
 * module's markup (frontend.php), unlike tbl-at-a-glance where JS itself
 * adds the class — so this module's JS skipping its own init inside the
 * editor (tbl-insights-feed.js mirrors the sibling fl-builder-edit guard)
 * would otherwise leave every row permanently at opacity:0 there, since
 * has-entered would never arrive. Reset here exactly like the
 * reduced-motion / no-JS blocks above, so the module is always editable.
 */
.fl-builder-edit .tbl-insights__feed.is-entering .tbl-insights-row {
	opacity: 1;
	transition: none;
}
