/*
 * VLINZZA — Single Post (Task 1: Core Reading Experience)
 *
 * Loaded only on is_singular('post') (see inc/blog/single.php), after
 * vlz-blog-archive so the shared Blog Nav (.vlz-blog-bar) is already styled
 * and can be overridden here without !important if ever needed — nothing
 * below touches .vlz-blog-bar or any Blog Archive selector.
 *
 * Scope rule: every selector is rooted under .vlz-single-post,
 * .vlz-single-toc or .vlz-single-toc-mobile. No bare element selector
 * (`h2`, `article`, `a:hover`) and no `.entry-content` alone — this file
 * must never leak onto Blog Archive or any other template.
 *
 * All colours/spacing/radii/typography reuse existing --vlz-* tokens
 * (assets/css/tokens.css) — no new palette, no literal colour values.
 *
 * Breakpoints match the values already used elsewhere in this theme's
 * feature stylesheets (see assets/css/blog-archive.css): 1024.98px and
 * 767.98px. Desktop: two-area reading layout with a sticky TOC rail.
 * Below 1025px there is no room for a fixed-width rail next to a
 * comfortable reading column (measured against --vlz-measure), so the
 * layout drops to one column and the Mobile compact TOC control takes over
 * — this is a deliberate Tablet decision (task brief §25), not a gap.
 */

/* ===================================================================
 * Reading layout
 * =================================================================== */
.vlz-single-post__layout {
	display: grid;
	grid-template-columns: 1fr;
	grid-template-areas: "main";
	gap: var(--vlz-space-lg);
	max-width: var(--vlz-wide-max);
	margin-inline: auto;
	padding-inline: var(--vlz-container-gutter);
	/*
	 * Owner Correction Round 2 §4: top padding only (bottom padding is
	 * unchanged, still var(--vlz-space-xl)) — this is the Single-Post-owned
	 * half of the excess gap the Owner flagged between the Blog Bar and the
	 * article header. Measured live before this change: 72px total
	 * (.vlz-blog-single-nav's own 40px margin-bottom + this 32px padding-top).
	 * See the body.single-post override below for the other half.
	 */
	padding-block-start: 1.75rem; /* 28px — desktop target 24-32px */
	padding-block-end: var(--vlz-space-xl);
}

/*
 * Owner Correction Round 2 §4: `.vlz-blog-single-nav` (the Blog Bar on
 * Single Post) is exclusively rendered by
 * `vlz_blog_render_single_post_navigation()` — it never appears on any Blog
 * Archive surface — so collapsing its own bottom margin here is a genuine
 * Single-Post-only change, not a shared-component edit. Scoped via
 * `body.single-post` (not touched in assets/css/blog-archive.css) so this
 * can never affect Category/Tag/Author/Search, whose own nav uses the
 * different `.vlz-blog-archive__nav` class untouched by this rule. The
 * Blog Bar's own visuals/border/padding-bottom (24px, above its own border)
 * are untouched — only the margin outside its border collapses to 0, so the
 * combined visible gap after this change is exactly `.vlz-single-post__layout`'s
 * own padding-top above (28px desktop / 22px tablet / 18px mobile,
 * responsive rules below), each inside this round's required ranges.
 */
body.single-post .vlz-blog-single-nav {
	margin-block-end: 0;
}

@media (max-width: 1024.98px) {
	.vlz-single-post__layout {
		padding-block-start: 1.375rem; /* 22px — tablet target 20-24px */
	}
}

@media (max-width: 767.98px) {
	.vlz-single-post__layout {
		padding-block-start: 1.125rem; /* 18px — mobile target 16-20px */
	}
}

.vlz-single-post__main {
	grid-area: main;
	min-width: 0;
}

.vlz-single-post__rail {
	grid-area: rail;
	min-width: 0;
}

/*
 * Single-Post Final Mobile Modal + TOC Correction — Mobile TOC left-gap root
 * cause: below 1025px `.vlz-single-post__layout` drops to a ONE-COLUMN grid
 * (`grid-template-areas: "main"`, no "rail" area declared), but this `<aside
 * class="vlz-single-post__rail">` (Desktop TOC wrapper, inc/blog/single.php)
 * is still in the DOM with `grid-area: rail` — an area name absent from the
 * active template. Confirmed live via Playwright at 390px: Chromium
 * synthesizes a new single-cell implicit named area for the unmatched
 * "rail" name plus its own `gap` track, which silently ate 64px from
 * `.vlz-single-post__main`'s available column width and shifted it right by
 * that same 64px — the "empty space toward the left" the Owner flagged on
 * the Mobile TOC, which is really this whole reading column being
 * mis-measured, not a `.vlz-single-toc-mobile` sizing bug (it was already
 * `width: 100%` of its — too-narrow — parent). `.vlz-single-post__rail`
 * only ever wraps the Desktop TOC (`vlz_single_post_render_toc_desktop()`)
 * and is never the Mobile control's container, so removing it from the flow
 * below 1025px (the same breakpoint its own child `.vlz-single-toc` already
 * uses to hide) cannot affect the Mobile TOC accordion/sticky/anchor logic
 * or Desktop's rail at all.
 */
@media (max-width: 1024.98px) {
	.vlz-single-post__rail {
		display: none;
	}
}

@media (min-width: 1025px) {
	.vlz-single-post__layout {
		/* First-listed area sits on the inline-start track, which is the
		 * RIGHT edge on this RTL site — "rail" first puts the TOC rail on
		 * the right and the article on the left, per task brief §7/§9,
		 * independent of DOM order (the article stays first in source for
		 * reading/tab order — see inc/blog/single.php). */
		grid-template-columns: 272px minmax(0, 1fr);
		grid-template-areas: "rail main";
		/*
		 * Task 3 Correction — Desktop TOC sticky regression root cause:
		 * `align-items: start` (previously set here) shrinks the `.rail`
		 * grid item's own box down to its content height (just the TOC
		 * panel, ~457px measured), instead of stretching it to the row's
		 * full height (the `.main` column's article height, ~21000px
		 * measured on this article). `position: sticky` on `.vlz-single-toc`
		 * can only "float" within its containing block's own box — when that
		 * box is the same height as the sticky element there is zero extra
		 * scroll room, so the TOC scrolls away with the rest of the page
		 * instead of pinning at `top`. Removing this (default is `stretch`)
		 * makes `.vlz-single-post__rail` span the full article height again,
		 * giving the sticky TOC room to stay pinned at `top` throughout the
		 * read. Confirmed live via Playwright at 1440×900: TOC held at
		 * top:32px at 20/40/60/80% scroll depth after this change.
		 * `.vlz-single-toc` itself still declares its own `position: sticky;
		 * top: var(--vlz-space-lg)` unchanged — this was never missing.
		 */
	}

	.vlz-single-post__layout--no-rail {
		grid-template-columns: 1fr;
		grid-template-areas: "main";
	}
}

