/*
Theme Name: TheoTrade Archive
Theme URI: https://archive.theotrade.com
Author: TheoTrade
Description: Dark trading-blog theme for the TheoTrade archive rebuild. Classic theme, single stylesheet, with tiny vanilla JS enhancements: a conditional facade enqueued on singular views (Vimeo click-to-load) and an unconditional header script enqueued on every page (mobile nav toggle plus the desktop search disclosure). Content renders exclusively through the standard editor (post_content).
Version: 0.1.0
Requires at least: 6.6
Requires PHP: 8.0
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: theotrade-archive
*/

/* ---------------------------------------------------------------------------
   Design tokens: ALIASES ONLY. Every colour value and the font stack live in
   theme.json (settings.color.palette / settings.typography.fontFamilies), so
   nothing here holds a value and the front end cannot drift from the editor.
   Audience skews 60-90: base 18px, line-height 1.6+, high contrast, 44px+
   click targets, always-visible focus.

   Declared on `body`, not `:root`, on purpose. WP 7.0.2 emits presets on
   :root (WP_Theme_JSON::ROOT_CSS_PROPERTIES_SELECTOR, ':root' since 6.6),
   verified by reading the rendered global-styles-inline-css. `body` is a
   descendant of :root, so these aliases resolve whether core emits presets on
   :root or on body, which older versions did. No rule in this file uses a
   token outside body (html only carries scroll-behavior). var() resolves at
   computed-value time, so load order relative to global-styles is irrelevant.

   --radius stays a literal deliberately: it has no home in theme.json v3
   (settings.custom only), has two consumers (.card, .tile), never reaches the
   editor, and every other radius in this file is already a literal.
--------------------------------------------------------------------------- */
body {
	--bg: var(--wp--preset--color--bg);
	--bg-2: var(--wp--preset--color--bg-2);
	--card: var(--wp--preset--color--card);
	--card-2: var(--wp--preset--color--card-2);
	--line: var(--wp--preset--color--line);
	--line-2: var(--wp--preset--color--line-2);
	--ink: var(--wp--preset--color--ink);
	--muted: var(--wp--preset--color--muted);
	--faint: var(--wp--preset--color--faint);
	--accent: var(--wp--preset--color--accent);     /* trading green */
	--accent-2: var(--wp--preset--color--accent-2); /* gold */
	--danger: var(--wp--preset--color--danger);
	--radius: 14px;
	/* settings.layout.contentSize. The fallback keeps .wrap/.article bounded if
	   theme.json ever loses its layout block: an unresolved var() would compute
	   to max-width: none and silently full-bleed 21k pages. Colours get no
	   fallback on purpose, because a broken colour is obvious on first paint. */
	--maxw: var(--wp--style--global--content-size, 1180px);
	--font: var(--wp--preset--font-family--system);
	/* OP2 two-column split. Single source of truth: theme.json
	   settings.custom.op2Split. WP emits it as --wp--custom--op-2-split, quoted
	   verbatim from the rendered global-styles-inline-css; _wp_to_kebab_case
	   turns op2Split, op2-split AND op2split all into op-2-split, so the
	   plausible-looking --wp--custom--op2-split does not exist in any spelling.
	   The fallback is mandatory here, unlike the colours: an unresolved var()
	   makes grid-template-columns invalid at computed-value time, the property
	   falls back to none, and all 704 column pages render as a single column —
	   visually identical to the bug this token exists to fix. Same class of
	   silent catastrophe as --maxw, hence the same treatment. The fallback also
	   covers a NON-iframed editor canvas, where add_editor_style rewrites body
	   to .editor-styles-wrapper and global styles may land on another element.
	   58% is a choice, not a restoration: no .op2-col in the archive carries a
	   width or style attribute (0 of 704), the original OP2 ratios did not
	   survive the scrape. */
	--op2-split: var(--wp--custom--op-2-split, 58%);
}

/* Base ------------------------------------------------------------------- */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
	margin: 0;
	background: var(--bg);
	color: var(--ink);
	font-family: var(--font);
	font-size: 1.125rem; /* 18px */
	line-height: 1.65;
	-webkit-font-smoothing: antialiased;
	/* Sticky footer. The footer used to sit wherever the content happened to
	   end, leaving dead space beneath it: measured 226px on a 1272px viewport
	   on analyze-tab-overview, and the archive is full of pages that short.
	   A flex column here plus a growing .site-main pins the footer to the
	   bottom with no height arithmetic, which is still the right shape even
	   though the masthead is now a fixed 70px at every width above 860px (the
	   search disclosure took the form out of the header row): below 860px the
	   header is multi-row, and with JS disabled the form is in the row again.
	   100dvh, not 100vh: on mobile the dynamic unit avoids making the
	   page scroll by exactly the height of the browser chrome.
	   Safe for the skip-link, the only other body child: it is
	   position:absolute while hidden and position:fixed when focused, so it is
	   never in flow and never becomes a flex item. */
	min-height: 100dvh;
	display: flex;
	flex-direction: column;
}
/* Takes the slack so the footer lands at the bottom. flex-shrink 0 so long
   content is never compressed instead of scrolling. */
