:root {
  color-scheme: light dark;
  --bg: #F0EBE3; --surface: #E8E2D8; --surface-2: #EDE8DF;
  --card-bg: #F5F0E8; --card-bg-hover: #FAF6EE; --card-bg-active: #FFFCF3;
  --text-primary: #1A1612; --text-secondary: #5C4F3D; --text-muted: #8C7E6D; --text-dim: #B5AC9E;
  --accent-orange: #D97706; --accent-orange-hover: #B45309;
  --accent-orange-bg: rgba(217,119,6,0.10); --accent-orange-glow: 0 4px 14px rgba(217,119,6,0.22);
  --accent-cyan: #0891B2; --accent-cyan-soft: rgba(8,145,178,0.08); --accent-cyan-edge: rgba(8,145,178,0.20);
  --accent-emerald: #16A34A; --accent-emerald-bg: rgba(22,163,74,0.10);
  --accent-emerald-edge: rgba(22,163,74,0.30); --accent-emerald-glow: 0 0 10px rgba(22,163,74,0.20);
  --accent-violet: #7C5BBF; --accent-violet-bg: rgba(124,91,191,0.10); --accent-violet-edge: rgba(124,91,191,0.25);
  --accent-amber: #C27D0E; --accent-amber-bg: rgba(194,125,14,0.12); --accent-amber-edge: rgba(194,125,14,0.30);
  --accent-rose: #D43450;
  --accent-slate: #4A6FA5; --accent-slate-bg: rgba(74,111,165,0.10); --accent-slate-edge: rgba(74,111,165,0.30); /* experience — the timeline/app identity color (orange is chrome, never a category) */
  /* Runtime-renderer tokens (design-tokens.md §3b). data.js carries semantic values
     (level / category / confidence.band); portfolio.js resolves them to these var()s
     so zero hex leaks into data.js / portfolio.js. */
  /* 4 skill-tier tokens — color the 4-bar skill-level visual; seg color = LEVEL tier,
     never category. Alias accents so light/dark/print already track. */
  --skill-expert: var(--accent-cyan); --skill-advanced: var(--accent-emerald); --skill-proficient: var(--accent-amber); --skill-beginner: var(--accent-violet);
  /* 8 skill-category tokens — drive --cat-color (ledge + name only, never segs).
     css-contract §13 cat palette; dark/print overrides below match the accent pattern. */
  --cat-programming: var(--accent-cyan); --cat-tools: var(--accent-orange); --cat-languages: var(--accent-violet); --cat-methods: var(--accent-emerald);
  --cat-analytical: var(--accent-slate); --cat-design: #D43450; --cat-domain: var(--accent-amber); --cat-interpersonal: #6366F1;
  /* 3 integrity tokens — per-section confidence bars via --conf-color. Alias accents. */
  --integrity-high: var(--accent-emerald); --integrity-mid: var(--accent-amber); --integrity-low: var(--accent-rose);
  /* App colour code (founder-set) — per-section accent on the nav tiles; an app
     with no code falls back to --app-default (dark gray). overview + social = brand orange. */
  --app-overview: var(--accent-orange); --app-social: var(--accent-orange); --app-experience: #2563EB; --app-skills: #9F1239; --app-education: #0D9488; --app-certifications: #C026D3; --app-projects: #6B7280; --app-default: #6B7280;
  /* Platform brand marks — the ONE place platform hex is allowed to live.
     portfolio.js's social brand registry reads these as var(--brand-<key>), so
     the renderer and data.js stay hex-free (design-tokens.md §3b). Brand hues
     are identity, not theme: they are deliberately NOT re-mapped in dark. */
  --brand-youtube: #FF0000; --brand-vimeo: #1AB7EA; --brand-spotify: #1DB954;
  --brand-soundcloud: #FF5500; --brand-applemusic: #FA243C; --brand-figma: #F24E1E;
  --brand-codepen: #0EA5E9; --brand-loom: #625DF5; --brand-bluesky: #0085FF;
  --brand-linkedin: #0A66C2; --brand-substack: #FF6719; --brand-flickr: #0063DC;
  --brand-tiktok: #EE1D52; --brand-instagram: #E1306C; --brand-x: #0F1419;
  --brand-threads: #000000; --brand-pinterest: #BD081C; --brand-dribbble: #EA4C89;
  --brand-behance: #1769FF; --brand-medium: #12100E; --brand-gist: #181717;
  --brand-link: #D97706;
  --border-default: rgba(31,22,14,0.10); --border-subtle: rgba(31,22,14,0.05); --border-hover: rgba(31,22,14,0.20);
  --radius-card: 8px; --radius-panel: 10px; --radius-button: 6px; --radius-pill: 9999px; --radius-tight: 3px;
  --shadow-sm: 0 1px 2px rgba(31,22,14,0.04); --shadow-md: 0 2px 8px rgba(31,22,14,0.06);
  --grid-line: rgba(31,22,14,0.04); --scan-line: rgba(31,22,14,0.02);
  --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "JetBrains Mono", "SF Mono", Consolas, monospace;
}
[data-theme="dark"] {
  --bg: #1A1612; --surface: #221D17; --surface-2: #2A241D;
  --card-bg: #1F1A14; --card-bg-hover: #261F18; --card-bg-active: #2D261D;
  --text-primary: #F0E7D5; --text-secondary: #B5A78F; --text-muted: #8C7E6D; --text-dim: #5C4F3D;
  --accent-orange: #FB923C; --accent-orange-hover: #FDBA74;
  --accent-orange-bg: rgba(251,146,60,0.14); --accent-orange-glow: 0 4px 14px rgba(251,146,60,0.30);
  --accent-cyan: #22D3EE; --accent-cyan-soft: rgba(34,211,238,0.10); --accent-cyan-edge: rgba(34,211,238,0.25);
  --accent-emerald: #34D572; --accent-emerald-bg: rgba(52,213,114,0.14);
  --accent-emerald-edge: rgba(52,213,114,0.30); --accent-emerald-glow: 0 0 12px rgba(52,213,114,0.25);
  --accent-violet: #A78BFA; --accent-violet-bg: rgba(167,139,250,0.12); --accent-violet-edge: rgba(167,139,250,0.30);
  --accent-amber: #F59E0B; --accent-amber-bg: rgba(245,158,11,0.14); --accent-amber-edge: rgba(245,158,11,0.30);
  --accent-rose: #F43F5E;
  --accent-slate: #7FA3D7; --accent-slate-bg: rgba(127,163,215,0.14); --accent-slate-edge: rgba(127,163,215,0.30);
  /* cat tokens that alias accents track automatically; only the two non-accent hues need a dark variant. */
  --cat-design: #F472B6; --cat-interpersonal: #818CF8;
  --border-default: rgba(240,231,213,0.08); --border-subtle: rgba(240,231,213,0.04); --border-hover: rgba(240,231,213,0.20);
  --grid-line: rgba(240,231,213,0.04); --scan-line: rgba(240,231,213,0.02);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text-primary);
  line-height: 1.55;
  font-size: 14.5px;
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
  transition: background 0.3s ease, color 0.3s ease;
}
button { all: unset; cursor: pointer; box-sizing: border-box; }
img { display: block; }
@keyframes materialize { 0% { opacity: 0; transform: translateY(6px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes pulse-dot { 0%,100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.55; transform: scale(0.85); } }
@keyframes scale-bar { from { transform: scaleX(0); transform-origin: left; } to { transform: scaleX(1); transform-origin: left; } }
.materialize { animation: materialize 0.4s ease-out forwards; }
.stagger-1 { animation-delay: 0.04s; opacity: 0; animation-fill-mode: forwards; }
.stagger-2 { animation-delay: 0.08s; opacity: 0; animation-fill-mode: forwards; }
.stagger-3 { animation-delay: 0.12s; opacity: 0; animation-fill-mode: forwards; }

.wrap { max-width: 880px; margin: 0 auto; padding: 24px 24px 80px; }
.material-symbols-rounded { font-variation-settings: 'opsz' 20, 'wght' 500, 'FILL' 0, 'GRAD' 0; user-select: none; vertical-align: middle; }

/* z-index 950: the materialize animation gives .topbar its own stacking context,
   so the share menu's z-index is trapped inside it — without this, later siblings
   (identity card, section panes) paint OVER the open menu. 950 beats content (0-2)
   and stays under the export modal (1000). */
.topbar { display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-bottom: 14px; position: relative; z-index: 950; }
.theme-toggle { width: 32px; height: 32px; border-radius: var(--radius-pill); background: var(--card-bg); border: 1px solid var(--border-default); color: var(--text-secondary); display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
.theme-toggle:hover { border-color: var(--accent-orange); color: var(--accent-orange); transform: translateY(-1px); }
.theme-toggle svg { width: 14px; height: 14px; }
.theme-toggle .icon-moon { display: none; }
[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
[data-theme="dark"] .theme-toggle .icon-moon { display: block; }
.action-btn { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px; border-radius: var(--radius-pill); background: var(--card-bg); border: 1px solid var(--border-default); color: var(--text-secondary); font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; transition: all 0.2s; }
.action-btn:hover { border-color: var(--accent-orange); color: var(--accent-orange); transform: translateY(-1px); }
.action-btn svg { width: 13px; height: 13px; }
.action-btn.copied { border-color: var(--accent-emerald); color: var(--accent-emerald); }
/* ─── Print / Save-as-PDF ──────────────────────────────────────────────
   One click → a clean PDF, no fiddling. We (1) force colors so the cream/orange
   design survives, (2) expand every collapsed pane + card so the whole
   portfolio prints (not just the open tab), (3) keep cards intact across page
   breaks, and (4) drop the interactive chrome (topbar, chevrons). */
@page { size: Letter; margin: 0.4in 0; }
@media print {
  :root { color-scheme: light; }
  html, body { background: #fff !important; }
  *, *::before, *::after { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  body { font-size: 11px; }
  .wrap { max-width: none; padding: 0; }
  .topbar { display: none !important; }
  /* Animations would otherwise leave staggered items at opacity:0 on the print canvas. */
  .materialize, .stagger-1, .stagger-2, .stagger-3 { animation: none !important; opacity: 1 !important; }
  /* Print every section, not just the active tab. */
  .section-grid { display: none !important; }
  .section-pane { display: block !important; }
  .section-pane[data-pane]::before {
    content: attr(data-pane);
    display: block;
    font-family: var(--font-mono); font-size: 10px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.16em;
    color: var(--accent-orange);
    margin: 14px 0 8px;
  }
  /* Pane labels carry the app identity colors in print too. */
  .section-pane[data-pane="experience"]::before { color: var(--accent-slate); }
  .section-pane[data-pane="projects"]::before { color: var(--accent-cyan); }
  .section-pane[data-pane="education"]::before { color: var(--accent-violet); }
  .section-pane[data-pane="certifications"]::before { color: var(--accent-amber); }
  .section-pane:first-of-type::before { margin-top: 4px; }
  /* Expand all collapsed cards and hide the now-meaningless chevron. */
  .item-body { max-height: none !important; overflow: visible !important; }
  .chevron { display: none !important; }
  /* Keep SMALL units intact; let big containers (pane, card, group) break across
     pages. Keeping .section-pane/.item-card/.contrib-group together is what caused
     page-sized blank gaps — a container taller than one page can never "avoid". */
  .identity-card, .summary-band, .item-head, .contrib, .skill-cell, .edu-card {
    break-inside: avoid; page-break-inside: avoid;
  }
  /* Headers never strand at the bottom of a page — they stay with their content. */
  .section-pane[data-pane]::before, .item-head, .group-header, .skill-cat-header {
    break-after: avoid; page-break-after: avoid;
  }
  /* When a card fragments across pages, give each fragment its full border/bg. */
  .item-card, .contrib-group { box-decoration-break: clone; -webkit-box-decoration-break: clone; }
  p { orphans: 3; widows: 3; }
  .footer { break-before: avoid; page-break-before: avoid; }
  /* ── Print-weight diet — keep the brand, drop the rasterizers. ──
     Blurred glows/shadows are the no.1 PDF rasterization trigger; depth is
     carried by the existing 1px borders. (!important where the component's
     own rule sets a literal shadow later in source order.) */
  :root { --accent-orange-glow: none; --accent-emerald-glow: none; --shadow-sm: none; --shadow-md: none; }
  .live-pill, .active-pill,
  .skill-cat-header .ledge { box-shadow: none !important; }
  /* Scanlines stay (they're brand) — but as a pre-tiled 1×3 PNG with the
     light-theme --scan-line color baked in (print forces light), instead of a
     repeating-linear-gradient that becomes a viewer-hostile Type-4 PostScript
     shading. Opacity stays 0.30, same as on screen. */
  .item-card::before { background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAADCAYAAABS3WWCAAAAD0lEQVR42mNggAN5MT5WAADuAEkdWZ51AAAAAElFTkSuQmCC") repeat !important; }
  /* Grid texture: ONE 32×32 PNG tile (light --grid-line baked in) replaces the
     2-axis gradient pair — kills the content-stream fill-op explosion. */
  .identity-card::before, .summary-band::before { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAANUlEQVR42u3OoQ0AQAgEQTTiJZb+uyT5JhDMJXt6ouvlZvFvcwAAAAAAAAAAAAAAAAAA5wEDA0kS9G3fBkQAAAAASUVORK5CYII=") !important; }
  /* Accent bars + divider rules go solid in print — gradients are PDF shading
     functions, and a gradient with an alpha stop emits TWO (the color ramp
     plus a function-based alpha ramp). Zero-shading budget. */
  .item-card .accent-bar { background: var(--text-muted) !important; }
  .item-card.is-current .accent-bar { background: var(--accent-orange) !important; box-shadow: none !important; }
  .item-divider { background: var(--accent-cyan-edge) !important; }
}

/* ─── Export chooser print styles ──────────────────────────────────────
   The tuned block above IS "portfolio classic" — the default look when no
   print-doc-… / print-style-… class is on <body>, and the foundation every
   variant builds on. Everything below is scoped to the body classes that
   #export-modal sets right before window.print() (removed on afterprint):
     body.print-doc-portfolio + .print-style-classic | -ink | -showcase
     body.print-doc-resume    + .print-style-classic | -modern | -compact
   print-doc-portfolio.print-style-classic intentionally has no rules — it
   IS the foundation.
   PORTFOLIO PDF — gated for the next release; re-enable by restoring the
   Document fieldset. THIS RELEASE the modal sets the resume classes only,
   so the print-doc-portfolio variants below are inert (kept intact). The
   default no-class browser print (Cmd+P) still prints the paginated classic
   portfolio — that's the browser's native path, untouched. */
@media print {
  /* In-page chrome never prints, whichever document was chosen. */
  #export-modal, #share-menu { display: none !important; }
}

/* The two portfolio style variants below are intentionally NOT @media-print
   gated: their body classes exist only during the export lifecycle (set on
   confirm, removed on afterprint), and the Continuous layout measures the
   document height ON SCREEN — the variant's layout must be part of that
   measurement or the injected single page comes out the wrong height. A brief
   restyle flash before the print dialog opens is acceptable. */
  /* ── PORTFOLIO · INK — conservative monochrome for corporate submissions.
        White bg, near-black text, no textures/scanlines/grid, no color
        accents; rules, weight and spacing carry the hierarchy. ── */
  body.print-doc-portfolio.print-style-ink {
    --bg: #fff; --surface: #fff; --surface-2: #fff;
    --card-bg: #fff; --card-bg-hover: #fff; --card-bg-active: #fff;
    --text-primary: #111; --text-secondary: #333; --text-muted: #555; --text-dim: #888;
    --accent-orange: #111; --accent-orange-hover: #111; --accent-orange-bg: transparent; --accent-orange-glow: none;
    --accent-cyan: #111; --accent-cyan-soft: transparent; --accent-cyan-edge: #c8c8c8;
    --accent-emerald: #111; --accent-emerald-bg: transparent; --accent-emerald-edge: #c8c8c8; --accent-emerald-glow: none;
    --accent-violet: #111; --accent-violet-bg: transparent; --accent-violet-edge: #c8c8c8;
    --accent-amber: #555; --accent-amber-bg: transparent; --accent-amber-edge: #c8c8c8;
    --accent-rose: #111;
    --accent-slate: #111; --accent-slate-bg: transparent; --accent-slate-edge: #c8c8c8;
    /* skill-tier + integrity + 6 cat tokens alias accents already forced monochrome above;
       only the two non-accent cat hues need an explicit ink override (matches L225 --cat-color: #333). */
    --cat-design: #333; --cat-interpersonal: #333;
    --border-default: #cfcfcf; --border-subtle: #e4e4e4; --border-hover: #aaa;
    --shadow-sm: none; --shadow-md: none;
    --grid-line: transparent; --scan-line: transparent;
  }
  body.print-doc-portfolio.print-style-ink .identity-card::before,
  body.print-doc-portfolio.print-style-ink .summary-band::before,
  body.print-doc-portfolio.print-style-ink .item-card::before { display: none !important; }
  /* Overview panel in ink: hexes + chips are already covered by the .hex-kpi
     .hex and .skill-chip rules below — no extra rules needed. */
  body.print-doc-portfolio.print-style-ink .item-card.is-current .accent-bar { background: #111 !important; box-shadow: none; }
  body.print-doc-portfolio.print-style-ink .live-pill,
  body.print-doc-portfolio.print-style-ink .active-pill { background: transparent; border-color: #c8c8c8; color: #111; box-shadow: none; }
  body.print-doc-portfolio.print-style-ink .live-pill .dot { background: #111; animation: none; }
  body.print-doc-portfolio.print-style-ink .skill-chip { background: transparent !important; color: #111 !important; border-color: #c8c8c8 !important; }
  body.print-doc-portfolio.print-style-ink .contrib::before { background: #111; }
  body.print-doc-portfolio.print-style-ink .metric-inline { color: #333; }
  body.print-doc-portfolio.print-style-ink .contrib-impact { background: transparent !important; border-color: #c8c8c8 !important; color: #333 !important; }
  body.print-doc-portfolio.print-style-ink .contrib-impact strong.m { color: #111 !important; }
  body.print-doc-portfolio.print-style-ink .group-header .material-symbols-rounded,
  body.print-doc-portfolio.print-style-ink .group-header .title { color: #333 !important; }
  body.print-doc-portfolio.print-style-ink .group-header .bar { background: #d8d8d8 !important; }
  body.print-doc-portfolio.print-style-ink .hex-kpi .hex { background: transparent; border-color: #c8c8c8; color: #111; }
  body.print-doc-portfolio.print-style-ink .org-fallback { background: #f2f2f2; border-color: #cfcfcf; color: #111; }
  /* filter: grayscale rasterizes everything it touches in the print pipeline —
     allowed ONLY on true raster images (cost bounded to their own pixels),
     never on anything containing text or vector fills. The level-bar segs are
     inline-styled, so !important does the monochrome swap; the ledge follows
     the --cat-color override (its glow is killed above). */
  body.print-doc-portfolio.print-style-ink .org-logo,
  body.print-doc-portfolio.print-style-ink .photo-upload img { filter: grayscale(1); }
  body.print-doc-portfolio.print-style-ink .level-bar .seg { background: #555 !important; }
  body.print-doc-portfolio.print-style-ink .skill-cat-header .ledge { box-shadow: none; }
  body.print-doc-portfolio.print-style-ink .skill-cat { --cat-color: #333 !important; }
  /* Radar in ink: crisp mono outline, no tint fill; the web rings follow
     --border-default (#cfcfcf) so the chart still reads on white. */
  body.print-doc-portfolio.print-style-ink .skill-radar-poly { fill: transparent !important; stroke: #111 !important; }
  body.print-doc-portfolio.print-style-ink .skill-radar-dot { fill: #111 !important; }

  /* ── PORTFOLIO · SHOWCASE — designer-forward (Behance-inspired): bigger
        display type for the name + section openers, more whitespace, color
        kept, projects elevated to print FIRST after identity (CSS order). ── */
  body.print-doc-portfolio.print-style-showcase .wrap { display: flex; flex-direction: column; }
  body.print-doc-portfolio.print-style-showcase .wrap > * { order: 2; }
  body.print-doc-portfolio.print-style-showcase .wrap > .identity-card { order: 0; }
  /* Contents strip rides at the same order as identity — the flexbox DOM-order
     tie-break keeps it right after the identity card. */
  body.print-doc-portfolio.print-style-showcase .wrap > .print-toc { order: 0; }
  /* The Overview pane (when shipped) rides at order 0 too — the DOM tie-break
     lands it right after the contents strip, ahead of the elevated projects
     pane. When show_summary is stripped, the selector simply matches nothing. */
  body.print-doc-portfolio.print-style-showcase .wrap > .section-pane[data-pane="overview"] { order: 0; }
  body.print-doc-portfolio.print-style-showcase .wrap > .section-pane[data-pane="projects"] { order: 1; }
  body.print-doc-portfolio.print-style-showcase .wrap > .footer { order: 3; }
  body.print-doc-portfolio.print-style-showcase { font-size: 11.5px; }
  body.print-doc-portfolio.print-style-showcase h1.name { font-size: 30pt; line-height: 1.05; letter-spacing: -0.02em; }
  body.print-doc-portfolio.print-style-showcase .headline { font-size: 13pt; }
  body.print-doc-portfolio.print-style-showcase .summary { font-size: 14px; line-height: 1.7; }
  body.print-doc-portfolio.print-style-showcase .identity-card { padding: 30px 30px; margin-bottom: 26px; }
  body.print-doc-portfolio.print-style-showcase .section-pane[data-pane]::before {
    font-family: var(--font-sans); font-size: 17pt; font-weight: 800;
    text-transform: capitalize; letter-spacing: -0.01em; margin: 30px 0 14px;
  }
  body.print-doc-portfolio.print-style-showcase .section-pane:first-of-type::before { margin-top: 14px; }
  body.print-doc-portfolio.print-style-showcase .item-card { margin-bottom: 14px; }
  body.print-doc-portfolio.print-style-showcase .item-head { padding: 16px 18px 16px 21px; }
  body.print-doc-portfolio.print-style-showcase .item-body-inner { padding-bottom: 22px; gap: 16px; }

/* ── RESUME document — swap the portfolio for #resume-view. ──
     Intentionally NOT @media-print gated (same reasoning as the portfolio
     variants above): the export modal's Auto-fit measures #resume-view's
     height ON SCREEN while these body classes are set — they exist only
     during the export lifecycle — so the measured layout must BE the printed
     layout. A brief restyle flash before the print dialog opens is fine. */
  /* ── READABILITY FLOOR — recruiters reject smaller ──
        Hard constraints for every resume style and for auto-fit:
          · body text never below 10pt   (compact's base IS 10pt — it never shrinks)
          · line-height never below 1.2  (compact sits exactly at the 1.2 floor)
          · page margins never below 0.5in (@page 0.5in/0.55in is already the floor)
        Auto-fit scales type via --rfs on #resume-view (default 1), clamped in
        JS (RESUME_MIN_BODY_PT / RESUME_MIN_LEADING) to
        floor-scale = 10pt / style-base-pt:
          classic 11pt → 0.909 · modern 10.5pt → 0.952 · compact 10pt → 1.0.
        Density beyond the floor comes from spacing, margins and leading —
        never from smaller type. */
  body.print-doc-resume .topbar,
  body.print-doc-resume .identity-card,
  body.print-doc-resume .print-toc,
  body.print-doc-resume .section-grid,
  body.print-doc-resume .section-pane,
  body.print-doc-resume .footer { display: none !important; }
  /* Measurement = print geometry: the on-screen auto-fit measure must see the
     same content box the printed page lays out (the print block's .wrap reset
     only applies at print time). */
  body.print-doc-resume { background: #fff; }
  body.print-doc-resume .wrap { max-width: none; padding: 0; }
  body.print-doc-resume #resume-view { display: block !important; background: #fff; color: #111; }
  /* Shared resume bones — ATS-safe, style-independent. */
  body.print-doc-resume #resume-view h1,
  body.print-doc-resume #resume-view h2,
  body.print-doc-resume #resume-view h3,
  body.print-doc-resume #resume-view p { margin: 0; }
  body.print-doc-resume #resume-view h2 { break-after: avoid; page-break-after: avoid; }
  body.print-doc-resume .resume-entry { break-inside: avoid; page-break-inside: avoid; }
  body.print-doc-resume #resume-view li { orphans: 2; widows: 2; }
  /* Title left, dates right — flex, not a table: copies out as linear text,
     so the PDF still passes the plain-text ATS paste test. */
  body.print-doc-resume .resume-entry-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
  body.print-doc-resume .resume-dates { white-space: nowrap; }

  /* ── RESUME · CLASSIC — the timeless serif résumé: single column, black
        on white, centered name, ruled caps headings, traditional margins
        (#resume-view padding + the 0.5in/0.55in @page margin ≈ 1in).
        Font sizes ride --rfs (auto-fit scale, floor 0.909) and the family
        rides --resume-font (the dialog's Font override). ── */
  body.print-doc-resume.print-style-classic #resume-view {
    font-family: var(--resume-font, Georgia, 'Charter', 'Times New Roman', serif);
    font-size: calc(10pt * var(--rfs, 1)); line-height: 1.35; padding: 0.08in 0.55in 0.1in;
  }
  body.print-doc-resume.print-style-classic #resume-view h1 { font-size: calc(21pt * var(--rfs, 1)); font-weight: 400; text-align: center; letter-spacing: 0.04em; }
  body.print-doc-resume.print-style-classic .resume-headline { text-align: center; font-size: calc(10pt * var(--rfs, 1)); margin-top: 2pt; }
  body.print-doc-resume.print-style-classic .resume-contact { text-align: center; font-size: calc(9pt * var(--rfs, 1)); margin-top: 3pt; }
  body.print-doc-resume.print-style-classic .resume-header { margin-bottom: 16pt; }
  body.print-doc-resume.print-style-classic #resume-view h2 { font-size: calc(10pt * var(--rfs, 1)); font-weight: 700; text-transform: uppercase; letter-spacing: 0.10em; border-bottom: 0.75pt solid #111; padding-bottom: 2pt; margin: 14pt 0 6pt; }
  body.print-doc-resume.print-style-classic #resume-view h3 { font-size: calc(10pt * var(--rfs, 1)); font-weight: 700; }
  body.print-doc-resume.print-style-classic .resume-entry { margin: 0 0 9pt; }
  body.print-doc-resume.print-style-classic .resume-org,
  body.print-doc-resume.print-style-classic .resume-dates { font-size: calc(9.5pt * var(--rfs, 1)); font-style: italic; }
  body.print-doc-resume.print-style-classic #resume-view ul { margin: 3pt 0 0; padding-left: 14pt; }
  body.print-doc-resume.print-style-classic #resume-view li { margin: 0 0 2.5pt; }
  /* One bolded key sub-phrase per bullet (see #resume-view authoring comments). */
  body.print-doc-resume.print-style-classic #resume-view strong { font-weight: 700; }
  /* Links: resume-convention subtle — inherit the ink, thin underline.
     No web-blue, no color shift; hrefs still print as clickable PDF annotations. */
  body.print-doc-resume.print-style-classic #resume-view a { color: inherit; text-decoration: underline; text-decoration-thickness: 0.5pt; text-underline-offset: 2pt; }

  /* ── RESUME · MODERN — Inter, compact left-aligned header, thin rules,
        one restrained orange accent on the name + section rules only.
        Fixed #D97706 (not the token) so dark-theme printing stays correct.
        Font sizes ride --rfs (floor 0.952); family rides --resume-font. ── */
  body.print-doc-resume.print-style-modern #resume-view {
    font-family: var(--resume-font, var(--font-sans));
    font-size: calc(10.5pt * var(--rfs, 1)); line-height: 1.4; color: #1a1a1a; padding: 0.05in 0.55in 0.1in;
  }
  /* No negative letter-spacing anywhere in #resume-view — ATS-safety rule. */
  body.print-doc-resume.print-style-modern #resume-view h1 { font-size: calc(23pt * var(--rfs, 1)); font-weight: 700; color: #D97706; }
  body.print-doc-resume.print-style-modern .resume-headline { font-size: calc(11pt * var(--rfs, 1)); font-weight: 600; margin-top: 1pt; }
  body.print-doc-resume.print-style-modern .resume-contact { font-size: calc(9.5pt * var(--rfs, 1)); color: #555; margin-top: 2pt; }
  body.print-doc-resume.print-style-modern .resume-header { margin-bottom: 14pt; }
  body.print-doc-resume.print-style-modern #resume-view h2 { font-size: calc(9pt * var(--rfs, 1)); font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: #444; border-bottom: 0.5pt solid #D97706; padding-bottom: 2pt; margin: 16pt 0 6pt; }
  body.print-doc-resume.print-style-modern #resume-view h3 { font-size: calc(11pt * var(--rfs, 1)); font-weight: 600; }
  body.print-doc-resume.print-style-modern .resume-entry { margin: 0 0 9pt; }
  body.print-doc-resume.print-style-modern .resume-org,
  body.print-doc-resume.print-style-modern .resume-dates { font-size: calc(9.5pt * var(--rfs, 1)); color: #555; }
  body.print-doc-resume.print-style-modern #resume-view ul { margin: 4pt 0 0; padding-left: 13pt; }
  body.print-doc-resume.print-style-modern #resume-view li { margin: 0 0 3pt; }
  /* One bolded key sub-phrase per bullet (see #resume-view authoring comments). */
  body.print-doc-resume.print-style-modern #resume-view strong { font-weight: 700; }
  /* Links inherit the ink; the accent tints ONLY the contact line's underline —
     bullets keep a plain underline. Restrained accent, never web-blue. */
  body.print-doc-resume.print-style-modern #resume-view a { color: inherit; text-decoration: underline; text-decoration-thickness: 0.5pt; text-underline-offset: 2pt; }
  body.print-doc-resume.print-style-modern .resume-contact a { text-decoration-color: #D97706; }

  /* ── RESUME · COMPACT — dense one-pager AT the readability floor: body is
        exactly 10pt (never smaller — its --rfs floor scale is 1.0, so
        auto-fit cannot shrink it), leading exactly 1.2, the 0.5in/0.55in
        @page margin IS the margin floor (no extra padding). Density is bought
        with tight spacing and margins only — never with sub-10pt type. ── */
  body.print-doc-resume.print-style-compact #resume-view {
    font-family: var(--resume-font, var(--font-sans));
    font-size: calc(10pt * var(--rfs, 1)); line-height: 1.2; padding: 0;
  }
  body.print-doc-resume.print-style-compact #resume-view h1 { font-size: calc(16pt * var(--rfs, 1)); font-weight: 700; }
  body.print-doc-resume.print-style-compact .resume-headline { font-size: calc(10.5pt * var(--rfs, 1)); font-weight: 600; margin-top: 1pt; }
  body.print-doc-resume.print-style-compact .resume-contact { font-size: calc(10pt * var(--rfs, 1)); color: #444; margin-top: 1pt; }
  body.print-doc-resume.print-style-compact .resume-header { margin-bottom: 8pt; }
  body.print-doc-resume.print-style-compact #resume-view h2 { font-size: calc(10pt * var(--rfs, 1)); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 0.5pt solid #999; padding-bottom: 1pt; margin: 7pt 0 2.5pt; }
  body.print-doc-resume.print-style-compact #resume-view h3 { font-size: calc(10pt * var(--rfs, 1)); font-weight: 700; }
  body.print-doc-resume.print-style-compact .resume-entry { margin: 0 0 4.5pt; }
  body.print-doc-resume.print-style-compact .resume-org,
  body.print-doc-resume.print-style-compact .resume-dates { font-size: calc(10pt * var(--rfs, 1)); color: #444; }
  body.print-doc-resume.print-style-compact #resume-view ul { margin: 1pt 0 0; padding-left: 11pt; }
  body.print-doc-resume.print-style-compact #resume-view li { margin: 0 0 1pt; }
  /* One bolded key sub-phrase per bullet (see #resume-view authoring comments).
     700 holds at 10pt; drop to 600 ONLY if it muddies in the printed PDF — judge by eye. */
  body.print-doc-resume.print-style-compact #resume-view strong { font-weight: 700; }
  /* Links: inherit the ink + thin underline. No color shift at 10pt density. */
  body.print-doc-resume.print-style-compact #resume-view a { color: inherit; text-decoration: underline; text-decoration-thickness: 0.5pt; text-underline-offset: 1.5pt; }

.identity-card { position: relative; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-panel); padding: 22px 24px; margin-bottom: 14px; box-shadow: var(--shadow-sm); overflow: hidden; }
.identity-card::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.55; background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); background-size: 32px 32px; z-index: 0; }
.identity-card > * { position: relative; z-index: 1; }
.identity-row { display: flex; align-items: flex-start; gap: 18px; }
.photo-upload { position: relative; width: 104px; height: 104px; border-radius: var(--radius-button); background: var(--surface-2); border: 1.5px dashed var(--border-hover); display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; overflow: hidden; transition: all 0.2s; color: var(--text-muted); font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.10em; text-transform: uppercase; }
.photo-upload:hover { border-color: var(--accent-orange); color: var(--accent-orange); background: var(--accent-orange-bg); }
.photo-upload .material-symbols-rounded { font-size: 26px; margin-bottom: 2px; }
.photo-upload.has-photo { border-style: solid; border-color: var(--accent-emerald); padding: 0; background: transparent; }
.photo-upload.has-photo .upload-prompt { display: none; }
/* The baked headshot is ~480px source → object-fit:cover downscale stays crisp at 104px (and on hi-DPI). */
.photo-upload img { width: 100%; height: 100%; object-fit: cover; display: none; }
.photo-upload.has-photo img { display: block; }
.photo-upload input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.photo-remove { position: absolute; top: -4px; right: -4px; width: 18px; height: 18px; border-radius: 50%; background: var(--accent-rose); border: 2px solid var(--card-bg); display: none; align-items: center; justify-content: center; color: white; cursor: pointer; z-index: 2; }
.photo-remove .material-symbols-rounded { font-size: 11px; }
.photo-upload.has-photo:hover .photo-remove { display: flex; }

/* ─── PUBLISHED-MODE CONTRACT — every editing affordance AND builder
   diagnostic gates on html[data-hope-mode=published] ─────────────────────
   A published copy carries data-hope-mode="published" on the <html> element,
   stamped by the publish skill into the STAGED site/index.html only. The
   generated/local file NEVER carries it — it stays the owner's editable copy.
   Under the flag a visitor sees a finished site, not an editor or a draft:
     · photo upload fully inert — pointer-events none (which also kills the
       :hover ring — hover can't engage on a pointer-events:none element),
       upload prompt hidden, remove × never shown, and with no baked photo
       the box hides entirely rather than invite an upload;
     · the matching JS gate (document.documentElement.dataset.hopeMode ===
       'published') disables the file input and SKIPS the photo localStorage
       read/write, so a visitor's stale localStorage never shadows the
       owner's baked photo;
     · the section-grid INTEGRITY bars (the % on each app tile) are hidden —
       that percentage is a data-confidence diagnostic for the OWNER ("this
       section is thin"), never a recruiter-facing grade. It already never
       prints (the grid is hidden in all print modes); this keeps it off the
       live site too.
   Visitor FEATURES are deliberately NOT gated: theme toggle (its
   localStorage persistence is a per-visitor preference), Share menu,
   Save-as-PDF, section-grid navigation, card expansion.
   ANY FUTURE editable or builder-facing diagnostic added to this template
   MUST ride the same flag — CSS scoped under html[data-hope-mode="published"],
   JS behind the same dataset.hopeMode check. */
html[data-hope-mode="published"] .photo-upload { pointer-events: none; cursor: default; }
html[data-hope-mode="published"] .photo-upload .upload-prompt { display: none; }
html[data-hope-mode="published"] .photo-upload:not(.has-photo) { display: none; }
html[data-hope-mode="published"] .photo-remove { display: none !important; }
html[data-hope-mode="published"] .section-btn .integrity { display: none; }

.identity-info { flex: 1; min-width: 0; }
.identity-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.identity-name-block { flex: 1; min-width: 0; }
h1.name { margin: 0 0 2px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); line-height: 1.15; }
.headline { margin: 0; color: var(--accent-cyan); font-size: 13.5px; font-weight: 600; letter-spacing: -0.005em; }
.live-pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 9px; background: var(--accent-emerald-bg); border: 1px solid var(--accent-emerald-edge); border-radius: var(--radius-pill); color: var(--accent-emerald); font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; flex-shrink: 0; box-shadow: var(--accent-emerald-glow); }
.live-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-emerald); animation: pulse-dot 2s ease-in-out infinite; }
.stats-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; color: var(--text-secondary); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.02em; }
.stats-row strong { color: var(--text-primary); font-weight: 700; }
.stats-row .sep { color: var(--text-dim); margin: 0 4px; }
/* Contact row — uniform pills (icon + text), one per detail; the pill + icon
   signal a clickable detail, so the external-link arrow is dropped as clutter. */
.contact-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.contact-row .item { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-pill); font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.01em; color: var(--text-secondary); text-decoration: none; transition: border-color 0.15s, background 0.15s, color 0.15s; }
.contact-row a.item:hover { border-color: var(--border-hover); background: var(--card-bg-hover); color: var(--text-primary); }
.contact-row .item .material-symbols-rounded { font-size: 14px; color: var(--text-muted); }
.contact-row .item svg { width: 13px; height: 13px; flex-shrink: 0; color: var(--text-muted); }
.contact-row .item .ext { display: none; }
.summary { margin: 12px 0 0; color: var(--text-secondary); font-size: 13.5px; line-height: 1.65; max-width: 640px; }

/* ─── Overview panel (opt-in) — curated hero stats + interests ─────────
   The inner panel of the Overview app (#pane-overview > .summary-band).
   Identity-card family: same panel tokens + 32px grid texture, no new colors.
   Rendered only when the user opted in (CuratedPortfolio.show_summary); the
   portfolio skill strips the Overview tile + pane conditionals otherwise, so
   these rules are inert on no-summary pages. */
.summary-band { position: relative; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-panel); padding: 16px 24px 18px; margin-bottom: 14px; box-shadow: var(--shadow-sm); overflow: hidden; }
.summary-band::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.55; background-image: linear-gradient(var(--grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); background-size: 32px 32px; z-index: 0; }
.summary-band > * { position: relative; z-index: 1; }
/* Impact rides in its own card now. Hide a band that ends up empty — no KPIs, or
   no visible feed strip + no interests — so a sparse portfolio never shows a
   blank panel. :has() is progressively enhanced; unsupported browsers keep the
   (rare) empty card rather than break. */
.summary-band.metrics-band:not(:has(.summary-stat)) { display: none; }
.summary-band.feed-band:not(:has(.overview-strip:not([hidden]))):not(:has(.summary-interests)) { display: none; }
.summary-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 28px; align-items: start; }
/* "Impact" heading spans the full first flex line so the KPIs wrap beneath it
   (style inherited from .overview-strip-title — same as Latest from / Highlights). */
.summary-stats .summary-stats-title { grid-column: 1 / -1; margin: 0 0 2px; }
/* Scaled-up hex KPI: same clip-path/orange tint via the .hex-kpi classes, ~2×
   size. Scoped selectors out-rank the later .hex-kpi base rules (specificity),
   and ink's monochrome .hex-kpi overrides keep covering these for free. */
.summary-band .hex-kpi { gap: 12px; }
.summary-band .hex-kpi .hex { width: 44px; height: 44px; border-radius: 6px; flex-shrink: 0; }
.summary-band .hex-kpi .hex .material-symbols-rounded { font-size: 22px; }
.summary-stat .stat-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.summary-stat .stat-value { font-size: 21px; font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); line-height: 1.1; }
.summary-stat .stat-label { font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; letter-spacing: 0.10em; color: var(--text-muted); }
.summary-interests { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 16px; }
.summary-interests .interests-eyebrow { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--text-muted); margin-right: 6px; }
/* Neutral chips — skill-chip bones, no category colors. */
.summary-interests .skill-chip { background: var(--surface-2); color: var(--text-secondary); border-color: var(--border-default); }

.section-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-bottom: 16px; }
.section-btn { background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-button); padding: 9px 12px; display: flex; align-items: center; gap: 11px; text-align: left; font-family: inherit; box-shadow: var(--shadow-sm); transition: all 0.2s ease; }
/* App identity colors — the timeline's type palette threads into each app's
   tile and content borders (color = navigation: the slate hex on the
   timeline leads to the slate-edged app). Fill stays BRAND orange on the
   active tile; identity lives in borders only. Orange is chrome, never a
   category. Overview and Skills aren't timeline types — they stay neutral. */
/* App colour code — each tile's accent (3px left border + icon); an app with no
   code falls back to --app (dark gray). overview + social = brand orange. */
.section-btn { --app: var(--app-default); border-left: 3px solid var(--app); }
.section-btn[data-section="overview"] { --app: var(--app-overview); }
.section-btn[data-section="experience"] { --app: var(--app-experience); }
.section-btn[data-section="skills"] { --app: var(--app-skills); }
.section-btn[data-section="education"] { --app: var(--app-education); }
.section-btn[data-section="certifications"] { --app: var(--app-certifications); }
.section-btn[data-section="projects"] { --app: var(--app-projects); }
.section-btn[data-section="social"] { --app: var(--app-social); }
.section-btn:hover { background: var(--card-bg-hover); border-color: var(--border-hover); transform: translateY(-1px); }
.section-btn.active { background: var(--accent-orange); border-color: var(--accent-orange); box-shadow: var(--accent-orange-glow); }
.section-btn .icon { width: 28px; height: 28px; border-radius: var(--radius-button); background: var(--surface-2); background: color-mix(in srgb, var(--app) 14%, transparent); display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--app); transition: all 0.2s; }
.section-btn.active .icon { background: rgba(255,255,255,0.22); color: white; }
.section-btn .icon .material-symbols-rounded { font-size: 18px; }
.section-btn .body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.section-btn .label { font-size: 13px; font-weight: 700; letter-spacing: -0.005em; color: var(--text-primary); }
.section-btn.active .label { color: white; }
.section-btn .meta { display: flex; align-items: center; gap: 8px; }
.section-btn .count { font-family: var(--font-mono); font-size: 10px; color: var(--text-muted); letter-spacing: 0.04em; flex-shrink: 0; }
.section-btn.active .count { color: rgba(255,255,255,0.78); }
.section-btn .integrity { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.section-btn .bar-track { width: 28px; height: 3px; border-radius: 2px; background: rgba(31,22,14,0.08); overflow: hidden; }
[data-theme="dark"] .section-btn .bar-track { background: rgba(240,231,213,0.10); }
.section-btn.active .bar-track { background: rgba(255,255,255,0.25); }
.section-btn .bar-fill { height: 100%; border-radius: 2px; background: var(--conf-color, var(--accent-emerald)); transition: width 0.5s cubic-bezier(0.4,0,0.2,1); }
.section-btn.active .bar-fill { background: white; }
.section-btn .pct { font-family: var(--font-mono); font-size: 10px; font-weight: 700; color: var(--conf-color, var(--accent-emerald)); }
.section-btn.active .pct { color: white; }

.section-pane { display: none; }
.section-pane.active { display: block; animation: materialize 0.3s ease-out; }

.item-card { position: relative; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); margin-bottom: 10px; box-shadow: var(--shadow-sm); overflow: hidden; transition: background 0.2s, border-color 0.2s, box-shadow 0.2s; }
.item-card::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.30; background: repeating-linear-gradient(0deg, transparent, transparent 2px, var(--scan-line) 2px, var(--scan-line) 4px); z-index: 0; }
.item-card > * { position: relative; z-index: 1; }
.item-card:hover { background: var(--card-bg-hover); border-color: var(--border-hover); }
.item-card.expanded { background: var(--card-bg-active); box-shadow: var(--shadow-md); }
.item-card .accent-bar { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: linear-gradient(180deg, var(--text-muted), transparent); z-index: 2; }
/* The thread, inside the content: pane-scoped accent bars + card edges take
   the app's identity color. is-current stays brand orange (re-asserted at
   pane specificity below). */
.section-pane[data-pane="experience"] .item-card .accent-bar { background: linear-gradient(180deg, var(--accent-slate), transparent); }
.section-pane[data-pane="projects"] .item-card .accent-bar { background: linear-gradient(180deg, var(--accent-cyan), transparent); }
.section-pane[data-pane] .item-card.is-current .accent-bar { background: var(--accent-orange); }
.section-pane[data-pane="education"] .edu-card { border-left: 3px solid var(--accent-violet); }
.section-pane[data-pane="certifications"] .edu-card { border-left: 3px solid var(--accent-amber); }
.item-card.is-current .accent-bar { background: linear-gradient(180deg, var(--accent-orange), var(--accent-orange) 60%, transparent); box-shadow: 0 0 12px rgba(217,119,6,0.30); }
.item-head { display: flex; align-items: center; gap: 14px; padding: 14px 16px 14px 19px; cursor: pointer; user-select: none; }
.org-logo { flex-shrink: 0; width: 44px; height: 44px; border-radius: var(--radius-button); background: var(--surface-2); border: 1px solid var(--border-default); padding: 4px; object-fit: contain; }
.org-fallback { flex-shrink: 0; width: 44px; height: 44px; border-radius: var(--radius-button); background: #E8DDD0; border: 1px solid #D4C5B3; display: flex; align-items: center; justify-content: center; font-family: 'Century Gothic', 'Futura', 'Avenir Next', sans-serif; font-weight: 800; font-size: 19px; color: #1A1612; letter-spacing: 0.08em; line-height: 1; }
.item-info { flex: 1; min-width: 0; }
.item-info .title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.item-info .role-title { font-size: 14.5px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.005em; }
.active-pill { display: inline-flex; align-items: center; padding: 1px 7px; background: var(--accent-emerald-bg); border: 1px solid var(--accent-emerald-edge); border-radius: var(--radius-pill); color: var(--accent-emerald); font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
.item-info .role-company { display: block; margin-top: 2px; color: var(--accent-cyan); font-size: 13px; font-weight: 600; }
.item-info .role-dates { display: block; margin-top: 1px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-muted); letter-spacing: 0.04em; }
.item-meta { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.item-card.expanded .contrib-pill { display: none; }
.contrib-pill { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-secondary); background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--radius-tight); padding: 2px 7px; letter-spacing: 0.02em; }
.hex-kpi { display: inline-flex; align-items: center; gap: 5px; }
.hex-kpi .hex { width: 22px; height: 22px; background: rgba(217,119,6,0.10); border: 1.5px solid rgba(217,119,6,0.20); border-radius: 4px; clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); display: flex; align-items: center; justify-content: center; color: var(--accent-orange); }
.hex-kpi .hex .material-symbols-rounded { font-size: 12px; }
.hex-kpi .num { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-muted); font-weight: 600; }
.chevron { width: 24px; height: 24px; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); transition: transform 0.25s, color 0.2s; }
.item-card.expanded .chevron { transform: rotate(180deg); color: var(--accent-orange); }
.chevron .material-symbols-rounded { font-size: 18px; }
.item-body { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; }
.item-card.expanded .item-body { max-height: 5000px; }
.item-body-inner { padding: 0 19px 18px 19px; display: flex; flex-direction: column; gap: 14px; }
.item-divider { height: 1px; background: linear-gradient(90deg, var(--accent-cyan-edge), var(--border-default), transparent); margin: 2px 0 4px; }

.contrib-group { display: flex; flex-direction: column; gap: 10px; }
.group-header { display: flex; align-items: center; gap: 8px; }
.group-header .material-symbols-rounded { font-size: 14px; }
.group-header .title { font-family: var(--font-mono); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; }
.group-header .bar { flex: 1; height: 1px; transform-origin: left; animation: scale-bar 0.4s ease-out forwards; }
.group-header.ic .material-symbols-rounded { color: rgba(8,145,178,0.55); }
.group-header.ic .title { color: rgba(8,145,178,0.85); }
.group-header.ic .bar { background: rgba(8,145,178,0.25); }
.group-header.lead .material-symbols-rounded { color: rgba(124,91,191,0.55); }
.group-header.lead .title { color: rgba(124,91,191,0.90); }
.group-header.lead .bar { background: rgba(124,91,191,0.25); }

/* Contribution bullets — recruiter-first: a dot, the sentence, the emerald
   result (figures bolded), one calm row of skills. Two colors total: text +
   emerald for metrics, cyan-tint for skills. The old head row (num / icon /
   domain / scope / boxed metric) and competencies are gone from the view. */
.contrib { position: relative; padding: 2px 0 2px 18px; }
.contrib::before { content: ""; position: absolute; left: 2px; top: 0.58em; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-cyan); opacity: 0.8; }
.contrib.lead::before { background: var(--accent-violet); }
.contrib-action { margin: 0; color: var(--text-primary); font-size: 13.5px; line-height: 1.6; }
.metric-inline { color: var(--accent-emerald); font-weight: 650; white-space: nowrap; }
/* V5 · soft slab — the result sits in a faint tinted panel, contained and deliberate. */
.contrib-impact { margin: 6px 0 0; padding: 6px 10px; border-radius: 7px; background: rgba(16,185,129,0.07); border: 1px solid rgba(16,185,129,0.14); font-size: 12.5px; line-height: 1.55; color: #065f46; }
.contrib-impact strong.m { font-weight: 700; }
[data-theme="dark"] .contrib-impact { background: rgba(52,213,114,0.08); border-color: rgba(52,213,114,0.16); color: rgba(110,231,160,0.95); }
[data-theme="dark"] .contrib-impact strong.m { color: rgba(134,239,172,1); }
.contrib-skills { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 7px; }
.skill-chip { font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; padding: 2px 8px; border-radius: var(--radius-pill); background: var(--accent-cyan-soft, rgba(8,145,178,0.08)); color: var(--accent-cyan); border: 1px solid rgba(8,145,178,0.22); letter-spacing: 0.02em; }
/* Optional project link (repo / live site / writeup) in a project card body. */
.project-link-row { margin-top: 8px; }
.project-link { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 11.5px; color: var(--accent-cyan); text-decoration: none; transition: opacity 0.2s; }
.project-link:hover { opacity: 0.8; }
.project-link .material-symbols-rounded { font-size: 13px; }
.project-link .ext { font-size: 10px; color: var(--text-dim); opacity: 0.55; }

.skills-hud { display: flex; flex-direction: column; gap: 16px; }
.skill-cat { display: flex; flex-direction: column; gap: 8px; }
.skill-cat-header { display: flex; align-items: center; gap: 10px; }
.skill-cat-header .ledge { width: 18px; height: 2px; border-radius: 1px; background: var(--cat-color, var(--accent-cyan)); box-shadow: 0 0 6px color-mix(in srgb, var(--cat-color, var(--accent-cyan)) 50%, transparent); }
.skill-cat-header .name { font-family: var(--font-mono); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--cat-color, var(--accent-cyan)); opacity: 0.92; }
.skill-cat-header .count { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-dim); }
.skill-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 6px; }

/* ── Skills radar — optional spider chart above the level-bar HUD ─────────
   Inline SVG built by renderSkillRadar(); EVERY color is a token so light /
   dark / ink track automatically (the radar carries no hex). Static (no
   animation) → inherits the pane's .materialize entrance, so prefers-reduced-
   motion is already honored. */
.skill-radar-wrap { display: flex; justify-content: center; margin: 4px 0 24px; }
.skill-radar { width: 100%; max-width: 440px; height: auto; overflow: visible; }
.skill-radar-web { fill: none; stroke: var(--border-default); stroke-width: 1; vector-effect: non-scaling-stroke; }
.skill-radar-axis { fill: none; stroke: var(--border-subtle); stroke-width: 1; vector-effect: non-scaling-stroke; }
.skill-radar-poly { fill: color-mix(in srgb, var(--accent-orange) 14%, transparent); stroke: var(--accent-orange); stroke-width: 2; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.skill-radar-dot { fill: var(--accent-orange); }
.skill-radar-dot.is-hot { stroke: var(--card-bg); stroke-width: 1.5; }
.skill-radar-label { font-family: var(--font-mono); font-size: 9.5px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; fill: var(--text-secondary); }
.skill-radar-label.is-hot { fill: var(--accent-orange); }

/* ── Social Feed (optional app) ─── cards JS-rendered by portfolio.js into
   #social-grid. Two templates: EMBED cards (live media) + branded PROFILE
   cards (designed link tiles) — no bland link cards. MASONRY via CSS columns,
   so a short card never leaves dead space below a tall one; inherently
   responsive (1 column on a phone, more as width allows). */
.social-grid { columns: 300px; column-gap: 14px; }
.social-card { break-inside: avoid; width: 100%; margin: 0 0 14px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; transition: border-color 0.15s; }
.social-card:hover { border-color: var(--border-hover); }
.social-chip { width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
.social-chip svg { width: 17px; height: 17px; fill: #fff; }
.social-letter { color: #fff; font-family: var(--font-sans); font-weight: 800; font-size: 14px; line-height: 1; }
.social-head { display: flex; align-items: center; gap: 10px; padding: 11px 13px 10px; }
.social-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.social-plat { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-secondary); }
.social-cap { font-size: 12.5px; color: var(--text-secondary); line-height: 1.4; }
.social-embed { position: relative; width: 100%; }
.social-embed iframe { width: 100%; border: 0; display: block; background: var(--surface-2); }
.social-embed blockquote { margin: 0 !important; }
/* ─── Loading states ───────────────────────────────────────────────── */
/* Embeds: a 2×2 grid of pulsing squares. The loader is an absolute OVERLAY
   (z-index 2) sitting ON TOP of the embed while it loads, removed in JS only
   once the iframe/widget paints (else a static "View on …" fallback card). */
.social-embed[data-embed-pending] { min-height: 120px; background: var(--surface-2); }
.embed-loader { position: absolute; inset: 0; z-index: 2; display: grid; grid-template-columns: repeat(2, 13px); grid-template-rows: repeat(2, 13px); gap: 5px; place-content: center; background: var(--surface-2); }
.embed-loader span { background: var(--accent-orange); border-radius: 3px; animation: embed-sq 1.1s ease-in-out infinite; }
.embed-loader span:nth-child(1) { animation-delay: 0s; }
.embed-loader span:nth-child(2) { animation-delay: 0.14s; }
.embed-loader span:nth-child(4) { animation-delay: 0.28s; }
.embed-loader span:nth-child(3) { animation-delay: 0.42s; }
@keyframes embed-sq { 0%, 100% { opacity: 0.25; transform: scale(0.6); } 40% { opacity: 1; transform: scale(1); } }
.social-embed-static { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 92px; padding: 18px; text-decoration: none; color: var(--text-muted); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; background: var(--surface-2); border-top: 1px solid var(--border-default); transition: color 0.15s, background 0.15s; }
.social-embed-static:hover { color: var(--accent-orange); background: var(--accent-orange-bg); }
.social-embed-static .material-symbols-rounded { font-size: 18px; }
/* Self-branding live embeds (LinkedIn, Instagram, X, TikTok, players, …) already
   show the platform + author INSIDE the embed, so once the embed paints the top
   .social-head chip is pure redundancy — hide it. Static / profile cards and the
   failed-embed fallback (.social-cls-failed, no .social-embed-ok) keep the head;
   so do the weak-branding exceptions (youtube/medium/dribbble/behance/figma/loom,
   never flagged .social-selfbrand). */
.social-selfbrand.social-embed-ok .social-head { display: none; }
/* FAILED-EMBED fallback — when an embed never paints (routine for tokenless
   Instagram/X widgets) JS clears the reserved min-height and flags the card
   .social-cls-failed, so it collapses to this brand-tinted link tile instead
   of leaving dead space. The head (chip + caption) above already carries the
   post content; this row is the intentional "View on …" call-to-action. */
.social-cls-failed .social-embed { min-height: 0; background: transparent; }
.social-cls-failed .social-link { display: none; }
.social-embed-fallback { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 11px 13px 12px; text-decoration: none; border-top: 1px solid var(--border-default); background: var(--surface-2); font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--brand); transition: background 0.15s; }
.social-embed-fallback:hover { background: var(--accent-orange-bg); }
.social-embed-fallback-cta { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.social-embed-fallback-cta .material-symbols-rounded { font-size: 16px; }
.social-embed-fallback .social-go { color: var(--text-dim); font-size: 18px; }
.social-embed-fallback:hover .social-go { color: var(--brand); transform: translate(2px, -2px); }
.social-link { display: inline-flex; align-items: center; gap: 5px; padding: 9px 13px 11px; font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em; color: var(--accent-cyan); text-decoration: none; transition: color 0.15s; }
.social-link .ext { font-size: 13px; }
.social-link:hover { color: var(--accent-orange); }
/* PROFILE card — the whole card is a designed brand-coloured link tile. */
.social-profile { display: flex; align-items: center; gap: 12px; padding: 15px 14px; text-decoration: none; }
.social-profile .social-chip { width: 44px; height: 44px; border-radius: 11px; }
.social-profile .social-chip svg { width: 24px; height: 24px; }
.social-profile .social-letter { font-size: 20px; }
.social-profile .social-meta { flex: 1; gap: 3px; }
.social-profile .social-plat { color: var(--brand); }
.social-profile .social-cap { color: var(--text-primary); font-size: 13.5px; font-weight: 600; }
.social-handle { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.social-go { color: var(--text-dim); font-size: 20px; flex-shrink: 0; transition: color 0.15s, transform 0.15s; }
.social-profile:hover .social-go { color: var(--brand); transform: translate(2px, -2px); }
.social-profile:hover .social-cap { color: var(--brand); }

/* ── POST card (.social-cls-post) ── the self-rendered rich preview we use in
   place of a platform iframe. LinkedIn only serves /embed/ for posts the author
   opted in, and even those paint an unclamped text wall with the image below
   the fold — so when a social entry carries its own payload (text/image/counts)
   portfolio.js builds this instead. Same shell as every other .social-card
   (card-bg + border + radius); everything inside is tokens only.
   Order: head (avatar · name · headline · age · brand mark) → clamped text →
   image → footer (counts · outbound link). */
.social-cls-post { display: flex; flex-direction: column; }
.post-head { display: flex; align-items: flex-start; gap: 10px; padding: 12px 13px 9px; }
.post-avatar { width: 38px; height: 38px; border-radius: var(--radius-pill); object-fit: cover; flex-shrink: 0; border: 1px solid var(--border-default); background: var(--surface-2); }
.post-avatar-fallback { display: inline-flex; align-items: center; justify-content: center; font-family: var(--font-sans); font-weight: 800; font-size: 16px; color: var(--text-secondary); }
.post-ident { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.post-author { font-size: 13.5px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.005em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.post-headline { font-size: 11.5px; color: var(--text-muted); line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.post-age { font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim); margin-top: 1px; }
/* Brand mark: currentColor so it inherits this one rule. color-mix pulls the
   platform hue toward the current text color, which keeps it legible in BOTH
   themes without a second brand token. */
.post-mark { flex-shrink: 0; display: inline-flex; line-height: 0; color: var(--text-dim); }
.post-mark svg { width: 17px; height: 17px; }
.social-linkedin .post-mark { color: color-mix(in srgb, var(--brand-linkedin) 72%, var(--text-primary)); }
/* Clamp is CSS, never a JS substring — the preview can't cut mid-word.
   --post-lines is the single knob (the narrow + print overrides below turn it
   down). max-height is belt-and-braces: -webkit-line-clamp alone leaks a sliver
   of the next line when the text carries hard breaks (white-space: pre-line),
   and vertical padding is kept off this box so the calc stays honest. */
.post-text {
  --post-lines: 4;
  margin: 0 0 11px; padding: 0 13px; font-size: 12.5px; line-height: 1.55;
  max-height: calc(var(--post-lines) * 1.55em);
  color: var(--text-secondary); white-space: pre-line; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--post-lines); line-clamp: var(--post-lines); overflow: hidden;
}
.post-media { display: block; flex: 1 1 auto; min-height: 0; padding: 0 13px; text-decoration: none; }
.post-media img { display: block; width: 100%; height: 100%; min-height: 128px; aspect-ratio: 16 / 10; object-fit: cover; border-radius: var(--radius-card); background: var(--surface-2); border: 1px solid var(--border-subtle); }
.post-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 13px 11px; margin-top: auto; }
.post-counts { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.02em; color: var(--text-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.post-cta { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em; color: var(--accent-cyan); text-decoration: none; transition: color 0.15s; }
.post-cta .ext { font-size: 14px; transition: transform 0.15s; }
.post-cta:hover { color: var(--accent-orange); }
.post-cta:hover .ext { transform: translate(2px, -2px); }
.social-cls-post:hover .post-media img { border-color: var(--border-hover); }

/* ── Overview "Featured" board ─── "Latest from" (pinned socials, 2-up) +
   "Highlights" (featured work items that jump to their full entry). Both
   strips JS-rendered + hidden until filled, so the board leaves no residue. */
.overview-featured { display: flex; flex-direction: column; gap: 18px; margin: 0 0 2px; }
.overview-strip[hidden] { display: none; }
.overview-strip-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0 0 9px; }
.overview-strip-title { font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-orange); }
.overview-seeall { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); text-decoration: none; transition: color 0.15s; }
.overview-seeall .material-symbols-rounded { font-size: 13px; }
.overview-seeall:hover { color: var(--accent-orange); }
.overview-latest { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; align-items: stretch; }
.overview-latest .social-card { display: flex; flex-direction: column; }
.overview-latest .social-cls-profile .social-profile { flex: 1; }
.overview-highlights { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 10px; }
.feature-card { display: flex; align-items: center; gap: 11px; padding: 12px 13px; background: var(--card-bg); border: 1px solid var(--border-default); border-left: 3px solid var(--accent); border-radius: var(--radius-card); text-decoration: none; transition: border-color 0.15s, background 0.15s, transform 0.15s, box-shadow 0.25s; }
.feature-card:hover { background: var(--card-bg-hover); transform: translateY(-1px); }
/* tl-live — the card the timeline playhead is currently on (synced glow). */
.feature-card.tl-live { border-color: var(--accent); background: var(--card-bg-active); box-shadow: 0 0 0 1px var(--accent), 0 4px 14px rgba(0,0,0,0.06); }
.feature-chip { width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; background: var(--surface-2); border: 1px solid var(--border-default); overflow: hidden; }
.feature-chip img { width: 22px; height: 22px; object-fit: contain; }
.feature-chip .material-symbols-rounded { font-size: 19px; color: var(--accent); }
.feature-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.feature-kicker { font-family: var(--font-mono); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: var(--accent); }
.feature-title { font-size: 13.5px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.005em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feature-sub { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feature-go { color: var(--text-dim); font-size: 18px; flex-shrink: 0; transition: color 0.15s, transform 0.15s; }
.feature-card:hover .feature-go { color: var(--accent); transform: translateX(2px); }

/* Embeds can't print / don't exist offline; profile cards always work. Print: links only.
   Post cards keep the same contract — the media is a raster the PDF budget
   doesn't want, so it drops out and the card prints as author + text + link. */
@media print {
  .social-embed, .post-media, .post-mark { display: none !important; }
  .social-link, .social-handle, .post-cta, .post-counts { color: #111 !important; }
  .post-text { --post-lines: 3; }
}
.skill-cell { display: flex; align-items: center; gap: 10px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); padding: 8px 12px; transition: all 0.2s; box-shadow: var(--shadow-sm); }
.skill-cell:hover { background: var(--card-bg-hover); border-color: var(--accent-orange); }
.level-bar { display: flex; align-items: flex-end; gap: 2px; flex-shrink: 0; }
.level-bar .seg { width: 3px; border-radius: 1px; }
.skill-cell .name { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: -0.005em; }
.skill-cell .years { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); flex-shrink: 0; }

.edu-list { display: flex; flex-direction: column; gap: 8px; }
.edu-card { display: flex; align-items: center; gap: 14px; padding: 14px 16px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); box-shadow: var(--shadow-sm); transition: all 0.2s; }
.edu-card:hover { background: var(--card-bg-hover); border-color: var(--border-hover); }
.edu-card .info { flex: 1; min-width: 0; }
.edu-card .title-line { font-size: 13.5px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.005em; }
.edu-card .sub-line { margin-top: 2px; color: var(--accent-cyan); font-size: 12.5px; font-weight: 600; }
.edu-card .date-line { margin-top: 2px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-muted); letter-spacing: 0.04em; }
/* Rich education/certification card — opt-in variant for entries with honors or achievements.
   Basic .edu-card (logo + info, one row) is unchanged; add class "rich" + a .edu-head wrapper
   and a .edu-body with .ach-bullets when the entry carries achievements/honors. */
.edu-card.rich { flex-direction: column; align-items: stretch; gap: 0; padding: 0; }
.edu-card.rich .edu-head { display: flex; align-items: center; gap: 14px; padding: 14px 16px 10px; }
.edu-card.rich .edu-body { padding: 0 16px 14px 16px; display: flex; flex-direction: column; gap: 10px; }
.edu-card .title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.edu-meta { flex-shrink: 0; align-self: center; }
.ach-bullets { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.ach-bullets li { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; line-height: 1.55; color: var(--text-secondary); }
.ach-bullets li > .material-symbols-rounded { font-size: 12px; color: rgba(22, 163, 74, 0.55); margin-top: 3px; flex-shrink: 0; }
.ach-bullets li em { font-style: italic; color: var(--accent-cyan); }
.ach-bullets li strong { color: var(--text-primary); font-weight: 700; }
/* Status pill — honors on edu/cert headers, shipped/in-progress/archived on projects. */
.status-pill { display: inline-flex; align-items: center; padding: 1px 7px; font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; border-radius: var(--radius-pill); flex-shrink: 0; }
.status-pill.honors { background: var(--accent-violet-bg); border: 1px solid var(--accent-violet-edge); color: var(--accent-violet); }
.status-pill.shipped { background: var(--accent-emerald-bg); border: 1px solid var(--accent-emerald-edge); color: var(--accent-emerald); }
.status-pill.in-progress { background: var(--accent-cyan-soft); border: 1px solid var(--accent-cyan-edge); color: var(--accent-cyan); }
.status-pill.archived { background: var(--surface-2); border: 1px solid var(--border-default); color: var(--text-muted); }
.no-date { font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.10em; color: var(--accent-amber); background: var(--accent-amber-bg); border: 1px solid var(--accent-amber-edge); border-radius: var(--radius-tight); padding: 2px 6px; }

.footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border-default); color: var(--text-dim); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; text-align: center; }
.footer a { color: var(--text-muted); text-decoration: none; border-bottom: 1px solid var(--border-default); transition: color 0.2s, border-color 0.2s; }
.footer a:hover { color: var(--accent-orange); border-color: var(--accent-orange); }

/* ─── Share menu (popover) ──────────────────────────────────────────────
   Opened by #share-btn. Items: Copy link · LinkedIn · X · WhatsApp · Email.
   Each item leads with an inline SVG glyph: LinkedIn / X / WhatsApp carry
   their monochrome single-path brand marks (Simple-Icons-style, viewBox
   0 0 24 24, fill="currentColor", pasted from assets/icons/brands/); Copy
   keeps a generic link glyph and Email a mail glyph. All inherit the item's
   color via currentColor — no per-icon color rules — and theme automatically.
   Never full-color logos, never icon fonts, never external icon loads.
   Token-styled so it works in both themes; never prints (topbar is hidden). */
.share-wrap { position: relative; display: inline-flex; }
#share-menu { position: absolute; top: calc(100% + 6px); left: 0; z-index: 900; min-width: 172px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); box-shadow: var(--shadow-md); padding: 5px; }
#share-menu[hidden] { display: none; }
.share-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 11px; border-radius: var(--radius-button); font-size: 12.5px; font-weight: 600; color: var(--text-secondary); transition: background 0.15s, color 0.15s; }
.share-item svg { width: 14px; height: 14px; flex-shrink: 0; }
.share-item:hover { background: var(--card-bg-hover); color: var(--text-primary); }

/* ─── Export chooser (modal) ────────────────────────────────────────────
   Opened by #pdf-btn. THIS RELEASE it is the "Save résumé as PDF" dialog:
   picks resume style + font + fit, sets body.print-doc-resume.print-style-
   <style> (plus --resume-font and, for Auto-fit, --rfs on #resume-view) and
   calls window.print().
   PORTFOLIO PDF — gated for the next release; re-enable by restoring the
   Document fieldset. */
#export-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px; }
#export-modal[hidden] { display: none; }
.export-backdrop { position: absolute; inset: 0; background: rgba(26,22,18,0.45); }
[data-theme="dark"] .export-backdrop { background: rgba(0,0,0,0.55); }
.export-dialog { position: relative; width: min(460px, 100%); max-height: calc(100vh - 40px); overflow-y: auto; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-panel); box-shadow: var(--shadow-md); padding: 18px 20px 20px; }
.export-head { display: flex; align-items: center; justify-content: space-between; }
.export-head h2 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); }
.export-x { width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-button); color: var(--text-muted); font-size: 18px; line-height: 1; transition: background 0.15s, color 0.15s; }
.export-x:hover { background: var(--surface-2); color: var(--text-primary); }
.export-group { border: 0; margin: 14px 0 0; padding: 0; min-width: 0; }
.export-group-label { display: block; font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: var(--text-muted); padding: 0; margin: 0 0 8px; }
.export-opts { display: grid; grid-template-columns: 1fr; gap: 6px; }
.export-opts-2 { grid-template-columns: 1fr 1fr; }
.export-opt { position: relative; display: block; background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--radius-button); padding: 9px 12px; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
.export-opt input { position: absolute; opacity: 0; pointer-events: none; }
.export-opt:hover { border-color: var(--border-hover); }
.export-opt.selected { border-color: var(--accent-orange); background: var(--accent-orange-bg); }
.export-opt .opt-name { display: block; font-size: 13px; font-weight: 700; color: var(--text-primary); }
.export-opt .opt-tag { margin-left: 6px; font-family: var(--font-mono); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.10em; color: var(--accent-orange); }
.export-opt .opt-desc { display: block; margin-top: 2px; font-size: 11.5px; line-height: 1.45; color: var(--text-secondary); }
.export-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.export-cancel { height: 32px; padding: 0 14px; border-radius: var(--radius-pill); border: 1px solid var(--border-default); color: var(--text-secondary); font-size: 12.5px; font-weight: 600; transition: border-color 0.15s, color 0.15s; }
.export-cancel:hover { border-color: var(--border-hover); color: var(--text-primary); }
.export-confirm { height: 32px; padding: 0 16px; border-radius: var(--radius-pill); background: var(--accent-orange); color: #fff; font-size: 12.5px; font-weight: 700; box-shadow: var(--accent-orange-glow); transition: background 0.15s; }
.export-confirm:hover { background: var(--accent-orange-hover); }
@media (max-width: 640px) { .export-opts-2 { grid-template-columns: 1fr; } }

/* ─── Resume view — hidden on screen except during the export lifecycle,
   when body.print-doc-resume (set by the export chooser) reveals it for
   auto-fit measurement and printing (see the resume blocks above).
   --rfs is the resume font scale auto-fit steps down (floor-clamped in JS);
   --resume-font is the dialog's font override. Both are inline-set on this
   element by the confirm handler and removed on afterprint. ─── */
#resume-view { display: none; --rfs: 1; }

@media (max-width: 640px) {
  .wrap { padding: 16px 14px 60px; }
  .identity-card { padding: 18px 18px; }
  /* Stack the (now larger) headshot above the identity info on narrow screens;
     left-align so the photo and name share an edge. */
  .identity-row { flex-direction: column; align-items: flex-start; gap: 14px; }
  .photo-upload { width: 96px; height: 96px; }
  .identity-top { flex-direction: column; align-items: flex-start; gap: 8px; }
  h1.name { font-size: 20px; }
  .summary-band { padding: 14px 18px 16px; }
  .summary-stats { gap: 12px 20px; }
  .section-grid { grid-template-columns: 1fr; }
  .overview-latest { grid-template-columns: 1fr; } /* one column on a phone — 2-up embeds would be too narrow */
  /* Post cards go full-bleed-width on a phone: the headline is the only piece
     that can't wrap, so it ellipsises; the media keeps a wider crop so a
     full-width card doesn't turn into a billboard. */
  .post-media img { aspect-ratio: 16 / 9; }
  .post-text { --post-lines: 3; }
  /* Collapsed/expanded tiles on a phone: give the role its own full-width row,
     then drop the summary cluster (contributions count + breakdown hexes +
     chevron) onto a clean right-aligned second row — so the contributions tag
     never lands on top of a wrapping company name. */
  .item-head { flex-wrap: wrap; align-items: flex-start; gap: 8px 12px; }
  .item-meta { flex-basis: 100%; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }
}

/* ─── Print navigation — contents strip + back-to-top chips ─────────────
   Hidden on screen (the section tabs do this job interactively); shown in
   both print layouts. Anchor links survive Chrome's Save-as-PDF as real
   /Link annotations with /Dest destinations: in the continuous PDF they jump
   down the single tall page, in paginated they land on the section labels.
   Chips are border-based on purpose — no background-color reliance — so they
   survive viewers/printers with "Background graphics" off. Colors ride the
   tokens, so ink's monochrome palette covers them for free.
   Chip numbers are CSS counters (decimal-leading-zero), not static text —
   the list self-renumbers when the conditional Overview entry is stripped. */
.print-toc, .backtotop { display: none; }
.print-toc { margin: 0 0 14px; padding: 10px 14px 12px; border: 1px solid var(--border-default); border-radius: var(--radius-card); }
.print-toc-title { font-family: var(--font-mono); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-orange); margin: 0 0 8px; }
.print-toc-links { display: flex; flex-wrap: wrap; gap: 7px 18px; counter-reset: toc; }
.print-toc-links a { display: inline-flex; align-items: center; gap: 7px; text-decoration: none; color: var(--text-primary); font-size: 11px; font-weight: 600; letter-spacing: -0.005em; counter-increment: toc; }
.toc-chip { display: inline-flex; align-items: center; justify-content: center; padding: 1px 7px; border: 1px solid currentColor; border-radius: var(--radius-pill); font-family: var(--font-mono); font-size: 9px; font-weight: 700; letter-spacing: 0.08em; color: var(--accent-orange); }
.toc-chip::before { content: counter(toc, decimal-leading-zero); }
.backtotop { margin: 6px 0 0; padding: 2px 9px; border: 1px solid currentColor; border-radius: var(--radius-pill); font-family: var(--font-mono); font-size: 9px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); text-decoration: none; }
@media print {
  .print-toc { display: block; }
  .backtotop { display: inline-flex; }
}

