/*
 * Path:        public_html/assets/css/main.css
 * Filename:    main.css
 * Project:     revisualized.com
 * Description: Design tokens for all three theme modes, reset, base typography,
 *              and every component shared across pages: preferences selector row,
 *              navigation, mobile navigation panel, project cards, article cards,
 *              article page preview, about block, footer, badges, content filter,
 *              terminal component, content body typography, accessibility utilities.
 *              Home-only components (hero, featured) live in page_home.css.
 *              All token values verified at 7:1 by scripts/verify_contrast_ratios.py
 *              Adjustments to individual token values are documented inline
 *              where they occur.
 * Status:      production
 * Revision:    15
 * Updated:     2026-08-22 (Rev 15: homepage article cards now match the
 *              preview. Rev 13 gave them align-items: start and a fixed 16/9
 *              image inside the card's 20px padding, so the thumbnail sat
 *              short and inset at the top with a tall band of empty card
 *              below it while the preview beside it ran its image flush and
 *              full height. Padding moves from the card to the body and the
 *              image track stretches. The base .article_card__main_image
 *              keeps its 16/9 ratio for the /articles listing, which stays
 *              stacked.
 *              Rev 14: .footer__logo_shadow is retired for
 *              .footer__logo_mark. The old rule set a 6rem letter R at 0.04
 *              opacity, which is not a watermark at that value, it is
 *              invisible. footer.php Rev 3 inlines the real SVG, which fills
 *              with currentColor, so the colour token here drives it and it
 *              follows the theme. Height is auto because the artwork is
 *              roughly 3.5:1 and a fixed height would distort it.
 *              Rev 13: article imagery sits beside the text rather
 *              than above it, matching .featured__block. The preview is a
 *              220px + 1fr grid and the homepage cards are 160px + 1fr; both
 *              carry a --no_media modifier stamped by index.php in the same
 *              condition that emits the image, so a piece with no hero base
 *              collapses to one column instead of holding an empty image
 *              track. The card rule is scoped to .article_card_row because the
 *              /articles listing shares the class in a narrower grid where a
 *              split leaves too little measure. Both return to stacked below
 *              700px. Rev 12 added the slots stacked above the text.
 *              Rev 11: browser-served comments depersonalized. Build-process
 *              and third-person vocabulary was removed from every comment in
 *              this file, since these ship to the browser and describe the
 *              stylesheet, not the process that produced it.
 *              Rev 10: project card bottom row. In the narrow columns of the
 *              projects grid the Read more button held its width via
 *              white-space: nowrap and flex-shrink: 0, leaving the tag list too
 *              little room to flow, so three badges stacked one per line and
 *              cards in the same row ended at different heights. The row now
 *              wraps, the tags claim available width first, and the button
 *              keeps its right alignment once it wraps to its own line.
 *              margin-top: auto pins the row to the card bottom so the buttons
 *              align regardless of description length.)
 * Included by: includes/header.php (every page)
 */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html { font-size: clamp(16px, 1rem + 0.5vw, 20px); }

/*
  Font size accessibility overrides.
  Attribute selectors have higher specificity than the base element rule,
  so these fire as soon as data-font-size is set on the html element.
  The head script applies this attribute before CSS parses, preventing flash.
*/
html[data-font-size="small"]  { font-size: clamp(13px, 0.8125rem + 0.4vw, 16px); }
html[data-font-size="normal"] { font-size: clamp(16px, 1rem      + 0.5vw, 20px); }
html[data-font-size="large"]  { font-size: clamp(19px, 1.1875rem + 0.6vw, 24px); }

/* Body reading text: one token controls prose size site-wide. Referenced by
   every prose and lede rule (about copy, article and project bodies, support
   pages, listing ledes, featured description, detail lede and status line).
   Bump this single value to resize all of them together. */
:root { --text-body: 1rem; }

body {
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 1rem;
  min-height: 100vh;
  background-color: var(--color-page-background);
  color: var(--color-page-text);
}

html[data-transitioning],
html[data-transitioning] * {
  transition: background-color 240ms ease,
              color           240ms ease,
              border-color    240ms ease !important;
}

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

/* Skip link: visually hidden until focused. */
.skip_link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 10px 16px;
  background: var(--color-page-background);
  color: var(--color-accent);
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  font-size: 0.8125rem;
  font-weight: 600;
  text-decoration: none;
}

