/*
 * Utility Classes
 *
 * Single-purpose helper classes for common patterns.
 * Includes text colors and scroll-triggered animations.
 */

/* ============================================
   TEXT COLOR UTILITIES
   ============================================ */
.text-light-on-dark {
  --text-color: var(--text-light-on-dark);

  color: var(--text-light-on-dark);
}

.text-dark-on-light {
  --text-color: var(--text-dark-on-light);

  color: var(--text-dark-on-light);
}

/* ============================================
   SCROLL-TRIGGERED ANIMATIONS
   Elements start hidden/offset and animate
   to visible when .in-view is added via JS
   ============================================ */

/* Fade in */
.fade-in {
  transition: opacity 0.6s var(--cubic-bezier-1);
  opacity: 0;
}

/* Fade up (from below) */
.fade-up {
  transition: var(--fade-transition);
  transform: translateY(30px);
  opacity: 0;
}

/* Fade left (from right) */
.fade-left {
  transition: var(--fade-transition);
  transform: translateX(30px);
  opacity: 0;
}

/* Fade right (from left) */
.fade-right {
  transition: var(--fade-transition);
  transform: translateX(-30px);
  opacity: 0;
}

/* In-view state - applied via IntersectionObserver */
.in-view,
.in-view .fade-in,
.in-view .fade-up,
.in-view .fade-left,
.in-view .fade-right {
  transform: none;
  opacity: 1;
}

/* ============================================
   ACCESSIBILITY
   ============================================ */

/* Visually hidden but available to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip to main content link */
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--digicert-blue);
  color: var(--digicert-white);
  padding: var(--space-s) var(--space-m);
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

/* ============================================
   DISCLAIMER
   Shared "legal fine print" styling applied by
   applyDisclaimer() to the last content element
   of blocks with the `with-disclaimer` variant.
   ============================================ */
.disclaimer {
  font-size: var(--body-font-size-xxs);
  font-weight: var(--font-weight-regular);
  line-height: var(--line-height-normal);
}

/* ============================================
   CONTENT ANCHORS
   Universal invisible scroll-targets created by
   utils/anchors.js from {{anchor:Label}} tokens
   (linkable on any page; the document-reader
   template additionally builds a TOC from them).
   ============================================ */
.content-anchor {
  position: relative; /* containing block for the focus caret below */
  display: block;
  width: 0;
  height: 0;
  overflow: hidden;
  scroll-margin-top: calc(var(--header-height) + var(--space-l));
}

/* Mid-paragraph token: inline so it never breaks the line of prose. */
.content-anchor-inline {
  display: inline;
}

/* Visible focus indicator after a keyboard TOC jump. The anchor is a 0×0 hidden scroll
   hook, so focus would otherwise have no visible ring. On focus, show a short accent bar
   at the anchor's position (the section start you just landed on), escaping the 0×0 clip.
   :focus (not :focus-visible) is safe here — the anchor is tabindex="-1", only ever focused
   programmatically by the reader's goTo, which now moves focus for keyboard activation
   only. position:absolute → no layout shift. */
.content-anchor:focus {
  overflow: visible;
  outline: none;
}

.content-anchor:focus::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 3px;
  height: 1.5em;
  border-radius: 2px;
  background: var(--link-color);
}

/* Windows High Contrast forces author backgrounds away, so the caret's fill would vanish
   (leaving a keyboard user no focus indicator after a TOC jump). A system color is honored
   in forced-colors and Highlight reads as a focus/selection cue. */
@media (forced-colors: active) {
  .content-anchor:focus::after {
    background: Highlight;
  }
}

/* PAGE SCOPE of the alignment axis (page Metadata `Style`). The axis, its default and the section
   scope are in styles/sections.css — read that comment first. utils/page-style.js puts these
   classes on <body>, filtered against a closed list. Both files are eager links in head.html. */

/* PAGE-STYLES:BEGIN - every page scope style lives between here and the matching end marker, and
   each must be listed in PAGE_STYLES in utils/page-style.js. test/page-style.test.mjs fails if the
   two fall out of step either way. Do not remove these markers. */

body.align-left {
  --section-align: start;
  --section-measure-margin: 0 auto;
}

body.align-center {
  --section-align: center;
  --section-measure-margin: auto;
}

/* PAGE-STYLES:END */

/* ============================================
   DOCUMENT-READER CONTENT CLEARANCE (eager)
   Loaded eagerly (head.html) so the rail-clearance padding is present at FIRST PAINT: the
   panel widget CSS (styles/document-reader.css) is lazy, and applying this clearance only
   when that lands shifts the whole content column right on cold-cache reader pages (CLS).
   Only the content-side clearance lives here; the panel's own styling stays with the block.
   ============================================ */