/*
 * Owner Correction Round 1 §15/§39: Author, Newsletter and Final Related
 * live OUTSIDE the rail/main reading grid, in their own full-width block —
 * same outer container width/gutters as .vlz-single-post__layout above, but
 * a plain single column (no rail track), so this section can use the whole
 * reading-container width once the TOC rail is no longer beside it. This is
 * what closes the dead space the Owner flagged where the rail used to end,
 * and lets the Final Related grid actually reach 4 Desktop columns.
 */
/* ===================================================================
 * Article header — clean: eyebrow (optional) + H1 only.
 * No author/date/updated/reading-time line (task brief §6).
 * =================================================================== */
.vlz-single-post__header {
	/*
	 * Round 3 Final H1 Correction (superseded by the H1 Alignment + Natural
	 * Wrapping fix below): measured live against the Legacy reference
	 * (legacy.vlinzza.com/blog/what-is-seo) — Legacy's H1 and its body
	 * paragraphs share the exact same unconstrained column width. Removing
	 * this header's own `var(--vlz-measure)` cap (68ch, sized for BODY
	 * paragraphs) let the eyebrow+H1 use the full main reading column.
	 *
	 * H1 Alignment + Natural Wrapping fix (see
	 * SINGLE-POST-H1-ALIGNMENT-WRAP-DIAGNOSIS-2026-09-13.md): that full
	 * main-column width no longer matches `.vlz-single-post__content`,
	 * which was later corrected to a 90%-of-column, centered measure (see
	 * "SINGLE POST — LEGACY GEOMETRY CORRECTION" below) — leaving the
	 * header's right/left edges 33-51px off the article text's edges on
	 * Desktop/Tablet. Per the Owner-approved design decision, the header is
	 * brought to the article content's own measure (not the other way
	 * around): same `max-width: 90%` + `margin-inline: auto` as
	 * `.vlz-single-post__content`, so the H1 box starts/ends exactly where
	 * the article text does. Reset to `none` below 767.98px (same
	 * breakpoint and value the content rule below uses) because Mobile's
	 * header and content already share one unconstrained width — measured
	 * live, 0px edge difference — so this preserves that already-correct
	 * Mobile state exactly.
	 */
	max-width: 90%;
	margin-inline: auto;
	margin-block-end: var(--vlz-space-md);
}

@media (max-width: 767.98px) {
	.vlz-single-post__header {
		max-width: none;
	}
}

.vlz-single-post__eyebrow {
	margin: 0 0 var(--vlz-space-2xs);
	font-size: var(--vlz-text-sm);
	font-weight: var(--vlz-weight-semibold);
}

.vlz-single-post__eyebrow a {
	color: var(--vlz-color-primary);
	text-decoration: none;
}

.vlz-single-post__eyebrow a:hover,
.vlz-single-post__eyebrow a:focus-visible {
	color: var(--vlz-color-primary-strong);
	text-decoration: underline;
}

.vlz-single-post__title {
	margin: 0;
	/*
	 * H1 Alignment + Natural Wrapping fix (see
	 * SINGLE-POST-H1-ALIGNMENT-WRAP-DIAGNOSIS-2026-09-13.md): overrides the
	 * global `h1, h2, h3, h4 { text-wrap: balance; }` rule
	 * (assets/css/base.css) for the Single Post title only. `balance`
	 * intentionally shortens the first line to even out line lengths, which
	 * on a long title wraps earlier than the available column width allows
	 * and leaves unused space on line 1. `wrap` (the standard "fill the
	 * line, break only when the next word no longer fits" behavior) is the
	 * Owner-approved requirement; it does not touch the global rule other
	 * headings still use.
	 */
	text-wrap: wrap;
	/*
	 * Owner Correction Round 3 §1 (superseded by Final H1 Correction below):
	 * Round 2's H1 (44px) was diagnosed live, not reduced by guess. The
	 * first attempt added `max-width: 32rem` directly on the H1 to tighten
	 * its box — that fixed the ragged-edge dead-space complaint, but the
	 * Owner's follow-up Legacy-parity comparison
	 * (legacy.vlinzza.com/blog/what-is-seo) showed Legacy renders this exact
	 * title on ONE line at 1440px, because Legacy never narrows the H1's
	 * column at all (935.6px measured, same width as its body paragraphs).
	 * A fixed 32rem (512px) cap could never reach one line at a readable
	 * article font-size, so it is REMOVED here — the fix is now the header's
	 * own width (opened up above, in `.vlz-single-post__header`), not a cap
	 * on the H1 itself. Confirmed live: at the full ~824px column this
	 * produces below, the title renders on exactly 1 line at 39.2px — no
	 * artificial wrapping left. Font family, weight and colour remain
	 * completely untouched.
	 */
	font-size: clamp(2.25rem, 2rem + 0.5vw, 2.5rem); /* desktop ~38-40px, measured 39.2px at 1440 */
	line-height: 1.3;
}

@media (max-width: 1024.98px) {
	.vlz-single-post__title {
		font-size: clamp(1.9rem, 1.3rem + 2.2vw, 2.5rem); /* tablet ~37.7-40px measured */
	}
}

@media (max-width: 767.98px) {
	.vlz-single-post__title {
		/*
		 * Mobile H1 Parity Correction: measured live against the Legacy
		 * reference (legacy.vlinzza.com/blog/what-is-seo) at 390px width —
		 * Legacy renders this exact title at 21px/1.4 line-height, 2 lines,
		 * 58.8px total visual height. The prior 32px/1.3 value (124.8px
		 * height, 3 lines) was carried over from the Desktop/Round 2 fix
		 * without a separate Mobile measurement and reads as Hero-sized.
		 * 22px/1.4 is the closest token-free step down from 32px that keeps
		 * the title clearly a heading (not shrunk to body-text size) while
		 * landing within one measured line of Legacy's rendered height at
		 * this theme's own (wider) available column. See
		 * SINGLE-POST-MOBILE-TOC-STICKY-H1-PARITY-CORRECTION.md for the
		 * full before/after measurement table.
		 */
		font-size: 1.375rem; /* 22px */
		line-height: 1.4;
	}
}

