/* ============================================================================
   prefs.css — reader preferences: text size, four themes, and the mobile fixes
   that fall out of both.

   MUST BE LINKED LAST on every page, after article.css / refarch.css / demo.css
   / resume.css. Two reasons:

     1. `html[data-theme="reading"]` and `html[data-scheme="light"]` are both
        specificity 0-1-1. Nothing but source order decides which wins, and
        Reading has to win — it reuses the entire light-theme component layer
        and only swaps the palette underneath it.
     2. The overflow and tap-target corrections below are deliberate overrides
        of rules in those files.

   Two custom properties drive everything:

     --fs      the reader's text multiplier. Every authored font-size in the
               other stylesheets reads `calc(<original> * var(--fs, 1))`, so at
               --fs: 1 the computed values are byte-identical to the original
               design and the default rendering cannot regress.
     --fs-menu the same multiplier, capped, for chrome that must not outgrow the
               viewport it sits in.

   No inline styles, no inline scripts — the strict CSP applies here too.
   ========================================================================= */


/* ---------------------------------------------------------------------------
   1 · TEXT SIZE
   Four steps rather than a slider: a slider invites hunting for a value, and
   every intermediate value is another layout to have tested. 112 / 125 / 140
   are roughly one comfortable notch apart at 16px body text (18 / 20 / 22.4px).
   140% is the ceiling because past it the two-column article grid and the
   header both need a different layout, not a bigger one — and browser zoom,
   which this site does not block, takes over from there.
------------------------------------------------------------------------------ */
:root { --fs: 1; --fs-menu: 1; }
html[data-fs="1"] { --fs: 1;    --fs-menu: 1;    }
html[data-fs="2"] { --fs: 1.12; --fs-menu: 1.12; }
html[data-fs="3"] { --fs: 1.25; --fs-menu: 1.25; }
html[data-fs="4"] { --fs: 1.4;  --fs-menu: 1.25; }

/* Figures opt out of the multiplier.

   .canvas holds the reference-architecture diagrams: node POSITIONS are
   percentages of the box and node SIZES are container-query units, so the
   drawing behaves like vector art — but the label text is what sets each node's
   height, and the zone-name pills sit in a 24px band that placeZones() reserves
   above the topmost member. Scaling the type 40% inside that geometry collides
   nodes and pushes pills into the row above. svg.diagram is the same argument in
   a stricter form: its text sits inside hand-placed <rect>s of fixed width.

   Both are figures with a text alternative — every diagram node opens a drawer,
   and each <svg> carries a descriptive aria-label — so the reader who needs
   larger text is not losing the content, only the picture's internal labelling.
   The pinning is one declaration and it is deliberate. */
.canvas, svg.diagram { --fs: 1; }

/* The header controls must not resize themselves: a button that grows when you
   press it reflows the header under your finger, and at 140% the cluster would
   no longer fit a 320px viewport. The menu it opens does scale, capped. */
.prefs { --fs: 1; }
.theme-menu { --fs: var(--fs-menu); }