body.document-reader {
  --document-reader-collapsed-w: 44px; /* collapsed rail width — fits Firefox's ~11px scrollbar */
  --document-reader-rail-space: calc(var(--document-reader-collapsed-w) + var(--space-xs));
}

/* Push reader content clear of the collapsed rail, but only by the part the centered
   container's own margin doesn't already absorb: from ~1360px up ((100% - container)/2 >=
   rail-space) the rail sits in the empty margin, so the formula self-resets to
   container-padding (no separate >=1600 rule needed) and never shoves the column off-center. */
body.document-reader main > .section > div {
  padding-left: calc(
    var(--container-padding)
    + max(0px, var(--document-reader-rail-space) - max(0px, (100% - var(--container-max-width)) / 2))
  );
}

/* Full-bleed blocks set their own padding (hero bg-image: padding:0; full-width logos:
   padding:unset) and out-specify the rule above, so the rail would overlap their left
   content. Re-clear them: the hero via its own --content-inset token (all widths — the
   max() picks centering when the gutter is wide, rail-clearance when narrow); the logos
   wrapper via padding-left that falls to 0 once the gutter absorbs the rail (mobile clears
   the edge rail; the rarer >=1600 floating-rail-over-full-width case is left to a ticket). */
body.document-reader .hero.bg-image,
body.document-reader .hero-event-beta.bg-image {
  --content-inset: max(
    var(--container-padding),
    calc((100% - var(--container-max-width)) / 2 + var(--container-padding)),
    calc(var(--container-padding) + max(0px, var(--document-reader-rail-space) - max(0px, (100% - var(--container-max-width)) / 2)))
  );
}

body.document-reader main > .section.full-width > .logos-wrapper {
  padding-left: max(0px, var(--document-reader-rail-space) - max(0px, (100% - var(--container-max-width)) / 2));
}

/* Default (non-bg-image) hero is a third case: not full-bleed, but its own wrapper padding
   (main > .section.hero-container > .hero-wrapper, blocks/hero/hero.css) out-specifies the
   generic clearance rule above, so the collapsed rail overlaps the headline. Re-assert the
   identical self-resetting term at winning specificity (resets to container-padding once the
   gutter absorbs the rail). bg-image heroes are unaffected — their :has() full-width wrapper
   rule out-specifies this and they clear via --content-inset. */
body.document-reader main > .section.hero-container > .hero-wrapper {
  padding-left: calc(
    var(--container-padding)
    + max(0px, var(--document-reader-rail-space) - max(0px, (100% - var(--container-max-width)) / 2))
  );
}

/* document-reader-beneath-hero: the TOC rides below the hero and never sits beside it, so the
   hero needs no rail clearance — reset it to its normal (non-reader) insets. Body sections keep
   their clearance (the rail still sits beside them once it rises). The toggle class is set on
   <body> at the reader module's import-time init — before `document-reader` is added — so this is
   active from the first frame the reader clearance exists (no CLS). */
body.document-reader.document-reader-beneath-hero main > .section.hero-container > .hero-wrapper {
  padding-left: var(--container-padding);
}

body.document-reader.document-reader-beneath-hero .hero.bg-image,
body.document-reader.document-reader-beneath-hero .hero-event-beta.bg-image {
  --content-inset: max(
    var(--container-padding),
    calc((100% - var(--container-max-width)) / 2 + var(--container-padding))
  );
}

/* Author guards (preview/dev only): utils/anchors.js adds html.content-anchor-debug when
   it detects an authoring problem, revealing a warning badge at the spot. Inert in
   production (the class is never added there). The variants:
   - [data-duplicate]   reused label — the page still works (ids deduped); rename it
   - [data-stripped]    fragment anchor removed on a reader page (TOC is page-authored only)
   - [data-empty-label] {{anchor:   }} — works as a target, can never join the TOC
   - .content-anchor-survivor  host of literal token text that survived normalization
     (typo'd keyword, single braces, unterminated, split by inline formatting, in a block)

   utils/section-autotab.js adds html.tab-style-debug for the same purpose, on the same
   shouldDebug() gate (utils/author-debug.js). It marks the SECTION rather than a content
   node, because the mistake is section-level configuration with nothing in the content to
   point at:
   - [data-tab-style-orphan]   a tab-style-* skin on a section that isn't tabbed at all
   - [data-tab-style-unknown]  a tab-style-* value that isn't a recognized skin (typo)
   These badge on ::before, not ::after: `main > .section` is a flex column, so ::before is
   the first flex item and lands at the TOP of the section, next to where the Section
   Metadata table was — an ::after would render at the section's bottom edge, potentially a
   screenful away. The badge therefore adds its own height plus the section gap; that layout
   shift is intentional and preview-only. Do NOT "fix" it with position:absolute — that would
   require position:relative on the section, which is the complication in-flow avoids. */