/* ===================================================================
 * Article content — comfortable reading measure, content-block safety.
 * Scoped to .vlz-single-post__content, not a bare .entry-content rule, so
 * no other template using .entry-content is affected.
 *
 * Visual Correction Round 1 (2026-09-05) — reading-column centering:
 * this element's own `max-width: var(--vlz-measure)` (≈68ch) was never
 * centered, so as a block box it kept its default inline-start position;
 * in this RTL site inline-start is the right edge, so the whole shrunken
 * box sat flush-right inside the wider `.vlz-single-post__main` (1024px)
 * column, leaving ~377px of dead space on the physical left (App Visual
 * Parity Inspection, Issue A). `.vlz-single-author` a few hundred lines
 * below already centers the same `--vlz-measure` cap
 * (`margin-inline: auto`) — this brings `.vlz-single-post__content` in
 * line with that existing pattern.
 *
 * SINGLE POST — LEGACY GEOMETRY CORRECTION (2026-09-05): the reference audit
 * (`SINGLE-POST-LEGACY-GEOMETRY-REFERENCE-AUDIT-ARTICLE-TOC.md`) measured
 * Legacy's real body-text width at 935.58px — 87% of its 1075.58px article
 * column, with H1/body/table all sharing one unified box — and found the
 * 68ch cap (647.36px, 63.2% of the 1024px column) itself, not the earlier
 * centering fix, to be the dominant cause of the oversized text↔TOC gap.
 * Per the Owner's design decision for this correction, `68ch` is replaced
 * with a percentage-based contract targeting ~88–90% of the article column
 * (Owner range, not a literal copy of Legacy's 87% or its asymmetric
 * one-sided `margin-left:40px`/`padding:50px` Elementor mechanics — this
 * project keeps modern symmetric centering via `margin-inline: auto`,
 * unchanged below). `--vlz-measure` (the global token) is left untouched;
 * this change is scoped to this selector only. `.vlz-single-inline-related`
 * (below) needed no equivalent change — it renders as a descendant inside
 * this box and inherits the corrected width automatically.
 */
.vlz-single-post__content {
	max-width: 90%;
	margin-inline: auto;
}

/*
 * Mobile artifact guard (measured live, LocalWP 390×844): under the previous
 * `68ch` cap this selector never actually engaged below ~1025px — the ch
 * value in px always exceeded `.vlz-single-post__main`'s own (already
 * narrow) column width, so content rendered at the column's full width with
 * no extra inset. A flat `90%` has no such natural ceiling and DOES engage
 * at Mobile, introducing a ~17px inset on each side that was never present
 * before and is not required by the Legacy reference (Legacy Mobile is a
 * plain symmetric full-stack column, no extra reading-measure inset). Reset
 * to full column width below the Mobile breakpoint only; Desktop/Tablet keep
 * the 90% contract above.
 */
@media (max-width: 767.98px) {
	.vlz-single-post__content {
		max-width: none;
	}
}

.vlz-single-post__content > * {
	max-width: 100%;
}

/*
 * Owner Correction Round 1 §24/§25: H2/H3 inside the article body are
 * deliberately smaller than the theme-wide heading-2/heading-3 presets (an
 * H2 that size read as a second H1) and each gets its own distinct
 * Blue/Navy-family colour so the hierarchy (H1 > H2 > H3) is visible at a
 * glance, not just by size. Every value below has a hardcoded literal
 * fallback (matching vlz_single_typography_defaults() in
 * inc/typography-settings.php) inside var(), so the rule still renders
 * correctly before VLINZZA Settings → Typography's inline `:root` override
 * ever loads or if it is ever missing — see §26/§27.
 */
.vlz-single-post__content h2 {
	scroll-margin-top: var(--vlz-space-lg);
	font-size: clamp(var(--vlz-single-h2-min, 1.17rem), var(--vlz-single-h2-min, 1.17rem) + 1vw, var(--vlz-single-h2-max, 1.5rem));
	color: var(--vlz-single-h2-color, var(--vlz-color-primary-strong));
	line-height: var(--vlz-leading-heading);
}

.vlz-single-post__content h3 {
	scroll-margin-top: var(--vlz-space-lg);
	font-size: clamp(var(--vlz-single-h3-min, 0.98rem), var(--vlz-single-h3-min, 0.98rem) + 0.8vw, var(--vlz-single-h3-max, 1.2rem));
	color: var(--vlz-single-h3-color, var(--vlz-color-primary));
	font-weight: var(--vlz-weight-bold);
	line-height: var(--vlz-leading-heading);
}

.vlz-single-post__content ul,
.vlz-single-post__content ol {
	padding-inline-start: var(--vlz-space-md);
}

.vlz-single-post__content a {
	word-break: break-word;
}

/* ===================================================================
 * Desktop TOC rail — H2-only, sticky, bounded height, internal scroll.
 * Hidden below 1025px; the Mobile control takes over instead.
 * =================================================================== */
.vlz-single-toc {
	display: none;
}

/*
 * The rail wrapper remains the sole sticky owner for the Desktop TOC.
 * Back-to-Top is now a page-level fixed control rendered after the footer,
 * so the TOC's existing sizing and scrolling behavior remain independent.
 */
@media (min-width: 1025px) {
	.vlz-single-post__rail-sticky {
		position: sticky;
		top: var(--vlz-space-lg);
		z-index: var(--vlz-z-sticky);
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: var(--vlz-space-sm);
	}

	.vlz-single-toc {
		display: block;
		width: 100%;
		background: var(--vlz-color-surface);
		/*
		 * Owner Correction Round 2 §7: stronger blue-family border, a literal
		 * Owner-specified hex (#9CC8E8 — not yet a shared --vlz-* token; Round
		 * 1's "primary-soft" (#e8f2fa) read too faint against the white panel
		 * per the Owner's visual review). Panel padding moves from here down
		 * to the title/list individually (below) so the title can become its
		 * own full-width coloured header band — `overflow:hidden` clips that
		 * band's top corners to the same radius as the panel.
		 */
		border: 1px solid #9CC8E8;
		border-radius: 10px;
		overflow: hidden;
		box-shadow: 0 4px 14px rgba(23, 102, 156, 0.08);
		padding: 0;
	}
}