.skip_link:focus {
  left: 12px;
  top: 12px;
}

/* Screen reader only utility. Used for the homepage heading. */
.visually_hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Visible focus, never suppressed (Section 9.5). Consistent indicator. */
/* The listing filter in main.js hides cards by setting the hidden
   attribute. .article_card and .project_card both declare display:flex,
   and any author declaration beats the user-agent [hidden]{display:none},
   so without this every filter button on /articles and /projects was
   inert. !important is deliberate: it has to outrank whatever display a
   future component sets on a filterable element. */
[hidden] { display: none !important; }

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Respect reduced motion preferences (Section 9.5 spirit). */
@media (prefers-reduced-motion: reduce) {
  html[data-transitioning],
  html[data-transitioning] *,
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ============================================================
   THEME TOKENS
   Token parity rule: all three modes define the same token
   names. When adding a token, add it to all three blocks in
   the same edit. Terminal tokens are per theme and switch with
   the theme (decision, 2026-07-05).
============================================================ */

/* ----------------------------------------------------------
   COBALT dark
   bg #0a1040 saturated cobalt-indigo
   Accent: #ff7878 (verified 7.07:1 on page background)
   Adjustments measured by verify_contrast_ratios.py:
     --color-text-label  #8aa0c4 (measured 6.81:1) -> #8ea4c8 (7.14:1)
     --color-text-muted  #96a8cc (measured 6.90:1 on card) -> #9baccf (7.23:1)
     --color-text-badge  #96a8cc (same measurement) -> #9baccf
   Earlier figures of ~7.5:1 and ~8.3:1 did not reproduce under
   measurement. These are the nearest compliant values, following the
   same precedent as the accent.
---------------------------------------------------------- */
:root[data-theme="dark"] {
  --color-page-background:      #0a1040;
  --color-page-text:            #cad8f6;   /* 12.63:1 verified */

  --color-border-dashed:        rgba(202, 216, 246, 0.14);
  --color-border-separator:     rgba(202, 216, 246, 0.07);
  --color-border-strong:        rgba(202, 216, 246, 0.26);
  --color-border-badge:         rgba(202, 216, 246, 0.20);
  --color-border-navigation:    rgba(202, 216, 246, 0.10);
  --color-input-border:         rgba(202, 216, 246, 0.22);

  /* Selector: translucent light surface + visible rim + light text */
  --color-surface-selector:     rgba(255, 255, 255, 0.10);
  --color-selector-border:      rgba(255, 255, 255, 0.22);
  --color-selector-text:        #a8b8d6;   /* 7.01:1 on selector surface, verified */
  --color-selector-text-active: #cad8f6;   /* 5.96:1 on active surface, AA, locked advisory */
  --color-selector-active-bg:   rgba(255, 255, 255, 0.16);

  --color-surface-navigation:   rgba(0, 0, 0, 0.22);
  --color-surface-card:         rgba(255, 255, 255, 0.04);

  --color-text-nav:             #a8b8d6;   /* 9.04:1 verified */
  --color-text-nav-active:      #cad8f6;
  --color-text-label:           #8ea4c8;   /* 7.14:1 verified, adjusted, see block note */
  --color-text-muted:           #9baccf;   /* 7.92:1 page, 7.23:1 card, adjusted */
  --color-text-badge:           #9baccf;   /* adjusted with muted */

  --color-accent:               #ff7878;   /* 7.07:1 on page, verified; 6.46:1 on card, AA, locked */
  --color-accent-hover:         #ff9090;   /* 8.31:1 verified */

  /* Terminal tokens, Cobalt. Verified against #0d1117 and the
     title bar. One adjustment: dim #7b8490 measured 4.95:1, raised to
     #9ea8b6 (7.87:1 background, 7.24:1 title bar). Pink kept at the
     original candidate #ff7b72 (7.51:1, passes). */
  --terminal-background:        #0d1117;
  --terminal-title-bar:         #1a1a1a;
  --terminal-border:            #30363d;
  --terminal-token-dim:         #9ea8b6;
  --terminal-token-green:       #3fb950;
  --terminal-token-cyan:        #79c0ff;
  --terminal-token-yellow:      #e3b341;
  --terminal-token-white:       #c9d1d9;
  --terminal-token-pink:        #ff7b72;
}

/* ----------------------------------------------------------
   PORCELAIN light
   bg #f4f8ff icy cool near-white
   Accent: #1a40b0 (verified 8.20:1 on page background)
   Terminal direction: a light surface tuned to the theme rather than a
   dark panel, which exercises the per-theme switching rule fully.
   Moving to a dark panel means editing the nine values in this block
   and nothing else. All nine verified at 7:1.
---------------------------------------------------------- */
:root[data-theme="light"] {
  --color-page-background:      #f4f8ff;
  --color-page-text:            #0a0e1a;   /* 18.08:1 verified */

  --color-border-dashed:        rgba(10, 14, 26, 0.14);
  --color-border-separator:     rgba(10, 14, 26, 0.07);
  --color-border-strong:        rgba(10, 14, 26, 0.26);
  --color-border-badge:         rgba(10, 14, 26, 0.20);
  --color-border-navigation:    rgba(10, 14, 26, 0.10);
  --color-input-border:         rgba(10, 14, 26, 0.22);

  /* Selector: translucent dark surface + visible dark rim + dark text */
  --color-surface-selector:     rgba(10, 14, 26, 0.06);
  --color-selector-border:      rgba(10, 14, 26, 0.22);
  --color-selector-text:        #2c4268;   /* 8.34:1 on selector surface, verified */
  --color-selector-text-active: #0a0e1a;
  --color-selector-active-bg:   rgba(10, 14, 26, 0.12);

  --color-surface-navigation:   rgba(10, 14, 26, 0.06);
  --color-surface-card:         rgba(0, 0, 0, 0.035);

  --color-text-nav:             #1a2e5e;   /* 12.35:1 verified */
  --color-text-nav-active:      #0a0e1a;
  --color-text-label:           #374878;   /* 8.35:1 verified */
  --color-text-muted:           #2c4268;   /* 9.45:1 verified */
  --color-text-badge:           #374878;

  --color-accent:               #1a40b0;   /* 8.20:1 verified */
  --color-accent-hover:         #142e88;   /* 11.10:1 verified */

  --terminal-background:        #e8eef8;
  --terminal-title-bar:         #dbe3f0;
  --terminal-border:            #b6c2d8;
  --terminal-token-dim:         #3a475f;   /* 8.02:1 background, 7.24:1 title bar */
  --terminal-token-green:       #0c5a28;   /* 7.18:1 */
  --terminal-token-cyan:        #0b4a9e;   /* 7.24:1 */
  --terminal-token-yellow:      #644500;   /* 7.53:1 */
  --terminal-token-white:       #131c30;   /* 14.57:1 */
  --terminal-token-pink:        #8c1a3c;   /* 7.74:1 */
}

/* ----------------------------------------------------------
   MONO
   Terminal tokens: grayscale only, differentiated by
   lightness steps, preserving the monochrome identity. All verified
   at 7:1 against #101010 and the dim token against the title bar.
---------------------------------------------------------- */
:root[data-theme="monochrome"] {
  --color-page-background:      #0a0a0a;
  --color-page-text:            #f0f0f0;

  --color-border-dashed:        rgba(255, 255, 255, 0.18);
  --color-border-separator:     rgba(255, 255, 255, 0.08);
  --color-border-strong:        rgba(255, 255, 255, 0.30);
  --color-border-badge:         rgba(255, 255, 255, 0.22);
  --color-border-navigation:    rgba(255, 255, 255, 0.08);
  --color-input-border:         rgba(255, 255, 255, 0.20);

  --color-surface-selector:     rgba(255, 255, 255, 0.10);
  --color-selector-border:      rgba(255, 255, 255, 0.22);
  --color-selector-text:        #b5b5b5;   /* 7.76:1 on selector surface, verified */
  --color-selector-text-active: #f0f0f0;
  --color-selector-active-bg:   rgba(255, 255, 255, 0.16);

  --color-surface-navigation:   rgba(255, 255, 255, 0.04);
  --color-surface-card:         rgba(255, 255, 255, 0.04);

  --color-text-nav:             #cecece;
  --color-text-nav-active:      #f0f0f0;
  --color-text-label:           #b5b5b5;
  --color-text-muted:           #cecece;
  --color-text-badge:           #cecece;

  --color-accent:               #c0c0c0;   /* 10.88:1 verified */
  --color-accent-hover:         #e0e0e0;

  --terminal-background:        #101010;
  --terminal-title-bar:         #1c1c1c;
  --terminal-border:            #3a3a3a;
  --terminal-token-dim:         #a8a8a8;   /* 8.00:1 background, 7.17:1 title bar */
  --terminal-token-green:       #cccccc;
  --terminal-token-cyan:        #dedede;
  --terminal-token-yellow:      #bcbcbc;
  --terminal-token-white:       #f0f0f0;
  --terminal-token-pink:        #b0b0b0;
}

/* ============================================================
   CONTROLS ROW
   Font size selector (left) and theme selector (right) sit in
   a single flex row. Both use --color-selector-* tokens and
   animate with the rest of the page during theme transitions.
============================================================ */

.selector {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  padding: 8px 12px 12px;
  border-bottom: 1px solid var(--color-border-separator);
}

/* ---- Font size selector ---- */

.font_size_selector__group {
  display: flex;
  gap: 3px;
  background: var(--color-surface-selector);
  border: 1px solid var(--color-selector-border);
  border-radius: 8px;
  padding: 3px;
}

.font_size_selector__button {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 5px 10px;
  border-radius: 5px;
  border: none;
  background: transparent;
  font-size: 0.75rem;
  font-family: inherit;
  cursor: pointer;
  color: var(--color-selector-text);
  transition: color 160ms ease, background-color 160ms ease;
  line-height: 1;
}

.font_size_selector__button:hover { color: var(--color-selector-text-active); }

.font_size_selector__button[aria-checked="true"] {
  background: var(--color-selector-active-bg);
  color: var(--color-selector-text-active);
}

.font_size_selector__arrow--small  { font-size: 0.72em; font-weight: 600; }
.font_size_selector__arrow--large  { font-size: 1.40em; font-weight: 600; line-height: 1; }

.font_size_selector__separator {
  display: flex;
  align-items: center;
  color: var(--color-selector-border);
  font-size: 0.75rem;
  font-weight: 300;
  pointer-events: none;
  user-select: none;
}

/*
  The three A letters are fixed sizes relative to the button's
  own font-size (0.75rem) so the widget reads consistently
  as a visual scale indicator regardless of active font size.
*/
.font_size_selector__letter--small  { font-size: 0.72em; font-weight: 600; }
.font_size_selector__letter--normal { font-size: 1.00em; font-weight: 600; }
.font_size_selector__letter--large  { font-size: 1.40em; font-weight: 600; line-height: 1; }

/* ---- Theme selector ---- */

.selector__group {
  display: flex;
  gap: 3px;
  background: var(--color-surface-selector);
  border: 1px solid var(--color-selector-border);
  border-radius: 8px;
  padding: 3px;
}

.selector__button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 5px;
  border: none;
  background: transparent;
  font-size: 0.75rem;
  font-family: inherit;
  font-weight: 500;
  cursor: pointer;
  color: var(--color-selector-text);
  transition: color 160ms ease, background-color 160ms ease;
}