/* ─── Continuous single-page export (Layout: Continuous) ────────────────
   PORTFOLIO PDF — gated for the next release; re-enable by restoring the
   Document fieldset. No UI path sets body.print-continuous this release;
   the engine below stays intact for the re-enable.
   body.print-continuous is set by the export confirm handler, which then
   measures document.documentElement.scrollHeight and injects a one-off
   `@page { size: 816px <H>px; margin: 0 }` as the LAST style in <head> (the
   only way to win the @page cascade — @page can't be selector-scoped).
   These rules live OUTSIDE @media print on purpose: the height is measured
   ON SCREEN, so the measured layout must be EXACTLY the printed layout.
   They mirror every layout-affecting rule of the base @media print block;
   !important mirrors the print block and overrides its break-avoid rules at
   print time. Without the class, printing stays the classic paginated path. */
body.print-continuous {
  width: 816px;            /* pin layout width = @page width (8.5in @ 96dpi) */
  padding: 48px 52px 56px; /* visual margins — @page margin is 0, so they live
                              on body where scrollHeight captures them */
  background: #fff;
  font-size: 11px;
}
body.print-continuous .wrap { max-width: none; padding: 0; }
body.print-continuous .topbar,
body.print-continuous .chevron,
body.print-continuous #export-modal,
body.print-continuous #share-menu { display: none !important; }
body.print-continuous .materialize,
body.print-continuous .stagger-1,
body.print-continuous .stagger-2,
body.print-continuous .stagger-3 { animation: none !important; opacity: 1 !important; }
body.print-continuous .section-grid { display: none !important; }
body.print-continuous .section-pane { display: block !important; animation: none !important; }
body.print-continuous .section-pane[data-pane]::before {
  content: attr(data-pane);
  display: block;
  font-family: var(--font-mono); font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.16em;
  color: var(--accent-orange);
  margin: 14px 0 8px;
}
body.print-continuous .section-pane:first-of-type::before { margin-top: 4px; }
body.print-continuous .item-body { max-height: none !important; overflow: visible !important; }
body.print-continuous .print-toc { display: block; }
body.print-continuous .backtotop { display: inline-flex; }
/* One tall page = zero fragmentation: neutralize every break rule the base
   print stylesheet sets, so nothing reserves space it doesn't need. */