.vlz-single-toc__title {
	/*
	 * Owner Correction Round 2 §8: the title is now its own full-width
	 * header band — brand-blue background, white centered text — instead of
	 * plain dark text on white, per the Owner's explicit visual correction
	 * ("Header must span full TOC width. Do not leave title black on
	 * white."). `var(--vlz-color-primary-strong)` already resolves to the
	 * exact #17669c the brief calls for, so the header band still reuses an
	 * existing brand token even though the border/active-state colours
	 * elsewhere in this block are new literals.
	 */
	margin: 0;
	background: var(--vlz-color-primary-strong);
	color: var(--vlz-color-text-invert);
	text-align: center;
	font-size: 1rem;
	font-weight: var(--vlz-weight-bold);
	padding: 14px 16px;
}

.vlz-single-toc__list {
	margin: 0;
	padding: 14px 16px;
	list-style: none;
	/*
	 * Content-Fit Height Repair (2026-09-11): superseded the fixed
	 * `min-height` floor previously set by "Desktop TOC Height Tuning
	 * (Final Local Closure QA)" (Owner asked the panel to grow into ~50% of
	 * the empty space below it). Live Owner review flagged that same rule
	 * as an unwanted UX regression on short-heading articles — measured on
	 * a real 5-H2 post at 1440×900, the `min-height: min(40rem, calc(50vh +
	 * 5.875rem))` floor forced the list to 544px tall while its own content
	 * ended at 220px, leaving ~324px of dead space below the last item.
	 * `min-height` is intentionally removed rather than overridden — the
	 * list now sizes to its own content (browser default `height: auto`)
	 * for short lists, while `max-height` (unchanged, still capped at
	 * `min(46rem, calc(50vh + 13.875rem))`) continues to bound genuinely
	 * long lists into internal scroll, exactly as before. The sticky
	 * parent `nav.vlz-single-toc`'s scroll room comes from the rail's CSS
	 * Grid `align-items: stretch` (Task 3 fix, `.vlz-single-post__layout`),
	 * not from this list's own height, so sticky behavior is unaffected by
	 * a shorter list.
	 */
	/*
	 * TOC + Back-to-Top Build & Final QA (contract §3): replaces the
	 * "Content-Fit Height Repair" formula above (kept short/normal TOCs
	 * content-fit, but scaled the cap itself off viewport height with no
	 * direct "leave room below the panel" term, so a short-viewport window
	 * — e.g. 1440x700 — still let a long TOC's cap crowd the bottom of the
	 * screen). New formula per the approved contract:
	 * `min(existing absolute cap, 100vh - reserve)`.
	 * `--vlz-single-toc-chrome-reserve` bundles the sticky top offset + TOC
	 * header/title height + a bottom breathing margin into one centrally
	 * tunable value (contract's permitted build-QA tuning range: 140-180px).
	 * Tuned UP from the contract's 160px starting value to the range's own
	 * 180px ceiling: live Very-Long-TOC (40 items) Playwright QA at the
	 * short-viewport tier measured only ~2px of space left between the rail
	 * Back-to-Top button's own bottom edge and the viewport bottom at 160px
	 * — the contract's reserve covers the TOC's own chrome (sticky top +
	 * header) but this build additionally places Back-to-Top (its own
	 * ~52px + a var(--vlz-space-sm) gap) directly below the list inside the
	 * same sticky unit (§4), which also has to fit above the reserved
	 * breathing margin. 180px (still inside the approved range, no owner
	 * escalation needed) was re-measured on the same article/viewport and
	 * restored a real, visible gap. The 46rem absolute cap is unchanged from
	 * the prior formula.
	 */
	--vlz-single-toc-chrome-reserve: 180px;
	max-height: min(46rem, calc(100vh - var(--vlz-single-toc-chrome-reserve)));
	max-height: min(46rem, calc(100dvh - var(--vlz-single-toc-chrome-reserve)));
	overflow-y: auto;
	overscroll-behavior: contain;
}

.vlz-single-toc__item + .vlz-single-toc__item {
	margin-block-start: var(--vlz-space-3xs);
}

/*
 * Visual Correction Round 1 (2026-09-05) — inactive-link color fix: the
 * base rule is qualified with its own stable ancestor
 * (`.vlz-single-toc .vlz-single-toc__link`, specificity 0,2,0) instead of
 * the bare single class it used before (0,1,0). This is a pure
 * specificity correction, not a new selector scope — `.vlz-single-toc` is
 * the exact `<nav>` this link always renders inside (inc/blog/single.php),
 * never used for the Mobile TOC. It exists only to beat Elementor's
 * site-wide global-Kit rule `.elementor-kit-9 a` (0,1,1), which still
 * loads on this fully-rebuilt Gutenberg page and was winning the cascade
 * for every inactive TOC link, rendering them Elementor blue instead of
 * this rule's intended dark slate (App Visual Parity Inspection, Issue
 * E — same defect class already fixed on the 404 page). No `!important`,
 * no Elementor edit. `:hover`/`:focus-visible`/`.is-active` below already
 * won this cascade on their own (0,2,0 via a pseudo-class/second class)
 * and are untouched.
 */
.vlz-single-toc .vlz-single-toc__link {
	display: block;
	padding: var(--vlz-space-3xs) var(--vlz-space-2xs);
	border-radius: var(--vlz-radius-sm);
	/* Owner Correction Round 2 §9: literal #44546A per the Owner's brief —
	 * close to but distinct from the existing --vlz-color-text-muted
	 * (#66707c), which read too light against the new white TOC body. */
	color: #44546A;
	font-size: var(--vlz-text-sm);
	line-height: var(--vlz-leading-tight);
	text-decoration: none;
	overflow-wrap: break-word;
}

.vlz-single-toc__link:hover {
	color: var(--vlz-color-text);
	background: var(--vlz-color-surface-alt);
}

.vlz-single-toc__link:focus-visible {
	outline: var(--vlz-focus-width) solid var(--vlz-color-focus);
	outline-offset: var(--vlz-focus-offset);
}