.site-main { flex: 1 0 auto; }
a { color: inherit; text-decoration: none; }
img { max-width: 100%; height: auto; display: block; }
a:focus-visible,
button:focus-visible,
input:focus-visible {
	outline: 3px solid var(--accent);
	outline-offset: 2px;
	border-radius: 4px;
}
.video-facade:focus-visible {
	outline: 3px solid var(--accent);
	outline-offset: 2px;
	border-radius: 4px;
}
.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 22px; }

/* Accessibility utilities ------------------------------------------------ */
.screen-reader-text {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}
.skip-link:focus {
	position: fixed;
	top: 10px;
	left: 10px;
	z-index: 100;
	width: auto;
	height: auto;
	clip-path: none;
	background: var(--accent);
	color: #04140d;
	font-weight: 800;
	padding: 12px 18px;
	border-radius: 8px;
	/* The global focus-visible ring (outline: 3px solid var(--accent)) is
	   invisible against this same-colour accent background, so give the
	   skip-link's own focus ring a distinct colour already in the file
	   (the dark ink used for on-accent text/icons above). */
	outline: 3px solid #04140d;
	outline-offset: 2px;
}

/* #content is the skip-link + tabindex="-1" target (header.php); clear the
   sticky masthead so focused content isn't hidden underneath it. The 70px is
   exact above 860px now that the header search sits in an absolutely
   positioned panel instead of wrapping to a second row; before this feature
   the masthead was 131px here and this value silently under-cleared it. */
#content {
	scroll-margin-top: calc(70px + 16px);
}

/* Header ----------------------------------------------------------------- */
.site-header {
	position: sticky;
	top: 0;
	z-index: 50;
	background: rgba(11, 15, 20, .88);
	backdrop-filter: blur(10px);
	border-bottom: 1px solid var(--line);
}
/* position: relative has zero layout effect here and is what makes .hd — not
   .site-header — the containing block for .search-panel. .site-header spans the
   full viewport, so above 1224px a panel pinned to its right edge would drift
   away from both the content column and the button it belongs to. */