body.print-continuous *, body.print-continuous *::before, body.print-continuous *::after {
  break-inside: auto !important; break-before: auto !important; break-after: auto !important;
  page-break-inside: auto !important; page-break-before: auto !important; page-break-after: auto !important;
}
/* A narrow browser window must not leak the mobile layout into the measured
   geometry — restore the desktop values the 816px page actually prints with
   (these out-specificity the @media (max-width: 640px) single-class rules). */
body.print-continuous .identity-card { padding: 22px 24px; }
body.print-continuous .identity-row { flex-direction: row; align-items: flex-start; gap: 18px; }
body.print-continuous .photo-upload { width: 104px; height: 104px; }
body.print-continuous .identity-top { flex-direction: row; align-items: flex-start; gap: 10px; }
body.print-continuous h1.name { font-size: 22px; }
body.print-continuous .summary-band { padding: 16px 24px 18px; }
body.print-continuous .summary-stats { gap: 14px 28px; }
body.print-continuous .item-meta { flex-wrap: nowrap; gap: 10px; }

/* ─── THE THROUGHLINE — chronological strip in the identity card ────────
   Shell markup: #throughline (index.html, bottom of .identity-card); data:
   window.HOPE_DATA.timeline (authoring contract in data.js); behavior:
   portfolio.js. tl-* namespace, tokens only. The card's 32×32 grid texture
   already covers this area — the strip adds NO texture of its own. Motion is
   transform/opacity ONLY: the playhead glides on transform:translateX (set
   inline by rAF-scheduled JS), nodes lift on transform:scale; `left` on
   nodes/ticks is static layout set once at build, never animated.
   Published mode: fully kept — it's a feature, not an editing affordance
   (see the published-mode contract above). Resume print modes exclude it for
   free: the whole .identity-card is display:none there. */