.vlz-single-toc__link.is-active {
	/* Owner Correction Round 2 §9: active color matches the header band
	 * exactly (var(--vlz-color-primary-strong) = #17669c, same token as the
	 * TOC header background above). Background #EEF6FC is a literal — a
	 * paler tint than the existing --vlz-color-primary-soft (#e8f2fa) the
	 * Owner asked for specifically for this state. */
	color: var(--vlz-color-primary-strong);
	font-weight: var(--vlz-weight-bold);
	background: #EEF6FC;
	border-radius: 6px;
	padding-inline: 6px;
	position: relative;
}

.vlz-single-toc__link.is-active::before {
	content: "";
	position: absolute;
	inset-inline-start: 0;
	top: var(--vlz-space-3xs);
	bottom: var(--vlz-space-3xs);
	width: 2px;
	border-radius: var(--vlz-radius-pill);
	background: var(--vlz-color-primary);
}

/* Back-to-Top control CSS relocated to assets/css/footer.css (Task 1 site-wide
 * CSS conditional-loading fix, 2026-09-19, Report 35) -- it is rendered on
 * every public front-end route via generate_after_footer
 * (vlz_single_post_register_back_to_top(), inc/blog/single.php), not only on
 * Single Post, so it is no longer owned by this Single-Post-only stylesheet. */

/* ===================================================================
 * Mobile compact TOC control — full-width sticky accordion.
 * Visible only below 1025px, i.e. exactly where the Desktop rail hides.
 *
 * Owner Correction (Mobile TOC visual, round 1): the original control was
 * `position: sticky` and pill-shaped, sized to its own content
 * (inline-flex). At small widths that read as a tiny floating chip that
 * appeared to drift during scroll rather than a normal part of the
 * article, so it was made a plain in-flow block instead.
 *
 * Owner Correction (Mobile TOC sticky, round 2): the plain in-flow
 * version kept the size/shape but disappeared off-screen once the reader
 * scrolled past it — the Owner wants it available throughout reading.
 * Diagnosed live (Playwright, 390px): `.site-header` and
 * `.vlz-blog-single-nav` are both `position: relative`/`static` with no
 * sticky/fixed header anywhere in this theme, so they scroll fully out of
 * view before this control's own sticky offset is ever reached — nothing
 * for a small `top` to collide with. `position: sticky` is restored, but
 * this time on the SAME full-width, non-pill box kept from round 1 (no
 * regression to the old `inline-flex`/pill chip): width, colour, radius
 * and text are all unchanged from the Owner-approved visual, so the box
 * simply pins in place instead of scrolling away, still occupying its own
 * space in the article flow (no width/layout side effects on the content
 * below). Desktop's `.vlz-single-toc` sticky rail (above) is untouched.
 * =================================================================== */
.vlz-single-toc-mobile {
	display: block;
	position: sticky;
	top: var(--vlz-space-2xs);
	z-index: var(--vlz-z-sticky);
	width: 100%;
	margin-block-end: var(--vlz-space-sm);
	border: var(--vlz-border-width) solid var(--vlz-color-border);
	/*
	 * Mobile TOC Correction: the Owner asked for a shape "like the project's
	 * buttons" — this project's full-width CTA buttons (e.g.
	 * .vlz-single-service-modal__button below) use var(--vlz-radius-pill),
	 * which on a short, wide bar like this collapsed control reads as a
	 * clear pill/stadium shape, not just a slightly-rounded card. `overflow:
	 * hidden` (unchanged, already present) clips the square-cornered toggle
	 * button inside to this same radius, so no change is needed there.
	 */
	border-radius: var(--vlz-radius-pill);
	background: var(--vlz-color-surface);
	overflow: hidden;
}

/*
 * Once the panel is open the box is much taller, and a full pill radius on a
 * tall box reads as an odd stretched-oval bottom edge rather than a rounded
 * corner — clamp back to the theme's standard "clearly rounded" card radius
 * while the accordion is expanded. Pure visual state read off the toggle's
 * own `aria-expanded` (already set by assets/js/single-post.js) — no new
 * class, no JS change.
 */
.vlz-single-toc-mobile:has(.vlz-single-toc-mobile__toggle[aria-expanded="true"]) {
	border-radius: var(--vlz-radius-lg, 16px);
}

@media (min-width: 1025px) {
	.vlz-single-toc-mobile {
		display: none;
	}
}

.vlz-single-toc-mobile__toggle {
	display: flex;
	align-items: center;
	gap: var(--vlz-space-2xs);
	width: 100%;
	max-width: none;
	padding: var(--vlz-space-xs) var(--vlz-space-sm);
	border: 0;
	border-radius: 0;
	background: var(--vlz-color-primary);
	box-shadow: none;
	color: var(--vlz-color-text-invert);
	font-size: var(--vlz-text-base);
	font-weight: var(--vlz-weight-semibold);
	font-family: inherit;
	text-align: start;
	cursor: pointer;
}

.vlz-single-toc-mobile__toggle:hover {
	background: var(--vlz-color-primary-strong);
}

.vlz-single-toc-mobile__toggle:focus-visible {
	outline: var(--vlz-focus-width) solid var(--vlz-color-focus-invert);
	outline-offset: calc(-1 * var(--vlz-focus-offset));
}

.vlz-single-toc-mobile__icon {
	display: inline-flex;
	flex: 0 0 auto;
	color: var(--vlz-color-text-invert);
}

.vlz-single-toc-mobile__label {
	flex: 1 1 auto;
	/* Owner request: "جدول المحتويات" centered instead of start (right on
	 * this RTL site) — the toggle's own `text-align: start` above still
	 * applies to any other text in the row, this overrides it for the label
	 * only; icon/chevron stay pinned to their own flex ends. */
	text-align: center;
}

.vlz-single-toc-mobile__chevron {
	display: inline-flex;
	flex: 0 0 auto;
	color: var(--vlz-color-text-invert);
	transition: transform 0.2s ease;
}

.vlz-single-toc-mobile__toggle[aria-expanded="true"] .vlz-single-toc-mobile__chevron {
	transform: rotate(180deg);
}

.vlz-single-toc-mobile__panel {
	margin: 0;
	padding: var(--vlz-space-sm);
	border: 0;
	border-block-start: var(--vlz-border-width) solid var(--vlz-color-border);
	border-radius: 0;
	background: var(--vlz-color-surface);
	box-shadow: none;
	max-width: 100%;
}

.vlz-single-toc-mobile__panel[hidden] {
	display: none;
}