.selector__button:hover { color: var(--color-selector-text-active); }

.selector__button[aria-checked="true"] {
  background: var(--color-selector-active-bg);
  color: var(--color-selector-text-active);
}

.selector__button__icon { width: 15px; height: 15px; flex-shrink: 0; }
.selector__button__text { white-space: nowrap; }

/*
  Decision (2026-07-05): selector button text persists at every width.
  An earlier rule hid .selector__button__text below 520px; it is retired
  and must not be restored as a fix. The padding compression below is
  kept deliberately.
*/
@media (max-width: 520px) {
  .selector__button { padding: 7px; }
}

/* ============================================================
   NAVIGATION
============================================================ */

.navigation {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 20px 40px;
  background-color: var(--color-surface-navigation);
  border-bottom: 1px solid var(--color-border-navigation);
}

.navigation__links { display: flex; align-items: center; gap: 40px; }

.navigation__link {
  font-size: 1.21875rem;
  font-weight: 500;
  color: var(--color-text-nav);
  text-decoration: none;
  transition: color 160ms ease;
}

.navigation__link:hover { color: var(--color-text-nav-active); }

.navigation__link[aria-current="page"] { color: var(--color-text-nav-active); }

.navigation__hamburger {
  display: none;
  border: none;
  background: transparent;
  color: var(--color-text-nav);
  font-size: 1.25rem;
  cursor: pointer;
  padding: 4px;
}