#throughline { position: relative; margin-top: 18px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
#throughline[hidden] { display: none; }
.tl-rail { position: relative; height: 14px; margin: 34px 10px 26px; }
.tl-track { position: absolute; left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px; border-radius: var(--radius-pill); background: var(--border-default); }
.tl-tick { position: absolute; top: calc(50% + 4px); width: 1px; height: 5px; background: var(--border-default); }
.tl-tick-year { position: absolute; top: 6px; left: 50%; transform: translateX(-50%); font-family: var(--font-mono); font-size: 9px; color: var(--text-dim); letter-spacing: 0.06em; }
/* The ridge — the density silhouette BEHIND the flat nodes ("mountain peaks"):
   busy eras rise, quiet stretches stay near the baseline. OPT-IN (issue #8):
   drawn only when HOPE_DATA.timeline_ridge = true; otherwise portfolio.js skips
   the SVG entirely and the timeline stays flat. Nodes never lift — the ridge is
   a static backdrop (z-index 0, under the z-index 2 nodes). Vector + tokens:
   prints crisp, the dark theme swaps it, ink's monochrome token override covers
   it for free. Geometry pairs with RIDGE_H in portfolio.js — change both
   together. */
.tl-ridge { position: absolute; left: 0; right: 0; bottom: 50%; width: 100%; height: 30px; z-index: 0; overflow: visible; }
.tl-ridge-fill { fill: var(--accent-orange); fill-opacity: 0.07; stroke: none; }
.tl-ridge-line { fill: none; stroke: var(--accent-orange); stroke-opacity: 0.38; stroke-width: 1.25px; }
/* Lifted nodes need label headroom — margins grow only when a ridge exists. */
.tl-rail.tl-has-ridge { margin-top: 64px; }
/* Nodes — 14px hexes (the brand clip-path, same polygon as .hex-kpi .hex),
   one per entry, positioned proportionally by start date. Fills per the
   Throughline contract (T3): experience slate · project cyan · education
   violet · certification amber — token names only, values live in :root. */