/* ---------------------------------------------------------------------------
   2 · READING THEME
   Warm paper, soft brown ink. Reading resolves to data-scheme="light", so every
   light-theme component rule in enhance.css and refarch.css already applies —
   this block only remaps the palette those rules read from.

   Contrast is measured, not eyeballed. The light theme's own floor is 4.71:1
   (--mist-500 on --ink-700); this ramp is tuned to the same ladder and its
   floor is 4.75:1, so nothing regresses. The one deliberate difference is the
   top of the ramp: --mist-100 sits at ~11.8:1 rather than light's 14.6:1,
   because the point of a reading theme is to take the glare off the extremes,
   not to reproduce black on white in beige.
------------------------------------------------------------------------------ */
html[data-theme="reading"] {
  --ink-950: #f3ead8;   /* page — warm paper */
  --ink-900: #fbf4e6;   /* raised card */
  --ink-800: #ede2cd;   /* card */
  --ink-700: #e4d7bd;   /* hover surface */
  --ink-600: #cbb894;   /* borders/dividers */

  --mist-100: #241d12;  /* strongest text — ~11.8:1 on the page */
  --mist-200: #3b3122;  /* secondary — ~9.6:1 */
  --mist-300: #493b2a;  /* nav links, stat labels — ~7.6:1 */
  --mist-400: #594933;  /* meta — ~6.1:1 */
  --mist-500: #6c583f;  /* faintest — 4.75:1 on the lightest surface it lands on */

  --teal-300: #0f5f66;
  --teal-400: #0f5f66;
  --teal-500: #0c4e54;
  --teal-600: #0a4248;
  --amber-400: #7a4a12;

  --ink-950-rgb: 243, 234, 216;
  --ink-900-rgb: 251, 244, 230;
  --ink-800-rgb: 237, 226, 205;
  --ink-700-rgb: 228, 215, 189;
  --ink-600-rgb: 203, 184, 148;
  --mist-100-rgb: 36, 29, 18;
  --mist-200-rgb: 59, 49, 34;
  --mist-300-rgb: 73, 59, 42;
  --mist-400-rgb: 89, 73, 51;
  --mist-500-rgb: 108, 88, 63;
}

html[data-theme="reading"] body { background: var(--ink-950); color: var(--mist-100); }

/* The hero's glow and grid are hardcoded teal rgba in the light block; warm them
   so the paper stays paper all the way up the page. */