@media (max-width: 600px) {
  .navigation__links     { display: none; }
  .navigation__hamburger { display: block; }
}

/* ============================================================
   MOBILE NAVIGATION PANEL
   The hamburger previously shipped with no panel and no handler.
   Behavior is specified: focus trap, Escape to close, aria wiring
   in main.js. The visual treatment is built entirely from existing
   tokens and can be adjusted without touching behavior.
============================================================ */

.mobile_navigation_panel {
  display: none;
  background-color: var(--color-surface-navigation);
  border-bottom: 1px solid var(--color-border-navigation);
  padding: 8px 20px 16px;
}

.mobile_navigation_panel[data-open="true"] { display: block; }

.mobile_navigation_panel__list {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.mobile_navigation_panel__link {
  display: block;
  padding: 12px 4px;
  font-size: 1.0625rem;
  font-weight: 500;
  color: var(--color-text-nav);
  text-decoration: none;
  border-bottom: 1px dashed var(--color-border-separator);
  transition: color 160ms ease;
}

.mobile_navigation_panel__link:hover { color: var(--color-text-nav-active); }

.mobile_navigation_panel__link[aria-current="page"] { color: var(--color-text-nav-active); }

.mobile_navigation_panel__close {
  margin-top: 12px;
  padding: 8px 14px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: var(--color-surface-card);
  color: var(--color-text-nav);
  font-family: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
}

@media (min-width: 601px) {
  .mobile_navigation_panel[data-open="true"] { display: none; }
}

/* ============================================================
   PROJECTS GRID
============================================================ */

.projects {
  width: 100%;
  padding: 32px 20px;
  border-top: none;
  box-sizing: border-box;
}

/* auto-fit rather than a fixed three, so the row fills whatever number of
   projects ship. A hardcoded 3 left an empty column at two projects. */
.projects__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px; }