.tl-node { position: absolute; top: 50%; width: 14px; height: 14px; margin: -7px 0 0 -7px; z-index: 2; cursor: pointer; }
.tl-node .tl-hex { position: absolute; inset: 0; clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); background: var(--accent-slate); transition: transform 0.25s ease; }
.tl-node.tl-project .tl-hex { background: var(--accent-cyan); }
.tl-node.tl-education .tl-hex { background: var(--accent-violet); }
.tl-node.tl-certification .tl-hex { background: var(--accent-amber); }
/* Ongoing (date_end null) — subtle emerald edge: a slightly larger hex
   peeking out from under the type-colored one (a border would be eaten by
   the clip-path). */
.tl-node.tl-ongoing::before { content: ""; position: absolute; inset: -2.5px; clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); background: var(--accent-emerald-edge); transition: transform 0.25s ease; }
.tl-node.tl-active .tl-hex, .tl-node.tl-active.tl-ongoing::before { transform: scale(1.45); }
.tl-node:focus-visible { outline: 2px solid var(--accent-orange); outline-offset: 4px; border-radius: var(--radius-tight); }
/* Floating label — shown on the ACTIVE node: sans 600 12px, mono date below. */
.tl-node-label { position: absolute; bottom: calc(100% + 8px); left: 50%; display: flex; flex-direction: column; align-items: center; gap: 1px; white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity 0.25s ease, transform 0.25s ease; }
.tl-node.tl-active .tl-node-label { opacity: 1; transform: translate(-50%, 0); }
.tl-node-text { font-size: 12px; font-weight: 600; color: var(--text-primary); letter-spacing: -0.005em; max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
.tl-node-date { font-family: var(--font-mono); font-size: 9.5px; color: var(--text-muted); letter-spacing: 0.04em; }
/* Edge clamping (classes set by JS near the rail ends, px-measured) keeps
   labels inside the card instead of bleeding past its rounded corners. */
.tl-node.tl-clamp-start .tl-node-label { left: -7px; align-items: flex-start; transform: translate(0, 3px); }
.tl-node.tl-clamp-end .tl-node-label { left: auto; right: -7px; align-items: flex-end; transform: translate(0, 3px); }
.tl-node.tl-clamp-start.tl-active .tl-node-label, .tl-node.tl-clamp-end.tl-active .tl-node-label { transform: translate(0, 0); }
/* Playhead + traveler. Default traveler: the soft orange glow dot — the
   --accent-orange-glow token IS the glow. SVG travelers (curated slugs
   embedded in portfolio.js, or a generator-inlined custom one via data.js)
   are monochrome single-path marks per the brand-icon law (design-tokens.md
   §6), ~16px, riding the same spot; .tl-flip mirrors them while the loop
   wraps backward. Playhead z-index 1 < node z-index 2: the traveler passes
   under the hexes. */
.tl-playhead { position: absolute; left: 0; top: 50%; z-index: 1; pointer-events: none; transition: transform 0.55s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform; }
.tl-traveler { position: absolute; left: 0; top: 0; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; }
.tl-traveler.tl-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-orange); box-shadow: var(--accent-orange-glow); }
.tl-traveler.tl-svg { color: var(--accent-orange); }
.tl-traveler.tl-svg svg { width: 16px; height: 16px; display: block; fill: currentColor; }
.tl-traveler.tl-flip svg { transform: scaleX(-1); }
/* Click-navigate target flash (T6) — outline pulse via the glow token, 1.2s.
   Both keyframes carry two shadows so the fade interpolates instead of
   snapping (mismatched list lengths animate discretely). */