.vlz-single-toc-mobile__list {
	margin: 0;
	padding: 0;
	list-style: none;
	max-height: 16rem;
	overflow-y: auto;
	overscroll-behavior: contain;
}

.vlz-single-toc-mobile__list li + li {
	margin-block-start: var(--vlz-space-3xs);
}

.vlz-single-toc-mobile__list a {
	display: block;
	padding: var(--vlz-space-3xs) var(--vlz-space-2xs);
	border-radius: var(--vlz-radius-sm);
	color: var(--vlz-color-text-muted);
	font-size: var(--vlz-text-sm);
	text-decoration: none;
	overflow-wrap: break-word;
}

.vlz-single-toc-mobile__list a:hover {
	color: var(--vlz-color-text);
	background: var(--vlz-color-surface-alt);
}

.vlz-single-toc-mobile__list a:focus-visible {
	outline: var(--vlz-focus-width) solid var(--vlz-color-focus);
	outline-offset: var(--vlz-focus-offset);
}

.vlz-single-toc-mobile__list a.is-active {
	color: var(--vlz-color-primary);
	font-weight: var(--vlz-weight-semibold);
	background: var(--vlz-color-surface-alt);
}

@media (prefers-reduced-motion: reduce) {
	.vlz-single-toc__list,
	.vlz-single-toc-mobile__list {
		scroll-behavior: auto;
	}

	.vlz-single-toc-mobile__chevron {
		transition: none;
	}
}

/* ===================================================================
 * Task 3 — Reading Progress. Task 3 Correction, Part D — Service Modal CTA
 * (its own section further below, native <dialog> instead of a JS-timed
 * floating card).
 *
 * `assets/js/single-post.js` computes reading progress from the top/bottom
 * of `.vlz-single-post__content` (article body only, never
 * header/TOC/newsletter/related/footer — Task 3 §A1) and reuses that same
 * range for the modal's trigger threshold. This stylesheet only paints the
 * progress bar; all measurement/timing lives in the JS.
 * =================================================================== */

/* Reading Progress — thin fixed track at the very top of the viewport. A
 * native <dialog> renders in the top layer (above every z-indexed element
 * in normal document flow), so this no longer needs to out-rank a CTA's own
 * z-index — still just a 3px hairline, never a large visual element, never
 * anything that reserves layout space (no content shift). */
.vlz-single-progress {
	position: fixed;
	inset-inline: 0;
	top: 0;
	height: 3px;
	background: transparent;
	z-index: 600;
	pointer-events: none;
}

.vlz-single-progress__bar {
	height: 100%;
	width: 0%;
	background: var(--vlz-color-primary-strong);
	transform-origin: inline-start;
}

@media (prefers-reduced-motion: no-preference) {
	.vlz-single-progress__bar {
		transition: width 0.1s linear;
	}
}

/* ===================================================================
 * Task 3 Correction, Part D/G — Service Modal CTA (replaces the Task 3
 * floating side card entirely — Owner-rejected, see the correction report).
 * Native `<dialog>` (assets/js/single-post.js calls `.showModal()`/`.close()`
 * — no manual `div + fixed + z-index` stack, no manual focus-trap: the
 * native top layer already sits above everything, including the Desktop
 * sticky TOC rail (Part I), with no z-index coordination needed.
 * Settings-driven colours: Service Modal Settings Hub Task — this component
 * now reads its OWN dedicated `--vlz-single-modal-*` custom properties
 * (VLINZZA → Single Post Appearance → Service Modal section), no longer the
 * `--vlz-single-cta-*` variables the 50% Scroll CTA colour fields still
 * control (Owner Locked Decision: changing one set must never change the
 * other). Every var() keeps its existing literal fallback — identical to
 * the values the old `cta_*` fields shipped as defaults — so an
 * unsaved/default install still renders exactly the same.
 * =================================================================== */
.vlz-single-service-modal {
	position: relative;
	padding: 0;
	border: 0;
	border-radius: var(--vlz-radius-lg, 20px);
	/* Final micro visual correction: Desktop was noticeably wider than the
	 * two-column content needed (920px), leaving dead horizontal space —
	 * narrowed to a more compact 620–680px band. */
	width: min(660px, 92vw);
	max-width: 660px;
	max-height: min(88vh, 640px);
	overflow: auto;
	background: #FFFFFF;
	box-shadow: 0 24px 64px rgba(23, 102, 156, 0.28);
}

/*
 * Final visual/positioning repair (2026-10-15): the "reference audit" note
 * above (removed) assumed the UA stylesheet's own `:modal` top-layer rule
 * would center this dialog with `position: fixed` — confirmed, via live
 * `getComputedStyle()` across multiple engines/sessions, to actually compute
 * as `position: absolute` in the engines tested. An absolutely positioned
 * top-layer box is centered correctly ONLY at the instant it opens, for the
 * exact scroll/viewport state active then — unlike `position: fixed`, it
 * does not track the viewport afterwards. Any subsequent change to that
 * state while the dialog stays open or gets restored (a bfcache/back-
 * forward-cache restore carrying a stale scroll offset — confirmed,
 * math-exact, in `SERVICE-MODAL-OWNER-REPRODUCED-VISUAL-ROOT-CAUSE.md`; a
 * mobile browser's dynamic viewport-height change as its own URL bar
 * shows/hides, a known class of bug for UA-only dialog centering on real
 * mobile browsers) leaves the dialog box rendered thousands of pixels off
 * its now-stale "centered" position — fully or partially outside the
 * viewport — while `::backdrop` (a separate top-layer box, always sized to
 * the live viewport regardless of the dialog's own position) keeps dimming
 * the whole page normally. That combination — full/partial dimming with the
 * dialog's own content partly or wholly invisible — is exactly both
 * Owner-reported defects. Declaring `position: fixed` explicitly here,
 * authored rather than left to the UA stylesheet, makes the browser
 * recompute this centering continuously against the CURRENT viewport on
 * every paint — the standard, robust way to center a fixed overlay —
 * instead of freezing it at the moment the dialog happened to open.
 */
.vlz-single-service-modal[open] {
	position: fixed;
	inset: 0;
	margin: auto;
}

.vlz-single-service-modal::backdrop {
	/* Owner brief Part D2: close to rgba(0,0,0,0.5), present the page behind
	 * it as clearly dimmed/inert without going fully opaque. */
	background: rgba(15, 23, 42, 0.5);
}