html[data-theme="reading"] .hero::after {
  background: radial-gradient(ellipse 60% 50% at 50% 30%, rgba(122, 74, 18, 0.09), transparent 60%);
}
html[data-theme="reading"] .hero-grid {
  background-image:
    linear-gradient(rgba(122, 74, 18, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(122, 74, 18, 0.055) 1px, transparent 1px);
}
html[data-theme="reading"] ::selection { background: var(--teal-400); color: #fff; }

/* refarch pages keep their own variable set for the page chrome. The canvas and
   the drawer are deliberately left alone: refarch.css restores the authored dark
   palette inside them under data-scheme="light", which Reading also matches, so
   the figures stay dark on warm paper exactly as they stay dark on white. */
html[data-theme="reading"] .ra {
  --bg0:#e4d7bd; --bg1:#f3ead8; --panel:#fbf4e6; --panel2:#ede2cd;
  --line:#cbb894; --ink:#241d12; --muted:#594933; --faint:#6c583f;
  --blue:#1d4ed8; --blue2:#1e40af; --teal:#0f5f66; --mint:#0f6a4a;
  --amber:#7a4a12; --slate:#594933; --rose:#a02560;
  --glow:0 0 0 1px rgba(36,29,18,.06), 0 12px 30px rgba(36,29,18,.12);
}
html[data-theme="reading"] .ra .chip:not(.on) { background: rgba(36, 29, 18, .045); }
html[data-theme="reading"] .ra .targets .t   { background: rgba(36, 29, 18, .045); }
html[data-theme="reading"] .ra .capcard:hover { box-shadow: 0 10px 24px rgba(36, 29, 18, .14); }


/* ---------------------------------------------------------------------------
   3 · HEADER CONTROLS
------------------------------------------------------------------------------ */
.prefs { position: relative; display: inline-flex; align-items: center; gap: 8px; }

/* A joined pair rather than two separate buttons: it reads as one control with
   two directions, and the shared border saves 2px of a very tight header. */
.fs-step {
  display: inline-flex;
  border: 1px solid color-mix(in srgb, var(--mist-400) 28%, transparent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--ink-800) 60%, transparent);
  overflow: hidden;
}
.fs-btn {
  display: inline-flex; align-items: baseline; justify-content: center; gap: 1px;
  width: 38px; height: 38px;
  padding: 0;
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 38px;
  color: var(--mist-200);
  cursor: pointer;
  transition: color .2s, background .2s;
}
.fs-btn + .fs-btn { border-left: 1px solid color-mix(in srgb, var(--mist-400) 28%, transparent); }
.fs-btn:hover:not(:disabled) { color: var(--teal-300); background: color-mix(in srgb, var(--teal-400) 12%, transparent); }
.fs-btn:active:not(:disabled) { background: color-mix(in srgb, var(--teal-400) 20%, transparent); }
.fs-btn:focus-visible { outline: 2px solid var(--teal-400); outline-offset: -2px; }
/* Disabled at the ends of the range. 3.4:1 against the control's own fill keeps
   the glyph legible while reading as unavailable — an ends-of-range control the
   reader cannot see is worse than one they can see is spent. */
.fs-btn:disabled { color: var(--mist-500); cursor: default; }
.fs-btn .a { font-size: 15px; }
.fs-btn[data-dir="-1"] .a { font-size: 12.5px; }
.fs-btn .s { font-size: 12px; font-weight: 600; }

/* The theme control is now a menu button, not a toggle. */
.theme-toggle[aria-expanded="true"] {
  color: var(--teal-300);
  border-color: color-mix(in srgb, var(--teal-400) 55%, transparent);
}
/* One icon per theme; the button shows the active one. */
.theme-toggle .icon-auto, .theme-toggle .icon-read { display: none; }
html[data-theme="dark"]    .theme-toggle .icon-moon { display: block; }
html[data-theme="dark"]    .theme-toggle .icon-sun,
html[data-theme="dark"]    .theme-toggle .icon-auto,
html[data-theme="dark"]    .theme-toggle .icon-read { display: none; }
html[data-theme="light"]   .theme-toggle .icon-sun  { display: block; }
html[data-theme="light"]   .theme-toggle .icon-moon,
html[data-theme="light"]   .theme-toggle .icon-auto,
html[data-theme="light"]   .theme-toggle .icon-read { display: none; }
html[data-theme="auto"]    .theme-toggle .icon-auto { display: block; }
html[data-theme="auto"]    .theme-toggle .icon-moon,
html[data-theme="auto"]    .theme-toggle .icon-sun,
html[data-theme="auto"]    .theme-toggle .icon-read { display: none; }
html[data-theme="reading"] .theme-toggle .icon-read { display: block; }
html[data-theme="reading"] .theme-toggle .icon-moon,
html[data-theme="reading"] .theme-toggle .icon-sun,
html[data-theme="reading"] .theme-toggle .icon-auto { display: none; }

.theme-menu {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 60;
  min-width: 216px;
  max-width: min(280px, calc(100vw - 32px));
  padding: 6px;
  border-radius: 14px;
  border: 1px solid var(--ink-600);
  background: var(--ink-900);
  box-shadow: 0 18px 40px -14px rgba(0, 0, 0, .55);
}
.theme-menu[hidden] { display: none; }
.tm-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border-radius: 10px;
  text-align: left;
  color: var(--mist-200);
  font-family: var(--font-body);
  font-size: calc(13.5px * var(--fs, 1));
  line-height: 1.35;
  cursor: pointer;
}
.tm-item:hover { background: var(--ink-800); color: var(--mist-100); }
.tm-item:focus-visible { outline: 2px solid var(--teal-400); outline-offset: -2px; }
.tm-item svg { flex: 0 0 auto; width: 16px; height: 16px; color: var(--mist-400); }
.tm-item[aria-checked="true"] { color: var(--mist-100); background: color-mix(in srgb, var(--teal-400) 12%, transparent); }
.tm-item[aria-checked="true"] svg { color: var(--teal-300); }
.tm-label { display: flex; flex-direction: column; min-width: 0; }
/* 12px floor: this is the smallest reader-facing string in the new chrome. */
.tm-note { font-size: calc(12px * var(--fs, 1)); color: var(--mist-400); }
/* The check is the only thing marking the active row for a reader who cannot
   perceive the tint, so it carries state and needs 3:1 (WCAG 1.4.11), which
   --teal-300 clears in all four themes. */