@keyframes tl-target-pulse { 0%, 65% { box-shadow: var(--accent-orange-glow), 0 0 0 2px var(--accent-orange); } 100% { box-shadow: 0 4px 14px transparent, 0 0 0 2px transparent; } }
.tl-flash { animation: tl-target-pulse 1.2s ease-out; }
/* THE SPOTLIGHT — .hope-spotlight, applied by the #spotlight=<key> hash
   directive (registry IIFE in portfolio.js). Same token glow as
   tl-target-pulse above — no new colors — but ~3 soft pulses over ~3s
   (50% peak per cycle × 3 iterations) instead of one flash; the JS removes
   the class when the run ends. Print is inert by construction: the directive
   only plays on an interactive load, the class lives ~3s, and the print
   blocks already zero the glow token. Both frames carry two shadows so the
   fade interpolates (same doctrine as tl-target-pulse). The !important is
   load-bearing: pane targets carry .section-pane.active (0,2,0), whose
   materialize shorthand would otherwise replace this animation and the
   spotlight would scroll but never glow. Reduced-motion stays safe — the
   #throughline media rule outranks this (1,0,0 vs 0,1,0, both !important). */
@keyframes hope-spotlight-pulse { 0%, 100% { box-shadow: 0 4px 14px transparent, 0 0 0 2px transparent; } 50% { box-shadow: var(--accent-orange-glow), 0 0 0 2px var(--accent-orange); } }
.hope-spotlight { animation: hope-spotlight-pulse 1s ease-in-out infinite !important; } /* runs until the user engages (JS counts 2 more flashes, then stops; 60s cap) */
/* Timeline spotlight needs air: the strip's own box hugs the rail and sits
   tight under the summary text, so the generic ring overlapped both. For the
   timeline the ring draws on an absolute OVERLAY instead — pushed down off
   the text above (positive top inset), padded outward on the sides/bottom,
   and rounded (radius-panel; box-shadow follows the radius). Paint-only:
   zero layout shift during the pulse. */