.hd { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 20px; min-height: 70px; flex-wrap: wrap; padding-top: 8px; padding-bottom: 8px; }
.logo { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 1.25rem; letter-spacing: -.02em; padding: 6px 0; }
.logo .mark {
	display: inline-grid;
	place-items: center;
	width: 32px;
	height: 32px;
	border-radius: 8px;
	background: linear-gradient(135deg, var(--accent), #0e9c67);
	color: #04140d;
	font-weight: 900;
}

/* Primary menu (wp_nav_menu markup; dropdowns are CSS-only hover/focus) --- */
.nav ul { list-style: none; margin: 0; padding: 0; display: flex; gap: 4px; flex-wrap: wrap; }
.nav a {
	display: inline-block;
	padding: 12px 14px;
	border-radius: 8px;
	color: var(--muted);
	font-size: 1rem;
	font-weight: 600;
	white-space: nowrap;
}
.nav a:hover { color: var(--ink); background: var(--card); }
.nav li { position: relative; }
.nav .sub-menu {
	display: none;
	position: absolute;
	top: 100%;
	left: 0;
	z-index: 60;
	min-width: 260px;
	padding: 8px;
	background: var(--bg-2);
	border: 1px solid var(--line);
	border-radius: 12px;
	box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
	flex-direction: column;
}
.nav li:hover > .sub-menu,
.nav li:focus-within > .sub-menu { display: flex; }
.nav .sub-menu a { white-space: normal; width: 100%; }

/* Active item: colour cue plus a non-colour cue (underline), so state does
   not rely on colour perception alone. */
.nav .current-menu-item > a,
.nav .current_page_item > a,
.nav .current-menu-ancestor > a,
.nav .current_page_ancestor > a {
	color: var(--ink);
	font-weight: 800;
	text-decoration: underline;
	text-underline-offset: 4px;
}

/* Mobile nav toggle button: hidden on desktop, shown at/under the 1000px
   breakpoint where .nav becomes the collapsible panel. Shares its box with
   .search-toggle (same masthead, same 44px floor, same surface); only the
   padding and the reveal condition differ. */
.menu-toggle,
.search-toggle {
	display: none;
	align-items: center;
	gap: 8px;
	min-height: 44px;
	min-width: 44px;
	padding: 10px 14px;
	background: var(--card);
	border: 1px solid var(--line);
	border-radius: 8px;
	color: var(--ink);
	font-size: 1rem;
	font-weight: 600;
	cursor: pointer;
}
.menu-toggle:hover,
.search-toggle:hover { border-color: var(--line-2); }
.menu-toggle:focus-visible {
	outline: 3px solid var(--accent);
	outline-offset: 2px;
	border-radius: 4px;
}

/* Square icon variant of the button above: 20px icon + 2 x 10px padding = 40px,
   lifted to the 44px click-target floor by the shared min-width/min-height.
   Square rather than the shared 10px/14px because the row above 1000px has only
   ~10px of slack (arithmetic in the Responsive section), and this buys 4px of it
   back. Ink on card is 14.4:1.
   Stays display:none from the shared rule: only html.search-js reveals it, so a
   page without JS shows the working form itself rather than a dead button.
   No :focus-visible rule on purpose — the global button:focus-visible already
   rings it in accent at 8.8:1. (.menu-toggle:focus-visible above re-declares
   that global rule byte for byte; that is an existing wart, not a pattern.) */
.search-toggle { padding: 10px; }

/* Header search: the base .search-form rule (further down) carries an 18px/6px
   vertical margin and 52px control heights. Unscoped, that makes the form 76px
   tall and grows the sticky masthead from 70px to ~92px, which both shifts the
   form off-centre and silently undercuts #content's scroll-margin-top above.
   Scope it to the header so the masthead keeps its height while controls stay
   >= 44px. The <=860px block gives the form a full-width row of its own and
   resets the base 560px cap there.

   RESOLVED (this is where the old KNOWN/UNRESOLVED note lived). The header no
   longer tries to fit the form into the masthead row at all: above 860px the
   form lives inside .search-panel, which is absolutely positioned and therefore
   not a flex item of .hd, so the row carries logo + nav + a 44px button and is
   70px at every width above 860px. The arithmetic that used to fail: .hd's
   CONTENT box is 1136px (1180 max-width minus 2 x 22px padding, box-sizing:
   border-box), while logo 215 + nav 648 + form 307 + two 20px gaps = 1211, so
   the form was 75px too wide to fit, wrapped, and made the masthead 131px —
   20px of which was .hd's `gap` acting as row-gap.

   The note here also used to blame flex-wrap: nowrap for horizontal overflow
   "because the nav is 648px with no min-width: 0". That was measurably false
   and is not preserved: .nav ul carries flex-wrap: wrap too, so the nav's
   min-content width is 127px (its widest single link). Under nowrap the nav
   would have shrunk towards that and wrapped its own list, producing a TALLER
   header, never a horizontal scrollbar. nowrap still was not the fix — for that
   reason, not the one previously written down.

   max-width: 320px is gone from the rule below. It capped nothing: the form's
   natural width in the header row is 307px, so the declaration was inert from
   the day it was written. Inside the panel the panel's own width governs, and
   re-adding a cap here could only shrink the form inside the box drawn around
   it. The <=860px reset of it therefore also disappears; what stays there is a
   reset of the BASE .search-form 560px cap, which is a different rule. */
.hd .search-form { margin: 0; }
.hd .search-form input[type="search"],
.hd .search-form input[type="submit"] {
	min-height: 44px;
	font-size: 1rem;
}

/* Search disclosure. Above 860px only: below that the form keeps its visible
   full-width row and every rule in here is inert, which is deliberate (a second
   disclosure on phones would mean two panels, mutual closing and shared Escape
   order for no layout gain, and this audience is better served by a visible
   field).

   No-JS contract, with a gate class of its own and NOT .nav-js. nav.js removes
   .nav-js again when the nav markup is missing; sharing one class would let a
   missing search button re-open the mobile menu, or a missing nav strand the
   search. Each gate heals itself independently. With no JS at all neither class
   is present: .search-toggle stays display:none from its shared rule and
   .search-panel stays an ordinary in-flow flex item holding a working form,
   i.e. exactly the pre-feature header. */
@media (min-width: 861px) {
	html.search-js .search-toggle {
		display: inline-flex;
	}
	html.search-js .hd .search-panel {
		/* Out of .hd's flex layout entirely: an absolutely positioned descendant
		   of a flex container is not a flex item, which is what keeps the logo,
		   nav and button rectangles identical open and closed.
		   right: 22px, not 0, because offsets on an absolutely positioned box
		   resolve against the containing block's PADDING box, and .hd is the
		   .wrap that carries `padding: 0 22px`; right: 0 would sit 22px outside
		   the content column. 22px lines the panel's right edge up with the
		   button above it and with the content column below it, at every width
		   including above 1224px.
		   z-index: 70 clears .nav .sub-menu's 60 inside .site-header's own
		   stacking context (z-index: 50 + backdrop-filter), so if the two ever
		   share the band under the masthead the panel is unambiguously on top;
		   the min-width: 1001px rule below keeps a hover submenu from being
		   there at all. */
		display: none;
		position: absolute;
		top: 100%;
		right: 22px;
		z-index: 70;
		width: 340px; /* 320px of form plus this box's 2 x 10px padding */
		padding: 10px;
		/* Its own opaque surface, in the language of .nav .sub-menu: the header's
		   translucent background stops at its bottom border, so anything hanging
		   below it would otherwise be read over raw page content. */
		background: var(--bg-2);
		border: 1px solid var(--line);
		border-radius: 12px;
		box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
	}
	html.search-js.search-open .hd .search-panel {
		display: block;
	}
}
@media (min-width: 1001px) {
	/* One header overlay at a time. Above 1000px the submenus are absolutely
	   positioned overlays that share the band under the masthead with an open
	   search panel, and the rightmost items overlap it outright, so a hover
	   submenu loses while the panel is open. Focus-opened submenus need no rule
	   here: nav.js closes the panel as soon as focus leaves it, so .search-open
	   and :focus-within on a nav item cannot coexist — which also means this
	   rule never takes a submenu away from a keyboard user.
	   Scoped to >=1001px because between 861 and 1000px the submenus are static,
	   always-expanded parts of the mobile nav panel, and hiding those would make
	   menu items unreachable while the search happened to be open. */
	html.search-open .nav li:hover > .sub-menu {
		display: none;
	}
}

/* Page/archive heads ----------------------------------------------------- */
.page-head { padding: 44px 0 10px; }
.page-head h1 {
	font-size: clamp(1.75rem, 4vw, 2.625rem);
	line-height: 1.12;
	letter-spacing: -.02em;
	margin: 0 0 10px;
}
.archive-desc { color: var(--muted); max-width: 680px; }

/* Card grid -------------------------------------------------------------- */
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; padding: 24px 0 10px; }
.card {
	background: var(--card);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	overflow: hidden;
	display: flex;
	flex-direction: column;
	transition: transform .15s ease, border-color .15s ease;
}
.card:hover { transform: translateY(-3px); border-color: var(--line-2); }
.card .thumb { display: block; aspect-ratio: 16 / 9; background: var(--bg-2); position: relative; overflow: hidden; }
.card .thumb img { width: 100%; height: 100%; object-fit: cover; }
.card .thumb .ph {
	position: absolute;
	inset: 0;
	display: grid;
	place-content: center;
	background: radial-gradient(120% 120% at 50% 0, rgba(22, 199, 132, .10), transparent), linear-gradient(135deg, var(--card-2), var(--bg-2));
}
.card .thumb .ph-mark {
	width: 44px;
	height: 44px;
	border-radius: 10px;
	background: linear-gradient(135deg, var(--accent), #0e9c67);
	color: #04140d;
	font-weight: 900;
	font-size: 1.375rem;
	display: grid;
	place-items: center;
}
.card .body { padding: 16px 17px 18px; display: flex; flex-direction: column; gap: 9px; flex: 1; }
.card-title { font-size: 1.125rem; line-height: 1.35; margin: 0; letter-spacing: -.01em; }
.card-title a { display: block; padding: 2px 0; }
.card:hover .card-title { color: var(--accent); }
.card .ex { color: var(--muted); font-size: 1rem; margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.chip {
	align-self: flex-start;
	display: inline-block;
	font-size: .8125rem;
	font-weight: 800;
	letter-spacing: .06em;
	text-transform: uppercase;
	color: var(--accent);
	background: rgba(22, 199, 132, .12);
	padding: 5px 10px;
	border-radius: 6px;
}
.meta { color: var(--faint); font-size: .9375rem; margin: 0; margin-top: auto; display: flex; gap: 8px; align-items: center; }
/* .dot is a shared separator with .byline .dot in single.php; card.php currently renders only
   <time> so .dot is unused here — reserved for a future secondary meta item (e.g. author), not dead CSS. */
.dot { width: 3px; height: 3px; border-radius: 50%; background: var(--faint); }

/* Single post / page ----------------------------------------------------- */
.article { max-width: var(--maxw); margin: 0 auto; padding: 40px 22px 60px; }
.article .chip { margin-bottom: 16px; }
.article h1 { font-size: clamp(1.75rem, 4vw, 2.625rem); line-height: 1.12; letter-spacing: -.02em; margin: 0 0 14px; }
.byline {
	color: var(--faint);
	font-size: 1rem;
	display: flex;
	gap: 9px;
	align-items: center;
	margin: 0 0 26px;
	padding-bottom: 22px;
	border-bottom: 1px solid var(--line);
}
.byline .au { color: var(--muted); font-weight: 600; }
.feat { border-radius: 12px; overflow: hidden; margin: 0 0 28px; border: 1px solid var(--line); }
.feat img { width: 100%; }

/* Breadcrumbs (deep page hierarchy, 4-5 levels) -------------------------- */
.breadcrumbs { color: var(--faint); font-size: 1rem; margin: 0 0 20px; line-height: 2; }
.breadcrumbs a { color: var(--muted); text-decoration: underline; text-underline-offset: 3px; padding: 4px 2px; }
.breadcrumbs a:hover { color: var(--accent); }
.breadcrumbs .sep { margin: 0 6px; }

/* Content — everything inside the_content() ------------------------------ */
.entry-content { font-size: 1.125rem; line-height: 1.75; }
.entry-content p { margin: 0 0 20px; }
.entry-content a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.entry-content img { border-radius: 10px; margin: 22px 0; border: 1px solid var(--line); }
.entry-content h2, .entry-content h3 { letter-spacing: -.01em; margin: 34px 0 12px; line-height: 1.25; }
.entry-content ul, .entry-content ol { margin: 0 0 20px; padding-left: 24px; }
.entry-content blockquote {
	border-left: 3px solid var(--accent);
	margin: 24px 0;
	padding: 6px 0 6px 20px;
	color: var(--muted);
	font-style: italic;
}
.entry-content table { width: 100%; border-collapse: collapse; font-size: 1rem; margin: 24px 0; }
.entry-content th {
	text-align: left;
	font-size: .8125rem;
	letter-spacing: .08em;
	text-transform: uppercase;
	color: var(--muted);
	border-bottom: 2px solid var(--line);
	padding: 10px 12px;
}
.entry-content td { padding: 10px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
.entry-content .wp-caption { max-width: 100%; }
.entry-content .wp-caption-text, .entry-content figcaption { color: var(--faint); font-size: .9375rem; margin-top: 8px; }
.alignwide { margin-left: auto; margin-right: auto; }
.aligncenter { margin-left: auto; margin-right: auto; }

/* Child pages navigation (hub pages: member-home, monthly archives) ------ */
.page-children {
	margin: 40px 0 0;
	padding-top: 26px;
	border-top: 1px solid var(--line);
}
.page-children h2 {
	font-size: 1.125rem;
	letter-spacing: -.01em;
	margin: 0 0 16px;
}
.page-children ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
	gap: 10px;
}
.page-children a {
	display: block;
	min-height: 44px;
	padding: 12px 16px;
	background: var(--card);
	border: 1px solid var(--line);
	border-radius: 10px;
	color: var(--muted);
	font-weight: 600;
	line-height: 1.3;
}
.page-children a:hover {
	color: var(--ink);
	border-color: var(--line-2);
}
.page-children a:focus-visible {
	outline: 3px solid var(--accent);
	outline-offset: 2px;
	border-radius: 4px;
}

/* Vimeo / embeds: responsive 16:9; facade markup gets iframe on click ----- */
.entry-content iframe { max-width: 100%; border: 0; }
.entry-content iframe[src*="player.vimeo"] { width: 100%; aspect-ratio: 16 / 9; height: auto; background: #000; }
.wp-embed-responsive .wp-block-embed.wp-embed-aspect-16-9 .wp-block-embed__wrapper { position: relative; aspect-ratio: 16 / 9; }
.embed {
	display: block;
	position: relative;
	aspect-ratio: 16 / 9;
	border-radius: 12px;
	overflow: hidden;
	margin: 0 0 28px;
	border: 1px solid var(--line);
	background: var(--bg-2);
}
/* Note: for a player.vimeo src the rule above (.entry-content
   iframe[src*="player.vimeo"], specificity 0-2-1) OUTRANKS this one (0-1-1) and
   applies height:auto here. That is safe, not luck: with inset:0 giving a
   non-auto top and bottom, CSS2.1 10.6.4 solves the height from the equation
   and the iframe still fills .embed; aspect-ratio does nothing once both axes
   are determined. Proven by measurement, not by reading: the facade click test
   asserts a 0px change in the box rect. A showcase src matches neither
   selector's attribute test, so this rule governs it alone. */
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.embed .poster { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; margin: 0; border: 0; border-radius: 0; }
.embed.video-facade { cursor: pointer; }
.embed .play { position: absolute; inset: 0; display: grid; place-items: center; background: rgba(4, 10, 7, .38); transition: background .15s; }
.embed.video-facade:hover .play { background: rgba(4, 10, 7, .18); }
.embed .play span {
	width: 76px;
	height: 76px;
	border-radius: 50%;
	background: var(--accent);
	color: #04140d;
	display: grid;
	place-items: center;
	font-size: 1.875rem;
	padding-left: 5px;
	box-shadow: 0 12px 34px rgba(22, 199, 132, .45);
}
.embed.no-poster { background: radial-gradient(120% 120% at 50% 0, rgba(22, 199, 132, .10), transparent), linear-gradient(135deg, var(--card-2), var(--bg-2)); }

/* OP2 columns (op2-cols / op2-col) ---------------------------------------
   704 scraped pages carry this markup with no CSS at all, so every one of them
   renders as a stack today. Measured before this change on
   analyze-tab-overview at 1280px: .op2-cols computes to display:block,
   grid-template-columns:none, and the two .op2-col boxes sit at y=401.69 and
   y=1074.69. Every selector below is scoped under .op2-cols / .op2-col, so the
   667 pages without the wrapper get no new declaration at all. */
.op2-cols {
	display: grid;
	/* Mobile first: one column. The 2/3-column tracks arrive above the theme's
	   860/861px stop, which since the header search disclosure is the
	   PHONE/TABLET stop only (card grid, header search row). The nav's collapse
	   moved up to 1000/1001px and is a header concern; these column tracks
	   deliberately did not follow it, because a content-width decision has no
	   business tracking how wide the primary menu happens to be. */
	grid-template-columns: 1fr;
	gap: 22px; /* the same gap .grid already uses in this file */
	margin: 24px 0;
	/* start, not the grid default stretch: shape D's near-empty column (a lone
	   <h2>Menu</h2>) must not be stretched to a tall neighbour's height.
	   Columns get no background and no border, so a short column simply ends
	   instead of drawing an empty block. */
	align-items: start;
}
@media (min-width: 861px) {
	/* 696 of 704 pages have exactly two columns, 8 have three, and NONE has one
	   (696 + 8 = 704), so two columns is the unguarded base and only the thirds
	   case needs a test. The :has() override is (0,3,0) against this rule's
	   (0,1,0), so it wins on specificity rather than on source order.
	   Real geometry, measured rather than assumed: .article is
	   max-width:1180px with 40px 22px 60px padding, so the content box is
	   exactly 1136px (.op2-col rect width === 1136 at 1280px viewport). A
	   percentage track resolves against the container's CONTENT BOX, NOT
	   against the width left after the gap: 58% of 1136 = 658.891px, the 22px
	   gap comes out of what 1fr receives, and 1fr gets 455.109px. The delivered
	   ratio is therefore 59.1/40.9, not 58/42. Change --op2-split in theme.json
	   if that reads wrong on a real screen. */
	.op2-cols {
		grid-template-columns: var(--op2-split) 1fr;
	}
	.op2-cols:has(> .op2-col:nth-child(3)) {
		/* 8 pages: 4 "Downloads" lists and 4 scraped OP2 footers. Equal thirds,
		   no per-page special cases. */
		grid-template-columns: repeat(3, 1fr);
	}
}
.op2-col {
	/* Grid items default to min-width:auto, which lets one long unbreakable
	   token (a bare URL, a long download label) push its track past its share
	   and give the whole page a horizontal scrollbar. min-width:0 lets the
	   track shrink; overflow-wrap lets the token actually break. Both are
	   needed, neither is sufficient alone. */
	min-width: 0;
	overflow-wrap: anywhere;
}
/* Zero the first element's own top margin so the two column tops line up.
   This has to reach 0, 1 or 2 div levels deep because OP2 wrapped content
   differently per page: a bare <h2> straight under .op2-col (shape D,
   measured on whitelist-instructions), an <h2> or <img> inside one div, or an
   <img>/<iframe> inside two divs (shape A and the livestream embeds). The
   7487 wrapper divs inside columns carry no attributes and need no rule of
   their own: this chain passes THROUGH them and declares on the child.
   Without this, .entry-content h2's 34px or .entry-content img's 22px top
   margin drops one column and the tops miss the 4px budget. */
.op2-col > :first-child,
.op2-col > div:first-child > :first-child,
.op2-col > div:first-child > div:first-child > :first-child {
	margin-top: 0;
}
/* Non-faced iframes inside a column: the 35 pages carrying a livestream.com
   embed as <iframe width="640" height="360">.
   THIS RULE IS NOT ABOUT OVERFLOW. .entry-content iframe { max-width: 100% }
   (line 381) and .op2-col { min-width: 0 } above already prevent horizontal
   scroll — verified before the change: scrollWidth === clientWidth === 1280 on
   the livestream page. It is about the height="360" ATTRIBUTE, which nothing
   overrides: clamped into a 455px column (or a 364px third) the box becomes
   455x360 or 364x360 and the 16:9 ratio breaks. Delete this and the aspect
   ratio breaks again, silently, on 35 pages.
   The border, 12px radius and 28px bottom margin match .embed so that a
   livestream box and a Vimeo box sitting in the same column are framed
   identically. A replaced element clips its own corners, so no overflow is
   needed, and no focus ring is involved because a raw iframe is not the
   focusable facade.
   SPECIFICITY, deliberately by construction and not by order: a plain
   .op2-col iframe would be (0,1,1) — an exact tie with BOTH .embed iframe
   (line 394) and .entry-content iframe (line 381) — and, sitting later in the
   file, it would win both ties and force height:auto onto a faced iframe that
   needs height:100%. The :not(.embed iframe) guard makes the two match sets
   DISJOINT, so that tie can never occur, and it lifts this selector to (0,2,2)
   so it also outranks .entry-content iframe's border:0 for the border below.
   Both effects are required; neither is cosmetic. */
.op2-col iframe:not(.embed iframe) {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 16 / 9;
	margin: 0 0 28px;
	border: 1px solid var(--line);
	border-radius: 12px;
	background: #000; /* literal, exactly as line 382 already does */
}

/* Hub tile grid (tile-grid / tile) ---------------------------------------
   7,412 scraped OP2 navigation tiles on 172 pages, rebuilt by
   theotrade_archive_tile_grid() at the_content priority 9. Every selector here is
   scoped under .tile-grid, the same discipline as the OP2 block above (:574-577), so
   the 1,202 pages without tiles receive no new declaration at all.

   Deliberately NOT reusing .grid: it is redeclared at :806 (1fr 1fr) and :820 (1fr),
   which would import fixed 3/2/1 breakpoints and defeat auto-fill. Measured at a
   666px viewport, auto-fill gives two 300px columns where .grid would give one 622px
   column. No media query in this section; auto-fill is the whole point.

   The 300px floor is the source material, not a taste call: 5,768 of the 7,412 tiles
   have a 300x169 image and all 72 unique files are 300x169 on disk, so a wider card
   upscales the majority with nothing better to fall back on. Measured card widths:
   364 at 1180+ (content box 1136), 397 at 860 (816), 339 at 375 (339 -- .article
   drops to 18px padding at :821, so it is 339 and not 331), and 300 at 666 where the
   grid still fits two columns. */
.tile-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
	gap: 22px; /* the same gap .grid (:383) and .op2-cols (:587) already use */
	margin: 24px 0;
}
.tile-grid .tile {
	/* Containing block for the stretched-link ::after below. */
	position: relative;
	background: var(--card);
	border: 1px solid var(--line);
	border-radius: var(--radius);
	overflow: hidden;
	display: flex;
	flex-direction: column;
	/* Grid items default to min-width:auto, so one unbreakable caption token could
	   push its track past its share and give the page a horizontal scrollbar. Same
	   reasoning as .op2-col (:620). */
	min-width: 0;
	transition: transform .15s ease, border-color .15s ease;
}
.tile-grid .tile:hover { transform: translateY(-3px); border-color: var(--line-2); }
/* 16:9 box so rows align across the six source sizes in the archive (300x169 for
   5,768 tiles, 960x540 for 1,295, 1920x1080 for 262, and 12 rarer sizes). Cropping
   is under one pixel: 300/169 = 1.7751 against 16/9 = 1.7778. */
.tile-grid .tile-media { display: block; aspect-ratio: 16 / 9; background: var(--bg-2); overflow: hidden; }
.tile-grid .tile-media a { display: block; height: 100%; }
/* Stretched link: the whole card navigates, no JS and no third anchor. inset:0
   resolves against .tile's padding box. The audience skews 60-90, so the hit area
   must not be the caption text alone. */
.tile-grid .tile-media a::after { content: ""; position: absolute; inset: 0; z-index: 1; }
/* Neutralise .entry-content img (:464 -- 10px radius, 1px border, 22px block margin).
   (0,2,1) against (0,1,1), so this wins on specificity and not on source order. Also
   outranks img{height:auto} (:103), which is (0,0,1). */
.tile-grid .tile-media img { width: 100%; height: 100%; object-fit: cover; margin: 0; border: 0; border-radius: 0; }
/* Neutralise .entry-content h2, .entry-content h3 (:465 -- 34px/12px margins).
   (0,2,0) against (0,1,1): two classes beat one class plus one element, so this wins
   outright rather than tying and depending on staying below line 465. */
.tile-grid .tile-title { margin: 0; font-size: 1.125rem; line-height: 1.35; letter-spacing: -.01em; }
/* Neutralise .entry-content a (:463 -- accent colour plus green underline). (0,3,1)
   against (0,1,1). display:block plus padding makes the whole caption band the click
   and focus target: measured 362x52 at a 1280px viewport, so comfortably past 44px.
   z-index:2 paints it above the stretched ::after, which keeps the caption text
   selectable; mirrors .card-title a (:416). */
.tile-grid .tile-title a {
	display: block;
	position: relative;
	z-index: 2;
	padding: 13px 17px 15px;
	min-height: 44px;
	color: var(--ink);
	text-decoration: none;
}
.tile-grid .tile:hover .tile-title { color: var(--accent); }
/* The base focus ring (:104-108) uses outline-offset:2px, and .tile's overflow:hidden
   clips descendant painting at the padding box. Measured at 1280px: the caption anchor
   spans x=73..435 and the padding box ends at x=435, so a +2px ring would be drawn to
   x=440 and clipped on three sides. A negative offset draws it inside the box, where
   nothing clips it. (0,3,2) against a:focus-visible (0,1,1). */
.tile-grid .tile-title a:focus-visible { outline-offset: -3px; }
/* The 63 tiles that carry a description. Neutralises .entry-content p (:462, 20px
   bottom margin), (0,2,0) against (0,1,1). Left OUTSIDE the stretched link's stacking
   context on purpose: the description area stays clickable, at the cost of not being
   selectable. Same two-line clamp as .card .ex (:418). */
.tile-grid .ex {
	margin: 0;
	padding: 0 17px 15px;
	color: var(--muted);
	font-size: 1rem;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

/* Post navigation (prev/next, global chronological) ----------------------- */
.post-nav {
	display: flex;
	flex-wrap: wrap;
	gap: 14px;
	margin: 40px 0 0;
	padding-top: 26px;
	border-top: 1px solid var(--line);
}
.post-nav .nav-previous,
.post-nav .nav-next {
	flex: 1 1 240px;
}
.post-nav .nav-next { text-align: right; }
.post-nav a {
	display: block;
	min-height: 44px;
	padding: 10px 16px;
	border: 1px solid var(--line);
	border-radius: 10px;
	color: var(--muted);
	font-weight: 600;
	line-height: 1.3;
}
.post-nav a:hover { color: var(--ink); border-color: var(--line-2); background: var(--card); }

/* Pagination ------------------------------------------------------------- */
.pagination { padding: 26px 0 50px; }
.pagination .nav-links { display: flex; gap: 8px; flex-wrap: wrap; }
.pagination .page-numbers {
	display: inline-grid;
	place-items: center;
	min-width: 48px;
	min-height: 48px;
	padding: 0 14px;
	border: 1px solid var(--line);
	border-radius: 10px;
	color: var(--muted);
	font-weight: 700;
}
.pagination .page-numbers:hover { color: var(--ink); background: var(--card); }
.pagination .page-numbers.current { background: var(--accent); border-color: var(--accent); color: #04140d; }

/* Search / 404 ----------------------------------------------------------- */
.search-form { display: flex; gap: 10px; max-width: 560px; margin: 18px 0 6px; }
.search-form input[type="search"] {
	flex: 1;
	min-height: 52px;
	padding: 0 16px;
	font-size: 1.125rem;
	color: var(--ink);
	background: var(--card);
	border: 1px solid var(--line-2);
	border-radius: 10px;
}
.search-form input[type="submit"] {
	min-height: 52px;
	padding: 0 22px;
	font-size: 1.125rem;
	font-weight: 800;
	color: #04140d;
	background: var(--accent);
	border: 0;
	border-radius: 10px;
	cursor: pointer;
}
.not-found { padding: 60px 0 80px; max-width: 640px; }
.not-found p { color: var(--muted); }

/* Footer ------------------------------------------------------------------ */
.site-footer { border-top: 1px solid var(--line); background: var(--bg-2); margin-top: 50px; }
.ft { display: flex; justify-content: space-between; gap: 24px; flex-wrap: wrap; padding: 34px 0; color: var(--faint); font-size: 1rem; }
.ft a { color: var(--muted); padding: 6px 0; }
.ft a:hover { color: var(--accent); }

/* Responsive -------------------------------------------------------------- */
/* Two stops, and they are not interchangeable:

   1000px — the nav collapses behind .menu-toggle. Raised from 860px by the
   search disclosure, and that raise is what makes "70px at every width above
   860px" true rather than aspirational. Above 1000px the row is logo 215 +
   nav 648 (six items) + button 44 + two 20px gaps = 947 against a 957px content
   box at a 1001px viewport: it fits with ~10px to spare. Between 861 and 1000px
   the row carries only a logo and two buttons, so one row is never in doubt. If
   the primary menu ever grows past six items, THIS number goes up; do not
   squeeze the nav, and do not put the form back in the row.
   The paired value in assets/js/nav.js is NAV_BREAKPOINT, and the JS closes the
   menu above 1000 exactly where this query stops applying. Change both or
   neither: a JS threshold that disagrees with this query leaves the panel open
   in a layout that has no button to close it.

   860px — the phone/tablet layout: two-column card grid, and the header search
   drops the disclosure for a visible full-width row. Paired with min-width:861px
   above (the disclosure) and with SEARCH_BREAKPOINT in nav.js. */
@media (max-width: 1000px) {
	.menu-toggle { display: inline-flex; }

	/* #primary-nav becomes a full-width panel. No-JS contract: with no
	   .nav-js class on <html> (JS never ran), the rules below make the
	   panel and every submenu inside it visible/reachable by default —
	   nothing here depends on JS to be usable. */
	.nav {
		display: flex;
		flex-direction: column;
		width: 100%;
		gap: 2px;
		padding: 10px 0;
	}
	.nav ul { flex-direction: column; align-items: stretch; gap: 2px; width: 100%; }
	.nav li { width: 100%; }
	.nav a { width: 100%; }

	/* Submenus: static and always expanded in the mobile panel, not
	   hover/focus-only overlays (style.css:413-415 previously hid them
	   entirely on mobile, making them unreachable by touch/keyboard). */
	.nav .sub-menu {
		display: flex;
		position: static;
		min-width: 0;
		padding: 4px 0 4px 16px;
		background: transparent;
		border: 0;
		box-shadow: none;
	}

	/* Collapsed state exists ONLY once nav.js has run (adds .nav-js to
	   <html>) and only while it hasn't also added .nav-open — i.e. the
	   script controls collapsing, the CSS default above is "open". */
	html.nav-js #primary-nav { display: none; }
	html.nav-js.nav-open #primary-nav { display: flex; }
}
@media (max-width: 860px) {
	/* Intentional tablet stop at 2 columns; 1-column applies only below 560px (phone). */
	.grid { grid-template-columns: 1fr 1fr; }

	/* Header search keeps its own full-width row here, with no disclosure. The
	   flex sizing now belongs to .search-panel, because the PANEL (not the form)
	   is the flex item of .hd since the disclosure was added; the panel carries
	   no decoration at all at this width, all of that lives in the min-width:861
	   block. The form's max-width reset is still needed, for a different reason
	   than before: with the header's own inert 320px cap gone, the form would
	   inherit the base .search-form 560px cap and stop short of the row it was
	   just given. */
	.hd .search-panel { flex: 1 1 100%; }
	.hd .search-form { max-width: none; margin: 10px 0 0; }
}
@media (max-width: 560px) {
	.grid { grid-template-columns: 1fr; }
	.article { padding: 28px 18px 44px; }
}