.tm-check { margin-left: auto; opacity: 0; color: var(--teal-300); }
.tm-item[aria-checked="true"] .tm-check { opacity: 1; }

/* Announcements for the stepper and the theme change. */
.prefs-status {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}


/* ---------------------------------------------------------------------------
   4 · HEADER SPACE BUDGET
   Measured at 320px: --gutter clamps to 20px a side, leaving 280px. The logo
   with its wordmark is 139px and the cluster is now four controls. Without the
   two reductions below the header overflows and the wordmark wraps to two lines
   (measured: 106x47 instead of 139x28).
------------------------------------------------------------------------------ */
@media (max-width: 900px) {
  .nav { gap: 8px; }
}
/* ⌘K is a keyboard affordance. Below this width the visitor is almost certainly
   on a touch keyboard-less device, where the hint is 44px of noise — and the
   command palette is still reachable from the nav menu. */
@media (max-width: 700px) {
  .cmdk-trigger { display: none; }
}
/* Collapse the wordmark to its mark. The link keeps its aria-label, so the
   accessible name is unchanged. */
@media (max-width: 400px) {
  .logo-text { display: none; }
}


/* ---------------------------------------------------------------------------
   5 · TAP TARGETS
   WCAG 2.5.8 (AA) wants 24x24 CSS px for anything you have to hit. Measured
   offenders were all navigation text links sitting at their line-box height:
   footer meta links at 14px tall, breadcrumbs at 15-19px.

   min-height on an inline-flex link, not padding: padding grows the painted
   background and would put a visible slab behind a breadcrumb; min-height grows
   only the hit area, and the surrounding line-height already has the room.
------------------------------------------------------------------------------ */
.footer-meta a,
.breadcrumb a,
.ra-crumb a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}
.footer-meta { align-items: center; row-gap: 2px; }
.breadcrumb, .ra-crumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0 6px; }

/* Coarse pointers get the AAA target (2.5.5) on the header controls, which are
   the most-used and least-forgiving hits on the page. */
@media (pointer: coarse) {
  .theme-toggle, .nav-toggle { width: 44px; height: 44px; }
  .fs-btn { width: 44px; height: 44px; line-height: 44px; }
  .tm-item { padding: 12px 10px; }
}


/* ---------------------------------------------------------------------------
   6 · RESIDUAL OVERFLOW
   gridfloor.py floored the grid TRACKS. A track that refuses to grow is only
   half the fix: a grid or flex ITEM still defaults to min-width:auto, i.e.
   "never shrink below my min-content width", and will happily overflow the
   track it sits in. These are the items measured doing exactly that.
------------------------------------------------------------------------------ */
.venture-grid > *, .writing-grid > *, .expertise-grid > *,
.arch-grid > *, .systems-grid > *, .channels > *,
.article-shell > *, .metric-row > *, .prose > * { min-width: 0; }

.venture-cta-row { flex-wrap: wrap; }

/* Measured at 390px: content 196px in a 134px box, inside a .metric-row that
   clips. The string is a stack name (".NET 10 · HotChocolate"), not a number, so
   a fixed 26px display size cannot be right on a phone. The clamp keeps 26px
   from ~620px up and gives the narrow case somewhere to go; overflow-wrap is the
   last resort for a single token longer than the column. */
.metric-row .metric .n {
  font-size: calc(clamp(18px, 4.2vw, 26px) * var(--fs, 1));
  overflow-wrap: anywhere;
  line-height: 1.15;
}
.metric-row .metric { min-width: 0; }