#throughline.hope-spotlight { animation: none !important; }
#throughline.hope-spotlight::after {
  content: ""; position: absolute; inset: -4px -24px -26px;
  border-radius: var(--radius-panel); pointer-events: none;
  animation: hope-spotlight-pulse 1s ease-in-out infinite;
}
/* Hover mini-card (T5) — token panel. Lives at BODY level: the identity
   card's overflow:hidden plus its materialize transform (fill-mode keeps a
   computed transform, which makes the card the containing block even for
   fixed children) would clip any card-level tooltip. JS clamps it inside the
   viewport. Chips reuse .skill-chip. z-index 980: above content, below the
   export modal (1000). */
.tl-tooltip { position: absolute; z-index: 980; max-width: 280px; background: var(--card-bg); border: 1px solid var(--border-default); border-radius: var(--radius-card); box-shadow: var(--shadow-md); padding: 10px 12px; pointer-events: none; }
.tl-tooltip[hidden] { display: none; }
.tl-tip-label { font-size: 12.5px; font-weight: 600; color: var(--text-primary); letter-spacing: -0.005em; }
.tl-tip-org { margin-top: 1px; font-size: 11.5px; font-weight: 600; color: var(--accent-cyan); }
.tl-tip-dates { margin-top: 2px; font-family: var(--font-mono); font-size: 10px; color: var(--text-muted); letter-spacing: 0.04em; }
.tl-tip-metric { margin-top: 5px; font-family: var(--font-mono); font-size: 10.5px; color: var(--accent-emerald); }
.tl-tip-skills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }
/* prefers-reduced-motion (T4) — static rail: playhead hidden, no autoplay
   (portfolio.js adds .tl-static and never starts the loop), all nodes
   visible as always, labels on hover/focus only. */