.projects__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 20px;
}

.projects__section_tag {
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-page-text);
}

.projects__all_link {
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
  transition: color 160ms ease;
}

.projects__all_link:hover { color: var(--color-accent-hover); }

.project_card {
  display: flex;
  flex-direction: column;
  background: var(--color-surface-card);
  border-radius: 8px;
  padding: 16px;
}

/* 16:9. At the old fixed three-column grid a card was ~300px so 4:3 gave
   a ~300x225 thumbnail. auto-fit widened cards to ~460px, where the same
   rule produced a ~460x345 slab that swallowed the card. */
.project_card__main_image {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 6px;
  margin-bottom: 8px;
  overflow: hidden;
}

.project_card__main_image img,
.project_card__main_image svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project_card__thumbnails {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  margin-bottom: 14px;
}

.project_card__thumb {
  aspect-ratio: 1;
  border-radius: 4px;
  overflow: hidden;
}

.project_card__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }

.project_card__thumb:empty { display: none; }

.project_card__title {
  font-size: 0.875rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--color-page-text);
  margin-bottom: 8px;
}

.project_card__description {
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--color-text-muted);
  margin-bottom: 14px;
  flex: 1;
}

/* Bottom row: tags left, Read more right */
.project_card__bottom_row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: auto;
}

.project_card__tags {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
  flex: 1 1 auto;
}

.project_card__badge {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
  border: 1px solid var(--color-border-badge);
  border-radius: 4px;
  color: var(--color-text-badge);
}

.project_card__link {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: var(--color-surface-card);
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  margin-left: auto;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.project_card__link:hover {
  background: var(--color-accent);
  color: var(--color-page-background);
  border-color: var(--color-accent);
}

/* The 960px and 560px column overrides and their orphan-span hacks were
   removed: auto-fit above handles both, and the overrides would defeat it. */



/* ============================================================
   ARTICLES
============================================================ */

.articles {
  width: 100%;
  padding: 32px 20px;
  border-top: none;
  box-sizing: border-box;
}

.articles__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 20px;
}

.articles__section_tag {
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-page-text);
}

/* Already right-aligned via flex justify-content: space-between */
.articles__all_link {
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
  transition: color 160ms ease;
}

.articles__all_link:hover { color: var(--color-accent-hover); }

.article_page_preview {
  display: grid;
  grid-template-columns: 220px 1fr;
  align-items: stretch;
  border-radius: 8px;
  overflow: hidden;
  margin-bottom: 16px;
  background: var(--color-surface-card);
}