html.content-anchor-debug .content-anchor[data-duplicate],
html.content-anchor-debug .content-anchor[data-stripped],
html.content-anchor-debug .content-anchor[data-empty-label] {
  display: inline;
  width: auto;
  height: auto;
  overflow: visible;
}

html.content-anchor-debug .content-anchor[data-duplicate],
html.content-anchor-debug .content-anchor-survivor {
  outline: 2px dashed var(--digicert-red);
}

/* No var() fallbacks: tokens.css always loads with this file, and hardcoded copies of
   token values silently go stale if the token changes. */
html.content-anchor-debug .content-anchor[data-duplicate]::after,
html.content-anchor-debug .content-anchor[data-stripped]::after,
html.content-anchor-debug .content-anchor[data-empty-label]::after,
html.content-anchor-debug .content-anchor-survivor::after {
  display: inline-block;
  margin-inline-start: var(--space-xs);
  padding: 0 var(--space-xs);
  border-radius: var(--border-radius-xs);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-xxs);
  font-weight: 700;
  white-space: nowrap;
}

html.content-anchor-debug .content-anchor[data-duplicate]::after {
  content: "⚠ duplicate anchor → #" attr(id);
}

html.content-anchor-debug .content-anchor[data-stripped]::after {
  content: "⚠ anchor removed (reader page): " attr(data-label);
}

html.content-anchor-debug .content-anchor[data-empty-label]::after {
  content: "⚠ empty anchor label → #" attr(id);
}

html.content-anchor-debug .content-anchor-survivor::after {
  content: "⚠ unprocessed anchor token";
}

/* Section-level author badges. `margin-inline` because the content inset comes from
   `main > .section > div` (sections.css) and matches div children only — a pseudo-element
   gets none of it and would otherwise sit flush against the viewport edge. */
html.tab-style-debug .section[data-tab-style-orphan]::before,
html.tab-style-debug .section[data-tab-style-unknown]::before,
html.sides-debug .section[data-sides-warning]::before,
html.fragment-debug .section[data-fragment-warning]::before {
  align-self: flex-start;
  margin-inline: var(--container-padding);
  padding: 0 var(--space-xs);
  border-radius: var(--border-radius-xs);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-xxs);
  font-weight: 700;
}

html.tab-style-debug .section[data-tab-style-orphan]::before {
  content: "⚠ " attr(data-tab-style-orphan) " has no effect — section is missing tabbed-blocks";
}

html.tab-style-debug .section[data-tab-style-unknown]::before {
  content: "⚠ unknown tab style: " attr(data-tab-style-unknown) " — falling back to horizontal";
}

/* blocks/fragment/fragment.js: Section Metadata that has no effect because of where the fragment
   block sits (a page section the fragment replaces, or a fragment sharing a section). */
html.fragment-debug .section[data-fragment-warning]::before {
  content: "⚠ " attr(data-fragment-warning);
}

/* utils/section-sides.js joins every message for a section into one attribute, because a section
   has one ::before. */
html.sides-debug .section[data-sides-warning]::before {
  content: "⚠ " attr(data-sides-warning);
}

/* From 900px a structured sides section is a grid, and an unplaced ::before lands in the first
   free cell, which is below the sides. Put the badge in row 1 across both columns and move the
   explicitly placed rows down one (sections.css places the top row and side 1; later sides follow
   by auto-placement). The section carries the container inset at this width, so the badge drops
   its own. */
@media (width >= 900px) {
  html.sides-debug main > .section.sides.structured-headings[data-sides-warning]::before {
    grid-column: 1 / -1;
    grid-row: 1;
    justify-self: start;
    margin-inline: 0;
  }

  html.sides-debug main > .section.sides.structured-headings[data-sides-warning] > .sides-top,
  html.sides-debug main > .section.sides.structured-headings[data-sides-warning] > .side[data-side="1"] {
    grid-row: 2;
  }

  html.sides-debug main > .section.sides.structured-headings[data-sides-warning] > .sides-top ~ .side[data-side="1"] {
    grid-row: 3;
  }
}

/* PAGE-LEVEL author badges, on `main::before`: a page-level problem has no section to hang on and
   `body::before` sits under the fixed header. Inert in production — utils/page-style.js adds
   html.page-style-debug only where utils/author-debug.js allows it. */
html.page-style-debug main[data-page-style-warning]::before {
  display: block;
  margin: var(--container-padding);
  padding: var(--space-xl);
  border-radius: var(--border-radius-l);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-m);
  font-weight: var(--font-weight-bold);
  content: attr(data-page-style-warning);
}