.tl-static .tl-playhead { display: none; }
.tl-static .tl-node:hover .tl-node-label, .tl-static .tl-node:focus-visible .tl-node-label { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  #throughline *, #throughline *::before { transition: none !important; animation: none !important; }
}

/* ─── THE THROUGHLINE — print (T8) ──────────────────────────────────────
   Static rail in EVERY portfolio print style: playhead + tooltip hidden, ALL
   node labels rendered. On screen only the ACTIVE node shows a label, so the
   line reads cleanly left→right; but in print every label prints at once. To
   keep the strict left→right read (no labels ever bounce ABOVE the line) while
   still clearing dense clusters (~12 nodes on a ~644px rail put neighbors as
   close as ~26px), labels print WRAPPED and narrow (max-width 78px, 8.5px/1.15
   text, 7.5px date) and sit in TWO rows that BOTH hang BELOW the rail:
   alternate nodes (.tl-stagger, JS-set on every 2nd node) drop to the lower
   row, so same-row neighbors are 2 entries apart — enough horizontal clearance
   for the narrow wrapped labels. Single horizontal stagger only, no vertical
   zig-zag. Colors ride the tokens, so ink's monochrome token override covers
   hexes/labels/ticks for free; showcase inherits this block unchanged. Resume
   modes never print the strip — it rides the existing identity-card hide. */
@media print {
  .tl-playhead, .tl-tooltip { display: none !important; }
  .tl-rail { margin: 34px 10px 116px; }
  .tl-rail.tl-has-ridge { margin-top: 60px; }
  .tl-node .tl-hex, .tl-node.tl-ongoing::before { transform: none !important; }
  /* All labels hang BELOW the rail — strict left→right, never above the line. */
  .tl-node-label { opacity: 1 !important; bottom: auto; top: calc(100% + 14px); transform: translate(-50%, 0) !important; max-width: 78px; white-space: normal; text-align: center; }
  .tl-node-text { font-size: 8.5px; line-height: 1.15; }
  .tl-node-date { font-size: 7.5px; line-height: 1.2; }
  .tl-node.tl-clamp-start .tl-node-label, .tl-node.tl-clamp-end .tl-node-label { transform: translate(0, 0) !important; }
  .tl-node.tl-clamp-start .tl-node-label { text-align: left; }
  .tl-node.tl-clamp-end .tl-node-label { text-align: right; }
  /* Lower row for alternate nodes — single horizontal stagger, still below. */
  .tl-node.tl-stagger .tl-node-label { top: calc(100% + 62px); }
}

/* THE THROUGHLINE on the continuous canvas — mirror of the print block above
   (the continuous height is measured ON SCREEN, so the measured layout must
   BE the printed layout — same doctrine as the rest of this section). */
body.print-continuous .tl-playhead, body.print-continuous .tl-tooltip { display: none !important; }
body.print-continuous .tl-rail { margin: 34px 10px 116px; }
body.print-continuous .tl-rail.tl-has-ridge { margin-top: 60px; }
body.print-continuous .tl-node .tl-hex, body.print-continuous .tl-node.tl-ongoing::before { transform: none !important; }
body.print-continuous .tl-node-label { opacity: 1 !important; bottom: auto; top: calc(100% + 14px); transform: translate(-50%, 0) !important; max-width: 78px; white-space: normal; text-align: center; }
body.print-continuous .tl-node-text { font-size: 8.5px; line-height: 1.15; }
body.print-continuous .tl-node-date { font-size: 7.5px; line-height: 1.2; }
body.print-continuous .tl-node.tl-clamp-start .tl-node-label, body.print-continuous .tl-node.tl-clamp-end .tl-node-label { transform: translate(0, 0) !important; }
body.print-continuous .tl-node.tl-clamp-start .tl-node-label { text-align: left; }
body.print-continuous .tl-node.tl-clamp-end .tl-node-label { text-align: right; }
body.print-continuous .tl-node.tl-stagger .tl-node-label { top: calc(100% + 62px); }

/* ─── H1 · Wide-screen rails (≥1440px) ──────────────────────────────────
   THE RAILS CONTRACT: at wide viewports the summary floats in a quiet LEFT
   rail and the section-grid becomes a sticky RIGHT rail. The portfolio
   column (identity card + panes + footer) keeps its exact narrow geometry —
   the rails occupy only viewport space that was empty margin before, and
   the identity card gains nothing inside it.
   APPROACH — CSS grid on .wrap + ONE JS relocation (authoring note in
   portfolio.js): the section-grid is already a direct .wrap child, so the
   grid below re-columns it with zero DOM changes — its click wiring and the
   Overview default-open logic never notice. The summary alone is moved by
   JS into a .summary-rail <aside>, because its in-card slot is unreachable
   by CSS alone: .identity-card's overflow:hidden + border/background chrome
   would have to dissolve (display:contents) to let a grid lift it out —
   the exact reverted v0.8.0 mistake this section replaces.
   BREAKPOINT 1440px — the center column needs its full 832px content width
   (880px .wrap max-width minus 48px padding) + 2 × 250px rail minimum
   + 2 × 24px gaps = 1428px before rails fit WITHOUT squeezing the portfolio
   column; 1440 is the first standard viewport tier above that floor (the
   logical width of common 13/14-inch laptops), and minmax(0, 832px) lets
   the center absorb scrollbar-width slack right at the threshold instead
   of overflowing. Rails flex 250→280px, capped by the 1488px max-width.
   VIEWPORT media query — and explicitly gated OFF for the whole export
   lifecycle: the continuous engine pins the BODY to 816px and resume
   auto-fit pins it to ~710px, but min-width media queries read the
   VIEWPORT, which stays wide — so every rule here carries
   body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio).
   Real print never matches anyway (the page box is 816px < 1440), so all
   print modes keep the single-column flow with the summary printed in-card
   (portfolio.js moves it home on beforeprint and before the continuous
   measurement). Below 1440px not one rule here applies — the narrow
   rendering stays byte-identical to the single-column layout. */
@media (min-width: 1440px) {
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .wrap {
    max-width: 1560px; /* center + identity rail + wider right column + gaps + padding */
    display: grid;
    /* REBALANCED: left rail is now identity-ONLY (compact), so it narrows; the
       RIGHT column widens to give the relocated About bio a premium, readable
       measure above the apps tiles (not a cramped sliver). */
    grid-template-columns: minmax(232px, 0.82fr) minmax(0, 760px) minmax(330px, 1.18fr);
    column-gap: 32px;
  }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .wrap > * { grid-column: 2; min-width: 0; }
  /* LEFT rail — the relocated summary, top-aligned with the identity card
     (row 2 IS the card's row; the long span parks any rail overflow in
     empty zero-height implicit rows, so neither rail inflates the card's
     row). Quiet on purpose: no card chrome — a thin orange ledge over a
     mono ABOUT eyebrow, then the same summary text. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .wrap > .summary-rail { grid-column: 1; grid-row: 2 / span 24; align-self: start; }
  /* Photo rides at the TOP of the rail: full rail width, forced square (img covers). */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .photo-upload { width: 100%; height: auto; aspect-ratio: 1 / 1; margin: 0 0 16px; }
  /* LEFT rail is identity-ONLY now — the About bio has moved to the RIGHT
     column. Give the identity-info room under the square photo; contact pills
     stack full-width so long links wrap. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .identity-info { flex: none; }
  /* PREMIUM STATS — drop the code-y "//" strip for a 2x2 metric grid that echoes
     the overview KPIs (.summary-stat): big Inter numeral over a mono micro-label. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .stats-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 14px; margin-top: 18px; padding-top: 18px; border-top: 1px solid var(--border-subtle); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .stats-row .sep { display: none; }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .stats-row > span { display: flex; flex-direction: column; gap: 2px; font-family: var(--font-mono); font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .stats-row > span strong { font-family: var(--font-sans); font-size: 20px; font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); }
  /* PREMIUM CONTACT — hairline-divided list (not boxy full-width pills), with the
     icons in accent for a branded, editorial feel. Long links wrap in place. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row { flex-direction: column; align-items: stretch; gap: 0; margin-top: 20px; border-top: 1px solid var(--border-subtle); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row .item { width: 100%; justify-content: flex-start; align-items: center; gap: 11px; padding: 10px 2px; border: none; border-bottom: 1px solid var(--border-subtle); border-radius: 0; background: transparent; color: var(--text-secondary); font-size: 12px; word-break: break-word; }
  /* Last contact link drops its hairline — it's the final element of the
     identity-only left rail, so a trailing rule would dangle. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row .item:last-child { border-bottom: none; }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row a.item:hover { background: transparent; color: var(--text-primary); border-color: var(--border-subtle); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row .item .material-symbols-rounded { font-size: 16px; color: var(--accent-orange); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .summary-rail .contact-row .item svg { width: 15px; height: 15px; color: var(--accent-orange); }
  /* Centre card keeps ONLY the timeline — the emptied .identity-row collapses out. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .identity-card > .identity-row { display: none; }
  /* "Career Timeline" header above the standalone strip — SAME convention as the
     centre-column section titles (.overview-strip-title): mono, orange, no ledge.
     :has() gates it to a visible strip (no orphan header on empty data; degrades
     to hidden where :has is unsupported). The strip's own divider is redundant
     under the header, so drop its border here. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .tl-rail-eyebrow { display: none; }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .identity-card:has(> .tl-strip:not([hidden])) .tl-rail-eyebrow { display: block; margin: 0 0 9px; font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-orange); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .identity-card:has(> .tl-strip:not([hidden])) > #throughline { border-top: none; margin-top: 4px; }
  /* RIGHT column — the .about-rail aside (JS-built at wide): the About bio at
     the TOP, then the apps grid (.section-grid, moved INTO the aside by JS so
     both share column 3 and read top→bottom). The whole column rides sticky so
     About + app navigation stay in reach while the center column scrolls.
     align-self:start is load-bearing — a default-stretched grid item fills its
     whole area and leaves sticky no room to travel. top offset matches the
     .wrap top padding. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .wrap > .about-rail { grid-column: 3; grid-row: 2 / span 24; align-self: start; position: sticky; top: 24px; }
  /* The About bio — premium editorial block at the top of the right column:
     a thin orange ledge over a mono ABOUT eyebrow, then a generous, readable
     measure (the widened column lets it breathe — no cramped sliver). */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .about-rail .summary::before { content: "About"; display: block; width: max-content; border-top: 2px solid var(--accent-orange); padding-top: 7px; margin-bottom: 11px; font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.16em; color: var(--accent-orange); }
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .about-rail .summary { margin: 0 0 22px; max-width: none; font-size: 14px; line-height: 1.72; color: var(--text-secondary); }
  /* Apps grid below the bio — single-column tiles, same anatomy and behavior. */
  body:not(.print-continuous):not(.print-doc-resume):not(.print-doc-portfolio) .about-rail > .section-grid { grid-template-columns: 1fr; margin-bottom: 0; }
}

.post-deep { display: flex; align-items: center; gap: 5px; margin-top: 7px; padding-top: 7px;
  border-top: 1px solid var(--border-subtle, rgba(0,0,0,.07));
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 600; letter-spacing: .02em;
  color: var(--accent-orange); text-decoration: none; }
.post-deep:hover { text-decoration: underline; }
.post-deep .ext { font-size: 14px; }
@media print { .post-deep { display: none !important; } }