/* index.php stamps this in the same condition that emits the image, so the
   two stay in step. Without it a piece with no hero base would squeeze its
   text into the 1fr track beside an empty 220px one. */
.article_page_preview--no_media { grid-template-columns: 1fr; }

/* No border-bottom. It used to close a header holding meta, title and lede.
   index.php Rev 10 moved the lede out, so the header now holds meta and title
   only, and that line rendered as a divider between a headline and its own
   summary paragraph. Those are one unit. The footer's border-top is a real
   separator and stays. Bottom padding is 0 because the excerpt below carries
   its own. */
.article_page_preview__header {
  padding: 24px 24px 0;
}

.article_page_preview__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.article_page_preview__badge {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 8px;
  border: 1px solid var(--color-border-badge);
  border-radius: 4px;
  color: var(--color-text-badge);
}

.article_page_preview__dot {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background-color: var(--color-page-text);
  opacity: 0.30;
  flex-shrink: 0;
}

.article_page_preview__date,
.article_page_preview__read_time { font-size: 0.688rem; color: var(--color-text-label); }

/* margin-bottom was 12px, spacing the title from the lede beneath it. The
   title is now the header's last child, so that margin separated nothing and
   simply stacked onto the header padding: 36px of dead space. */
.article_page_preview__title {
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.25;
  color: var(--color-page-text);
  margin-bottom: 0;
}

/* Body copy for the featured preview. Larger than the card excerpt because
   this is the lead article, and padded to match the header, which carries its
   own horizontal padding. */
.article_page_preview__excerpt {
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--color-page-text);
  padding: 12px 24px 18px;
  margin: 0;
}

.article_page_preview__footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 24px;
  border-top: 1px dashed var(--color-border-separator);
}

.article_page_preview__tags { display: flex; gap: 6px; flex-wrap: wrap; }

/* Already right-aligned via flex justify-content: space-between */
.article_page_preview__read_full {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: var(--color-surface-card);
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.article_page_preview__read_full:hover {
  background: var(--color-accent);
  color: var(--color-page-background);
  border-color: var(--color-accent);
}

/* Article card imagery. Article cards shipped text-only while project cards
   carried an image slot, so the homepage read as two unrelated card families.
   The image sits beside the text rather than above it, matching
   .featured__block. Sources resolve through resolve_card_image_source(); a
   missing file returns null, the template omits the slot, and the --no_media
   modifier collapses the grid to one column so nothing is left holding an
   empty image track. */
.article_page_preview__main_image {
  overflow: hidden;
}

.article_page_preview__main_image img,
.article_page_preview__main_image svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.article_page_preview__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.article_card_row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* Scoped to the homepage row. The /articles listing uses the same
   .article_card class in a narrower grid, where a side-by-side split leaves
   too little measure for the excerpt, so it keeps the stacked layout.

   The card mirrors .article_page_preview: padding moves off the card and onto
   the body so the image runs flush to the top, bottom and left edges, and the
   image track stretches to the card's full height. An earlier pass used
   align-items: start with a fixed 16/9 ratio, which left the thumbnail short
   and floating against a tall block of empty card beneath it. */
.article_card_row .article_card {
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 0;
  align-items: stretch;
  padding: 0;
  overflow: hidden;
}

.article_card_row .article_card--no_media { grid-template-columns: 1fr; }

.article_card_row .article_card__main_image {
  aspect-ratio: auto;
  height: 100%;
  border-radius: 0;
  margin-bottom: 0;
}

.article_card_row .article_card__body { padding: 20px; }

.article_card__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  height: 100%;
}

/* Base slot, used stacked above the text on the /articles listing. */
.article_card__main_image {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 6px;
  margin-bottom: 12px;
  overflow: hidden;
}

.article_card__main_image img,
.article_card__main_image svg {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}


.article_card {
  display: flex;
  flex-direction: column;
  border-radius: 8px;
  padding: 20px;
  background: var(--color-surface-card);
}

.article_card__meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.article_card__date { font-size: 0.688rem; color: var(--color-text-label); }

.article_card__title {
  font-size: 0.875rem;
  font-weight: 700;
  line-height: 1.3;
  color: var(--color-page-text);
  margin-bottom: 8px;
}

.article_card__excerpt {
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--color-text-muted);
  margin-bottom: 14px;
  flex: 1;
}