/* ---------------------------------------------------------------------------
   7 · THE HAND-DRAWN SVG DIAGRAMS
   The worst legibility defect on the site, and the one the font-size audit could
   not see. These figures have viewBox="0 0 1200 …" and are sized to the column,
   so at 390px they render 312px wide — a scale of 0.26. getComputedStyle still
   reports "10px" for a sub-label because that is 10 USER units; on screen it is
   2.6px. Unreadable, on the three writing posts and the home page.

   The fix is the pattern refarch.css already uses for its own canvas: stop
   shrinking the drawing, hold it at a width where its type is legible, and let
   the reader pan. 1320px puts the smallest run at 11 * 1.1 = 12.1px on screen.
   prefs.js adds tabindex and a role to the wrapper so the pan is reachable from
   a keyboard (2.1.1) and announced.
------------------------------------------------------------------------------ */
@media (max-width: 1000px) {
  .diagram-wrap {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
  }
  .diagram-wrap > svg.diagram {
    width: 1320px;
    max-width: none;
    flex: 0 0 auto;
  }
  .diagram-wrap:focus-visible { outline: 2px solid var(--teal-400); outline-offset: 3px; }
  /* A drawing that runs off the edge with no scrollbar reads as a broken layout.
     The fade says "there is more this way" without costing a control. */
  .diagram-wrap::after {
    content: "";
    position: sticky;
    right: 0;
    display: block;
    width: 34px;
    margin-left: -34px;
    align-self: stretch;
    flex: 0 0 auto;
    pointer-events: none;
    background: linear-gradient(90deg, transparent, var(--ink-950));
  }
  .diagram-wrap { display: flex; }
}


/* ---------------------------------------------------------------------------
   8 · LARGE TEXT SETTINGS
   At 125% and above the desktop nav runs out of room before the mobile
   breakpoint does, and the article's 240px table-of-contents column stops being
   worth its width. Both are reflow requirements (WCAG 1.4.10) rather than
   nice-to-haves: content must not require two-dimensional scrolling.
------------------------------------------------------------------------------ */
html[data-fs="3"] .nav-links,
html[data-fs="4"] .nav-links { gap: 18px; }

@media (max-width: 1180px) {
  html[data-fs="3"] .article-shell,
  html[data-fs="4"] .article-shell { grid-template-columns: minmax(0, 1fr); }
  html[data-fs="3"] .article-toc,
  html[data-fs="4"] .article-toc { display: none; }
}
@media (max-width: 1080px) {
  html[data-fs="4"] .nav-links { display: none; }
  html[data-fs="4"] .nav-toggle { display: inline-flex; }
}
@media (max-width: 980px) {
  html[data-fs="3"] .nav-links { display: none; }
  html[data-fs="3"] .nav-toggle { display: inline-flex; }
}

/* Long single words (URLs, class names, Azure resource names) are the usual
   cause of a phone-width overflow in prose, and they get longer as the reader
   scales up. */
.prose p, .prose li, .prose h2, .prose h3, .prose h4 { overflow-wrap: break-word; }


/* ---------------------------------------------------------------------------
   9 · REFLOW — THE STRINGS THAT WILL NOT BREAK
   Measured over 15 pages x 5 widths x both ends of the text-size range. At the
   default size every page fits. At 140% six of them ran past the right edge,
   and because body{overflow-x:hidden} propagates to the viewport the overrun is
   CLIPPED rather than scrollable — the tail of a URL simply stops existing.
   That is WCAG 1.4.10 (reflow), not a cosmetic complaint.

   Three causes, measured individually with min-content probes rather than
   guessed at. The widest element on every failing page was the fixed header,
   which was a red herring: Chrome's mobile layout viewport widens to the
   scrollable width, so a full-bleed fixed bar stretches to match whatever else
   overflowed. Its width equalled viewport + overflow to the pixel in all six
   cases — the signature of a symptom.

   (a) URLs and email addresses have no legal break point. They sit inside flex
       items, and a flex item's default min-width:auto means "never shrink below
       my min-content width", so the item held its full 340px and pushed the
       document out. `anywhere` rather than `break-word` is required here:
       only `anywhere` also lowers the min-content contribution, which is the
       number the flex algorithm actually reads. hyphens:none because a hyphen
       inserted mid-URL reads as part of the address.

   (b) One nowrap label, "All reference architectures ->", which fits at 100%
       and does not at 140%. The nowrap is deliberate — it stops the arrow
       orphaning onto its own line — so it is relaxed only once the reader has
       scaled up, leaving the default rendering exactly as designed.

   (c) Ordinary long words in headings and prose. overflow-wrap:break-word is a
       pure safety net: it never fires while a word fits, so it cannot change a
       layout that currently renders correctly. hyphens:auto is switched on only
       above the default size, so that a break the reader has forced looks like
       typography ("architec-tures,") rather than damage ("architecture" "s,").
       Numerals cannot hyphenate and read as broken when split, so the two
       display figures that overran instead get a viewport-aware floor — the
       clamp ceiling is unchanged, so nothing moves above ~550px.
------------------------------------------------------------------------------ */