.vlz-single-service-modal[open] {
	display: block;
	animation: vlz-service-modal-in 0.18s ease;
}

@keyframes vlz-service-modal-in {
	from {
		opacity: 0;
		transform: translateY(8px) scale(0.98);
	}
	to {
		opacity: 1;
		transform: translateY(0) scale(1);
	}
}

.vlz-single-service-modal__close-form {
	position: absolute;
	top: 0.85rem;
	left: 0.85rem;
	z-index: 1;
	margin: 0;
}

.vlz-single-service-modal__close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2rem;
	height: 2rem;
	padding: 0;
	border: 0;
	border-radius: var(--vlz-radius-pill);
	background: rgba(255, 255, 255, 0.7);
	color: var(--vlz-single-modal-text-color, #44546A);
	cursor: pointer;
}

.vlz-single-service-modal__close:hover {
	background: rgba(23, 102, 156, 0.15);
}

.vlz-single-service-modal__close:focus-visible {
	outline: var(--vlz-focus-width) solid var(--vlz-color-focus);
	outline-offset: var(--vlz-focus-offset);
}

/*
 * Service Modal Final Visual Implementation — a flat CSS Grid of named
 * areas. Desktop groups the RIGHT (first, per this RTL site's logical
 * "start" side) column from the label/heading/text/benefits items and the
 * LEFT column from the image + action card; Mobile reassigns the SAME
 * elements to a single-column stack in image → label → heading → text →
 * benefits → action order — no DOM reordering, no JS, no duplicated markup.
 */
.vlz-single-service-modal__layout {
	display: grid;
	/* Rebalanced after the Desktop width reduction so neither column reads
	 * squeezed at the new ~660px modal width (~58/42 split). */
	grid-template-columns: 1.2fr 1fr;
	grid-template-areas:
		"label    image"
		"heading  image"
		"text     image"
		"benefits action";
	align-items: start;
	gap: 0 var(--vlz-space-lg);
	padding: var(--vlz-space-xl) var(--vlz-space-lg) var(--vlz-space-lg);
}

.vlz-single-service-modal__visual-image {
	grid-area: image;
	border-radius: var(--vlz-radius-md, 14px);
	overflow: hidden;
	background: #EAF3FB;
}

.vlz-single-service-modal__image {
	display: block;
	width: 100%;
	height: 100%;
	max-height: 220px;
	object-fit: cover;
}