/* Bottom row: tech tags left, Read more right */
.article_card__bottom_row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.article_card__tags {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
}

.article_card__link {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border: 1px solid var(--color-border-strong);
  border-radius: 5px;
  background: var(--color-surface-card);
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.article_card__link:hover {
  background: var(--color-accent);
  color: var(--color-page-background);
  border-color: var(--color-accent);
}

/* Below this the side-by-side split leaves no usable measure, so both card
   families return to image above text. The flush-edge treatment stays: only
   the track count and the image ratio change. */
@media (max-width: 700px) {
  .article_page_preview { grid-template-columns: 1fr; }
  .article_page_preview__main_image { aspect-ratio: 2 / 1; height: auto; }
  .article_card_row .article_card { grid-template-columns: 1fr; }
  .article_card_row .article_card__main_image { aspect-ratio: 2 / 1; height: auto; }
}

@media (max-width: 600px) {
  .article_card_row { grid-template-columns: 1fr; }
}

/* ============================================================
   ABOUT
============================================================ */

.about {
  width: 100%;
  padding: 32px 20px;
  border-top: none;
  box-sizing: border-box;
}

.about__section_tag {
  display: block;
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-page-text);
  margin-bottom: 16px;
}

.about__copy { font-size: var(--text-body); line-height: 1.75; color: var(--color-text-muted); }
.about__copy p { margin-bottom: 12px; }
.about__copy p:last-child { margin-bottom: 0; }
/* Anchor rule ADDITION (Rev 2): the about copy previously had no link style,
   so anchors fell back to browser defaults outside the theme system. Same
   token pair as .content_body a and .support_page__copy a, already verified
   by verify_contrast_ratios.py; no new contrast run required. */
.about__copy a { color: var(--color-accent); text-decoration: underline; }
.about__copy a:hover { color: var(--color-accent-hover); }

/* Footer row: social links left, byline card right */
.about__footer_row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  margin-top: 20px;
}

.about__social_links {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-shrink: 0;
}

.about__social_link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  color: var(--color-text-nav);
  text-decoration: none;
  transition: color 160ms ease;
}

.about__social_link:hover { color: var(--color-text-nav-active); }

.about__byline_card {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  border-radius: 8px;
  padding: 12px;
  background: var(--color-surface-card);
  text-align: right;
  flex: 1;
}

.about__byline_name { font-size: 0.8125rem; font-weight: 700; color: var(--color-page-text); }

.about__byline_title {
  font-size: 0.688rem;
  color: var(--color-text-label);
  margin-top: 2px;
}

/* ============================================================
   CONTENT FILTER
   ADDITION required by Contracts 2 and 3: listing pages carry
   tag filter buttons in priority order. PHP emits data-tags on
   cards; main.js reads dataset.tags (Contract 3). Visuals are
   composed from existing selector and badge tokens.
============================================================ */

.content_filter {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 24px;
}

.content_filter__label {
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text-label);
  margin-right: 4px;
}

.content_filter__button {
  padding: 5px 12px;
  border: 1px solid var(--color-border-badge);
  border-radius: 5px;
  background: transparent;
  font-family: inherit;
  font-size: 0.688rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-badge);
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease;
}

.content_filter__button:hover { color: var(--color-page-text); border-color: var(--color-border-strong); }

.content_filter__button[aria-pressed="true"] {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-page-background);
}

/* ============================================================
   TERMINAL COMPONENT
   Per Section 9.4: plain bash path line with a hairline rule,
   no window chrome, no macOS traffic lights. Tokens are per
   theme and switch with mode changes; the transition rule at
   the top of this file animates them. Prism token colors are
   mapped in assets/css/prism.css.
   Markup convention:
     <div class="terminal">
       <div class="terminal__path_line">user@host:~/path$</div>
       <pre><code class="language-bash">...</code></pre>
     </div>
============================================================ */

.terminal {
  background: var(--terminal-background);
  border: 1px solid var(--terminal-border);
  border-radius: 6px;
  overflow: hidden;
  margin: 20px 0;
}

.terminal__path_line {
  padding: 8px 14px;
  background: var(--terminal-title-bar);
  border-bottom: 1px solid var(--terminal-border);
  font-family: ui-monospace, "Cascadia Code", "Consolas", "DejaVu Sans Mono", monospace;
  font-size: 0.75rem;
  color: var(--terminal-token-dim);
}