/* dynamic-cards author badges. Inert in production — blocks/dynamic-cards/dynamic-cards.js adds
   html.dynamic-cards-debug only where utils/author-debug.js allows it. The block badge sits at
   the top of the block: a configuration mistake (the block then renders nothing else), an empty
   list, or a tie at an Include / Exclude newest-N cut. The row badge sits at the top of the card: a missing or
   malformed editorial date, a byline name problem, or an image a date tile displaces. On a
   horizontal row it takes a line of its own above the left cell and the text. Both are in-flow
   ::before, for the same reason as the section badges
   above. `position: relative` + `z-index` keep a row badge above a link-card's overlay link.
   Known collision, left as is: a hero card draws its background overlay on the same li::before
   (blocks/dynamic-cards/dynamic-cards.css), so a row badge on a hero card breaks the overlay on
   preview. No production page uses hero-card, and the variant is to be removed (PROTO-735). */
html.dynamic-cards-debug .dynamic-cards[data-dynamic-cards-warning]::before,
html.dynamic-cards-debug .dynamic-cards li[data-dynamic-cards-warning]::before {
  display: block;
  position: relative;
  z-index: 1;
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--border-radius-xs);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-xxs);
  font-weight: 700;
}

html.dynamic-cards-debug .dynamic-cards[data-dynamic-cards-warning]::before {
  margin-block-end: var(--space-m);
  content: "⚠ dynamic-cards: " attr(data-dynamic-cards-warning);
}

html.dynamic-cards-debug .dynamic-cards li[data-dynamic-cards-warning]::before {
  align-self: stretch;
  content: "⚠ " attr(data-dynamic-cards-warning);
}

/* Side by side (image-left, or any horizontal row from 600px), the row wraps so the badge gets the
   whole first line. Stacked, the badge is already the first line. */
html.dynamic-cards-debug .dynamic-cards.horizontal.image-left li[data-dynamic-cards-warning] {
  flex-wrap: wrap;
}

html.dynamic-cards-debug .dynamic-cards.horizontal.image-left li[data-dynamic-cards-warning]::before {
  flex-basis: 100%;
}

@media (width >= 600px) {
  html.dynamic-cards-debug .dynamic-cards.horizontal li[data-dynamic-cards-warning] {
    flex-wrap: wrap;
  }

  html.dynamic-cards-debug .dynamic-cards.horizontal li[data-dynamic-cards-warning]::before {
    flex-basis: 100%;
  }
}

/* cards author badges, for horizontal rows. Inert in production — blocks/cards/cards.js adds
   html.cards-debug only where utils/author-debug.js allows it. The block badge sits at the top of
   the block: more than two cards per row. The row badge sits at the top of the row: a link-card row
   without exactly one link, or a non-square image. The same shape as the dynamic-cards badges
   above; the row badge takes a line of its own above the image and the text. */
html.cards-debug .cards[data-cards-warning]::before,
html.cards-debug .cards li[data-cards-warning]::before {
  display: block;
  position: relative;
  z-index: 1;
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--border-radius-xs);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-xxs);
  font-weight: 700;
}

html.cards-debug .cards[data-cards-warning]::before {
  margin-block-end: var(--space-m);
  content: "⚠ cards: " attr(data-cards-warning);
}

html.cards-debug .cards li[data-cards-warning]::before {
  align-self: stretch;
  content: "⚠ " attr(data-cards-warning);
}

html.cards-debug .cards.horizontal.image-left li[data-cards-warning] {
  flex-wrap: wrap;
}

html.cards-debug .cards.horizontal.image-left li[data-cards-warning]::before {
  flex-basis: 100%;
}

@media (width >= 600px) {
  html.cards-debug .cards.horizontal li[data-cards-warning] {
    flex-wrap: wrap;
  }

  html.cards-debug .cards.horizontal li[data-cards-warning]::before {
    flex-basis: 100%;
  }
}

/* fragment author badge, on the block. Inert in production — blocks/fragment/fragment.js adds
   html.fragment-debug only where utils/author-debug.js allows it, when a fragment does not load.
   The block is empty then, so the badge is all it shows. The same shape as the cards badges. The
   section badges for Section Metadata that has no effect are with the section-level badges above. */
html.fragment-debug .fragment[data-fragment-warning]::before {
  display: block;
  padding: var(--space-xs) var(--space-s);
  border-radius: var(--border-radius-xs);
  background: var(--digicert-red);
  color: var(--digicert-white);
  font-size: var(--body-font-size-xxs);
  font-weight: 700;
  content: "⚠ fragment " attr(data-fragment-warning);
}

/* ============================================
   REDUCED MOTION
   Respects user preference for reduced motion
   ============================================ */
@media (prefers-reduced-motion: reduce) {
  .fade-in,
  .fade-up,
  .fade-left,
  .fade-right {
    transition: none;
    transform: none;
    opacity: 1;
  }
}