/* (a) */
.venture-cta,
.hero-cta-row .cta,
.r-contact, .r-contact > *, .r-contact a {
  min-width: 0;
  overflow-wrap: anywhere;
  hyphens: none;
}

/* (b) */
html[data-fs]:not([data-fs="1"]) .systems-more .arch-cta { white-space: normal; }

/* (c) overflow-wrap and hyphens both inherit, so this is two declarations, not
   a selector list that has to be kept in step with the markup. */
body { overflow-wrap: break-word; }
html[data-fs]:not([data-fs="1"]) body { hyphens: auto; }
code, pre, kbd, samp, .mono, .venture-cta, .cmdk-input { hyphens: none; }

html[data-fs]:not([data-fs="1"]) .stat .num {
  font-size: calc(clamp(20px, 6.6vw, 36px) * var(--fs, 1));
}
html[data-fs]:not([data-fs="1"]) .stat .label {
  font-size: calc(clamp(11px, 3.6vw, 13px) * var(--fs, 1));
}


/* ---------------------------------------------------------------------------
   10 · MOTION
------------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .fs-btn, .theme-toggle, .tm-item { transition: none; }
}

/* ---------------------------------------------------------------------------
   11 · COLOURS THAT ONLY EXISTED FOR THE DARK THEME

   Two accents on the site were written as literals before there was a light
   theme to check them against, and adding one turned both into the worst
   contrast on the site. Both are fixed at the source rather than patched here:
   refarch's capgroup accents now resolve through its per-scheme token blocks,
   and the tag below reads a custom property that this section retunes. What is
   left here is only the part that is genuinely per-scheme.

   Reading resolves to data-scheme="light" and then repaints the surfaces sepia,
   so one selector covers both — but the ratio has to hold on both backgrounds,
   which are not the same colour. Measured, composited, at the tag's real
   position on the page: #E4F3EC on light and #E3E5C9 / #E3E6CA on reading.
------------------------------------------------------------------------------ */

/* #84e28a measured 1.39:1 on light and 1.23:1 on reading — the tag was very
   nearly invisible. Scaled 45% toward black in sRGB, which is an exact scalar
   multiply: hue moves 123.8deg -> 124.2deg and saturation 0.416 -> 0.422, so it
   is the same green, only darker. Worst case across the three backgrounds is
   5.17:1. The background stays as authored — at 12% alpha it is what produces
   those measured backdrops, and it is decoration, not a state cue. */
html[data-scheme="light"] {
  --oss-tag-ink: #3B663E;
}

@media print {
  .prefs, .theme-menu { display: none !important; }
  /* Paper has no reader-adjustable viewport; the pt sizes in style.css's print
     block are chosen for physical output and must not be multiplied. */
  :root { --fs: 1; --fs-menu: 1; }
  .diagram-wrap { overflow: visible !important; display: block !important; }
  .diagram-wrap > svg.diagram { width: 100% !important; }
  .diagram-wrap::after { display: none !important; }
}