.terminal pre {
  margin: 0;
  padding: 14px;
  overflow-x: auto;
}

.terminal pre code {
  font-family: ui-monospace, "Cascadia Code", "Consolas", "DejaVu Sans Mono", monospace;
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--terminal-token-white);
  background: transparent;
}

/* ============================================================
   CONTENT BODY TYPOGRAPHY
   ADDITION: shared typography for trusted body HTML on article
   and project detail pages. Composed from existing tokens.
============================================================ */

.content_body { font-size: var(--text-body); line-height: 1.75; color: var(--color-page-text); }

.content_body h2 {
  font-size: 1.125rem;
  font-weight: 700;
  margin: 32px 0 12px;
  color: var(--color-page-text);
}

.content_body h3 {
  font-size: 0.9375rem;
  font-weight: 700;
  margin: 24px 0 10px;
  color: var(--color-page-text);
}

.content_body p { margin-bottom: 16px; }

.content_body ul,
.content_body ol { margin: 0 0 16px 24px; }

.content_body li { margin-bottom: 6px; }

.content_body a { color: var(--color-accent); text-decoration: underline; }

.content_body a:hover { color: var(--color-accent-hover); }

.content_body code {
  font-family: ui-monospace, "Cascadia Code", "Consolas", "DejaVu Sans Mono", monospace;
  font-size: 0.85em;
  padding: 1px 5px;
  border: 1px solid var(--color-border-badge);
  border-radius: 4px;
  color: var(--color-text-muted);
}

.content_body pre code {
  padding: 0;
  border: none;
  color: var(--terminal-token-white);
}

/* ============================================================
   FOOTER
   Three zones:
     1. Upper row  -- logo mark | social icons
     2. Location   -- the full location string, full-width
     3. Bottom bar -- all nav + legal links flat, copyright right
============================================================ */

.footer {
  width: 100%;
  border-top: none;
  box-sizing: border-box;
}

/* Zone 1: upper row */
.footer__upper {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 40px 20px 32px;
}

/* The mark replaces a 6rem letter R that sat at 0.04 opacity. At that value it
   read as a rendering fault rather than a watermark, so this is a visible
   brand mark instead. The SVG is inlined by footer.php and fills with
   currentColor, so colour comes from the token below and follows the theme.
   Height is left to the aspect ratio: the artwork is roughly 3.5:1 and a fixed
   height would distort it. */
.footer__logo_mark {
  color: var(--color-page-text);
  opacity: 0.55;
  line-height: 0;
  user-select: none;
  flex-shrink: 0;
}

.footer__logo_mark svg {
  display: block;
  width: 200px;
  height: auto;
}

@media (max-width: 700px) {
  .footer__logo_mark svg { width: 148px; }
}

/* Social icons */
.footer__social_column {
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
}

.footer__social_links { display: flex; gap: 12px; }

.footer__social_link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  color: var(--color-text-nav);
  text-decoration: none;
  transition: color 160ms ease;
}

.footer__social_link:hover { color: var(--color-text-nav-active); }

/* Zone 2: location strip */
.footer__location {
  padding: 20px 20px 24px;
  border-top: 1px dashed var(--color-border-separator);
}

.footer__location_label {
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-label);
  margin-right: 6px;
}

.footer__location_text {
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--color-text-muted);
}

/* Zone 3: bottom bar */
.footer__bottom_bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 20px;
  border-top: 1px dashed var(--color-border-separator);
}

.footer__bottom_links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
}

.footer__navigation_link,
.footer__legal_link {
  font-size: 0.8125rem;
  color: var(--color-text-nav);
  text-decoration: none;
  white-space: nowrap;
  transition: color 160ms ease;
}

.footer__navigation_link:hover,
.footer__legal_link:hover { color: var(--color-text-nav-active); }

.footer__copyright {
  font-size: 0.688rem;
  color: var(--color-text-label);
  white-space: nowrap;
}

/*
  Recorded and closed: grid-template-columns was set on .footer__upper
  inside this breakpoint, but .footer__upper is a flex container, so
  the rule was inert and the intended one-column collapse never
  happened. The flex-direction change below produces the
  documented behavior. Recorded here so the inert rule is not
  restored as a fix.
*/
@media (max-width: 700px) {
  .footer__upper { flex-direction: column; gap: 24px; }
  .footer__social_column { justify-content: flex-start; }
}