.vlz-single-service-modal__label {
	grid-area: label;
	justify-self: start;
	margin: 0 0 var(--vlz-space-2xs);
	padding: 0.3rem 0.85rem;
	border-radius: var(--vlz-radius-pill);
	background: rgba(23, 102, 156, 0.1);
	color: var(--vlz-single-modal-button-bg, #17669C);
	font-size: var(--vlz-text-xs, 0.8125rem);
	font-weight: var(--vlz-weight-semibold);
}

.vlz-single-service-modal__heading {
	grid-area: heading;
	/* Owner correction: the heading must read as the Modal's centered,
	 * visually-prominent starting point on every viewport, not start
	 * (right-)aligned. */
	margin: 0 0 var(--vlz-space-2xs);
	color: var(--vlz-single-modal-heading-color, #1B2538);
	text-align: center;
	/* Desktop target ~28–32px. */
	font-size: 1.875rem;
	font-weight: var(--vlz-weight-bold);
	line-height: 1.3;
}

.vlz-single-service-modal__text {
	grid-area: text;
	margin: 0 0 var(--vlz-space-sm);
	max-width: 46ch;
	color: var(--vlz-single-modal-text-color, #44546A);
	font-size: var(--vlz-text-base);
	line-height: var(--vlz-leading-normal, 1.6);
}

.vlz-single-service-modal__benefits {
	grid-area: benefits;
	display: flex;
	flex-direction: column;
	margin: 0;
	padding: 0;
	list-style: none;
}

.vlz-single-service-modal__benefits li {
	padding: var(--vlz-space-2xs) 0;
	border-top: 1px solid rgba(27, 37, 56, 0.08);
}

.vlz-single-service-modal__benefits li:first-child {
	border-top: 0;
	padding-top: 0;
}

.vlz-single-service-modal__benefit-title {
	display: block;
	color: var(--vlz-single-modal-heading-color, #1B2538);
	font-size: var(--vlz-text-sm);
	font-weight: var(--vlz-weight-semibold);
}

.vlz-single-service-modal__benefit-text {
	display: block;
	margin-block-start: 0.15rem;
	color: var(--vlz-single-modal-text-color, #44546A);
	font-size: var(--vlz-text-xs, 0.8125rem);
	line-height: 1.5;
}

.vlz-single-service-modal__action-card {
	grid-area: action;
	margin-block-start: var(--vlz-space-sm);
	padding: var(--vlz-space-sm);
	border-radius: var(--vlz-radius-md, 14px);
	background: var(--vlz-single-modal-bg, #EEF6FC);
}

.vlz-single-service-modal__action-heading {
	margin: 0 0 0.35rem;
	color: var(--vlz-single-modal-heading-color, #1B2538);
	font-size: var(--vlz-text-base);
	font-weight: var(--vlz-weight-bold);
	line-height: 1.35;
}

.vlz-single-service-modal__action-text {
	margin: 0 0 var(--vlz-space-sm);
	color: var(--vlz-single-modal-text-color, #44546A);
	font-size: var(--vlz-text-xs, 0.8125rem);
	line-height: 1.5;
}

.vlz-single-service-modal__button {
	display: block;
	width: 100%;
	box-sizing: border-box;
	padding: var(--vlz-space-xs) var(--vlz-space-lg);
	border-radius: var(--vlz-radius-pill);
	background: var(--vlz-single-modal-button-bg, #17669C);
	color: var(--vlz-single-modal-button-text-color, #FFFFFF);
	font-size: var(--vlz-text-base);
	font-weight: var(--vlz-weight-semibold);
	text-align: center;
	text-decoration: none;
}

.vlz-single-service-modal__button:hover,
.vlz-single-service-modal__button:focus-visible {
	background: #12547F;
	color: var(--vlz-single-modal-button-text-color, #FFFFFF);
}

.vlz-single-service-modal__button:focus-visible {
	outline: var(--vlz-focus-width) solid var(--vlz-color-focus);
	outline-offset: var(--vlz-focus-offset);
}

/*
 * Task 1 §D3: body scroll lock while the dialog is open is applied/removed
 * by JS as `document.documentElement` (not `body`) getting this class — see
 * assets/js/single-post.js. `<html>` rather than `<body>` because some
 * browsers still allow rubber-band/touch scroll on `<body>` alone.
 */
html.vlz-scroll-locked {
	overflow: hidden;
}

/* Tablet portrait (≤900px, covers the 768×1024 reference viewport): the
 * two-column composition gets cramped before Mobile's own breakpoint, so it
 * drops to the same single-column stack Mobile uses rather than forcing two
 * narrow columns (Owner brief §20 — a QA judgement call, not a fixed rule). */
@media (max-width: 900px) {
	.vlz-single-service-modal {
		width: 90vw;
		max-width: 560px;
		/*
		 * Final micro correction: the base rule's `max-height: min(88vh,
		 * 640px)` caps Tablet at a flat 640px regardless of the taller
		 * 768×1024 viewport (88vh there is 901px), so content that needed
		 * ~860–960px of stacked single-column height was forced into an
		 * internal scrollbar well before the viewport ran out of room.
		 * Raised to a Tablet-appropriate cap so the dialog can actually use
		 * the available height instead of clipping to a Mobile-ish number.
		 */
		max-height: 85vh;
	}

	/* Owner correction: the heading is the Modal's actual content start on
	 * Tablet/Mobile — it now leads the stack, ahead of the image, instead of
	 * the image opening the visible content. */
	/*
	 * Mobile close-button/heading overlap fix (2026-10-16): below 901px the
	 * heading above is the FIRST stacked grid item, directly under the close
	 * button (`.vlz-single-service-modal__close-form`, `position: absolute;
	 * top: 0.85rem; left: 0.85rem;`, button `width/height: 2rem`). The
	 * heading is centered text spanning the full column width with no
	 * horizontal inset, so on narrow viewports its RTL-trailing word (the
	 * physically left-most word of a right-to-left sentence) lands directly
	 * under the button — confirmed live via getBoundingClientRect() at
	 * 360×640 (button right edge 82.2px vs. the heading's first text line
	 * extending to 58.9–316px — a real, measured intersection, not just a
	 * visual impression). A horizontal-only fix (not extra top height) was
	 * chosen deliberately: it adds zero vertical space on already-short
	 * mobile viewports (360–414px) where the dialog's own internal scroll
	 * already engages. Value derived directly from the button's own authored
	 * geometry — 0.85rem offset + 2rem width + a small 0.25rem gap — so the
	 * two rules stay self-consistent if the button is ever resized.
	 */
	.vlz-single-service-modal__heading {
		padding-left: 3.1rem;
	}

	.vlz-single-service-modal__layout {
		grid-template-columns: 1fr;
		grid-template-areas:
			"heading"
			"label"
			"image"
			"text"
			"benefits"
			"action";
		gap: 0;
		/* Compaction: trim the vertical padding a step from Desktop's
		 * space-xl/space-lg so the stack takes noticeably less height,
		 * without going as tight as Mobile's own space-sm. */
		padding: var(--vlz-space-md) var(--vlz-space-sm) var(--vlz-space-sm);
	}

	.vlz-single-service-modal__heading {
		/* Tablet target ~26–30px. */
		font-size: 1.75rem;
		margin: 0 0 var(--vlz-space-2xs);
	}

	.vlz-single-service-modal__label {
		justify-self: center;
		margin: 0 0 var(--vlz-space-xs);
	}

	.vlz-single-service-modal__visual-image {
		margin-block-end: var(--vlz-space-xs);
	}

	.vlz-single-service-modal__image {
		/* Compaction: `aspect-ratio: 4/3` at Tablet's ~90vw width produced a
		 * tall image (a large share of the internal-scroll overflow) — a
		 * fixed height keeps the image clearly visible without dominating
		 * the stack's height budget. */
		max-height: none;
		aspect-ratio: auto;
		height: 200px;
	}

	.vlz-single-service-modal__text {
		margin: 0 0 var(--vlz-space-xs);
	}

	.vlz-single-service-modal__benefits li {
		padding: var(--vlz-space-2xs) 0;
	}

	.vlz-single-service-modal__action-card {
		margin-block-start: var(--vlz-space-xs);
		padding: var(--vlz-space-sm);
	}
}

@media (max-width: 767.98px) {
	.vlz-single-service-modal {
		width: 88vw;
		max-width: 88vw;
		max-height: 85vh;
		border-radius: var(--vlz-radius-md);
	}

	/* Mobile height/no-internal-scroll correction: the stack was noticeably
	 * taller than the 390×844 viewport, forcing an internal scrollbar.
	 * Trimmed padding, gaps and the image height (not a max-height clamp on
	 * the dialog itself) so the whole content genuinely fits. */
	.vlz-single-service-modal__layout {
		padding: var(--vlz-space-sm) var(--vlz-space-sm) var(--vlz-space-sm);
	}

	.vlz-single-service-modal__heading {
		/* Mobile target ~24–28px. */
		font-size: 1.5rem;
		margin: 0 0 var(--vlz-space-xs);
	}

	.vlz-single-service-modal__label {
		margin: 0 0 var(--vlz-space-xs);
	}

	/*
	 * Owner Correction — Mobile-only compaction: drop the second
	 * (supporting-text) line under each benefit row, keep the title. Titles
	 * are never removed and Desktop/Tablet keep both lines — Mobile only.
	 */
	.vlz-single-service-modal__benefit-text {
		display: none;
	}

	.vlz-single-service-modal__visual-image {
		margin-block-end: var(--vlz-space-xs);
	}

	.vlz-single-service-modal__image {
		aspect-ratio: auto;
		height: 170px;
	}

	.vlz-single-service-modal__text {
		margin: 0 0 var(--vlz-space-xs);
	}

	.vlz-single-service-modal__benefits li {
		padding: var(--vlz-space-2xs) 0;
	}

	.vlz-single-service-modal__action-card {
		margin-block-start: var(--vlz-space-xs);
		padding: var(--vlz-space-xs);
	}

	.vlz-single-service-modal__action-heading {
		margin: 0 0 0.25rem;
	}

	.vlz-single-service-modal__action-text {
		margin: 0 0 var(--vlz-space-xs);
	}
}

@media (prefers-reduced-motion: reduce) {
	.vlz-single-service-modal[open] {
		animation: none;
	}
}
