/* ============================================================
   THE MOBILE STYLESHEET. Chrome (M0), the home screen (M1), and
   since M2 the module frame, the routing, About and Projects. Theme, resume, contacts,
   github and status land in M3-M4.

   It is emitted as its own file, `dist/m.<hash>.css`, linked with
   `media="(max-width:820px)"`, and it goes through the SAME `tokenize()` the desktop
   stylesheet does (`scripts/theme-build.mjs`) -- so every accent literal below is rewritten
   to `rgb(var(--aRRGGBB))` and the four profile blocks are appended by the generator.
   Two consequences, and both are the point:

     - a pink written by hand here is themed automatically, in all four profiles, with
       no second table to keep in sync. The literals below are the desktop's own, so
       they map to the same values.
     - THIS FILE CONTAINS NO @media QUERY OF ITS OWN. The breakpoint is emitted once,
       into the <link media> and into the shell's loader, from `MOBILE_MAX` in
       `scripts/prerender.mjs`. A second copy here is how the two drift, and the failure is a
       phone that loads the mobile stylesheet and the desktop bundle.

   The desktop stylesheet (`v2/styles.css`) is UNTOUCHED by every mobile phase. Anything
   here that undoes one of its rules does so by overriding it at equal specificity from
   a later stylesheet -- see "the tree switch" below.
   ============================================================ */

/* ---- the tree switch -----------------------------------------------------
   Three trees are in every shell: `#fit` (desktop), `#doc` (the static document) and
   `#m`. Below the breakpoint this file reveals `#m` and takes the other two away.

   `html,body{height:auto;overflow:auto}` is the half that is easy to miss and fatal to
   miss: `styles.css:41` pins `height:100%;overflow:hidden`, which is right for a fixed
   1536x1024 canvas and means a mobile page CANNOT BE SCROLLED. `body::after` is the
   fixed vignette, which has nothing left to vignette once the canvas is gone.

   `#m[hidden]` rather than `#m`: the attribute is the fail-safe (a desktop never loads
   this file at all), and the guard in the shell head hides the tree again with
   `#m[hidden]{display:none}` at EQUAL specificity from a later <style>, which is how a
   broken `m.js` falls back to the static document. */
/* `overflow: visible`, and the `overflow-x: hidden` that was here is GONE on purpose.
   Hiding sideways overflow does not fix it, it conceals it twice over: the reader gets
   content clipped at the right edge with no way to reach it, and
   `documentElement.scrollWidth` is CLAMPED to the viewport, which took the measurement
   away as well. Measured at 375: a 575px child inside #m-main left scrollWidth at 375
   -- test-mobile's "nothing overflows sideways" assertion could not fail. Visible
   overflow scrolls, and a gate that can go red is worth more than a symptom that cannot
   be seen. */
html, body { height: auto; overflow: visible; }
body::after { display: none; }
#fit { display: none; }
#doc { display: none; }
/* `display: flex` HERE and not in the `#m` block below, and it is a specificity trap
   rather than a preference: `#m[hidden]` is (0,1,1,0) and `#m` is (0,1,0,0), so a
   `display:flex` down there loses to this rule and the column never forms. Measured:
   the UPLINK footer sat under the terminal box in the middle of the screen instead of
   at the bottom of the viewport, with every other declaration in the block applying
   normally -- which reads as a layout mistake, not as a cascade one. */
#m[hidden] { display: flex; }
/* ...and the attribute keeps working INSIDE the tree, which the rule above would
   otherwise undo one specificity level down: `.m-back{display:inline-flex}` beat the UA's
   `[hidden]{display:none}` and painted the module frame's back and close controls on the
   home screen. Descendants only -- `#m [hidden]` cannot match `#m` itself. */
#m [hidden] { display: none; }

#m {
  --m-mono: 'JetBrains Mono', ui-monospace, monospace;
  --m-disp: 'Chakra Petch', system-ui, sans-serif;
  --m-ink: #cfd7e6;
  --m-ink-b: #eef2fa;
  --m-dim: #8590a6;
  --m-faint: #5b6478;
  --m-acc: rgb(var(--aff4e93));
  --m-acc-s: rgb(var(--aff8cba));
  --m-line: rgba(168,186,224,.20);
  --m-glass: rgba(10,14,28,.62);
  /* estimated from the mockups at 852px / 375pt = 2.272; every value here is a
     STARTING one and M1 re-measures it against a screenshot (MOBILE-SPEC). */
  --m-pad: 20px;
  --m-gap: 10px;
  --m-hdr: 46px;
  --m-tap: 46px;      /* >= the 44px tap target 2.7 asks for */
  --m-r: 6px;
  position: relative;
  z-index: 2;
  /* display is on `#m[hidden]` above -- see the note there before adding one here. */
  /* `100%` does not work here: `html,body` are `height:auto` above (they have to be, or
     the page cannot scroll), so a percentage height has nothing to resolve against and
     the tree is as tall as its content -- which parked the UPLINK footer in the middle
     of the screen on the M0 chrome. `dvh` follows the phone's collapsing browser chrome
     and `vh` is the fallback for engines without it. */
  min-height: 100vh;
  min-height: 100dvh;
  flex-direction: column;
  font-family: var(--m-mono);
  color: var(--m-ink);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
}

/* ---- the background ------------------------------------------------------
   A static image per profile, captured offline from the real engine
   (`tools/shot-mbg.mjs`), hashed into the build and swapped by `html[data-theme]`. The
   URL tokens are baked by `scripts/pipeline.mjs`; a leftover `__MBG_` throws the build.

   It is on the CONTAINER, not on the layer, so it is correct before any JS runs --
   the shell's preflight has already set `data-theme` at that point. The one layer is
   M3's cross-fade: `setTheme` paints the OUTGOING image onto it inline and fades it out
   over the container's new one, the same shape as the desktop's `#bgx` snapshot. */
#m-bg {
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: #04050c no-repeat center / cover;
  background-image: url(/m-bg-pink.671e365e.jpg);
}
html[data-theme="mint"]  #m-bg { background-image: url(/m-bg-mint.c384bfae.jpg); }
html[data-theme="amber"] #m-bg { background-image: url(/m-bg-amber.00211828.jpg); }
html[data-theme="blue"]  #m-bg { background-image: url(/m-bg-blue.a0920560.jpg); }

/* AMBIENT SYNC OFF (M3), and this is why there are two attributes rather than one. The
   desktop's rule is `ambientIdx = ambientSync ? i : 0` (main-ann.js:1171): sync off pins
   the AMBIENT -- the background -- to profile 0, Neural Pink, while the interface keeps
   the chosen accent. It is not "keep the previous image": switching mint -> amber with
   sync off leaves pink both times, and a phone that kept the previous one would drift
   away from a laptop reading the same localStorage value.
   `data-theme` therefore cannot own the image. `data-bg` does, `m.js` sets both, and
   these four rules are (0,2,1) against the four above them at (0,2,1) -- equal, so ORDER
   decides and these are later. The two agree whenever sync is on, which is why the four
   above are still the ones that paint before the bundle runs.
   The repeated `__MBG_<id>__` tokens are deliberate and are the case the M0 review round
   made `bakeImages` global for: `String.replace` with a string pattern substitutes once,
   and the second copy shipped as a leftover token. */
html[data-bg="pink"]  #m-bg { background-image: url(/m-bg-pink.671e365e.jpg); }
html[data-bg="mint"]  #m-bg { background-image: url(/m-bg-mint.c384bfae.jpg); }
html[data-bg="amber"] #m-bg { background-image: url(/m-bg-amber.00211828.jpg); }
html[data-bg="blue"]  #m-bg { background-image: url(/m-bg-blue.a0920560.jpg); }
#m-bg i {
  position: absolute; inset: 0; display: block;
  background: no-repeat center / cover;
  opacity: 0; transition: opacity var(--th-ms, 480ms) ease;
}
#m-bg i.on { opacity: 1; }
/* The dimming the mockups' module pages have and their home page does not. It is a CSS
   layer rather than a second capture: one image per profile, and how dark it reads is a
   decision that can be reviewed and changed without re-running Playwright. */
#m-veil {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(4,5,12,.10) 0%, rgba(4,5,12,.58) 46%, rgba(4,5,12,.88) 100%);
}

/* ---- header --------------------------------------------------------------- */
#m-hdr {
  position: sticky; top: 0; z-index: 3;
  display: flex; align-items: center; gap: 10px;
  height: var(--m-hdr); padding: 0 var(--m-pad);
  background: rgba(4,5,12,.86);
  border-bottom: 1px solid var(--m-line);
  backdrop-filter: blur(6px);
}
.m-mark { width: 26px; height: 22px; flex: none; fill: var(--m-acc); }
.m-id { margin: 0; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.m-id b {
  font-family: var(--m-disp); font-weight: 400; font-size: 13px;
  letter-spacing: .22em; color: var(--m-ink-b);
}
.m-id span {
  /* SITE.tagline is "AI ENGINEERING WORKSTATION" and the mockup's header says "AI
     ENGINEERING" -- MOBILE-PLAN's rule is that content.mjs wins, so the string stays
     whole and the type gives way. Measured at 375: 6.5px/.2em is 158px against the
     205px the header has left beside the mark and the controls. */
  font-size: 6.5px; letter-spacing: .2em; color: var(--m-dim); white-space: nowrap;
}
.m-nav { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.m-live { width: 7px; height: 7px; border-radius: 50%; background: var(--m-acc); flex: none; }
.m-back, .m-x {
  font: inherit; font-size: 10.5px; letter-spacing: .18em;
  color: var(--m-acc); background: none; border: 0; padding: 0 2px;
  min-height: var(--m-tap); display: inline-flex; align-items: center;
  text-decoration: none; cursor: pointer;
}
/* M4. The cross is one 14px glyph, so `min-height` alone left it 14px WIDE -- a 46px-tall
   sliver, and the one control on this tree a finger has to hit exactly. `min-height` was
   never the whole target and nothing measured the other axis until `test-a11y` 8 did.
   MEASURED at 375: the header reads 158px of wordmark + 80px of TERMINAL + 46px here
   against 335px of line, and at 320 the `.m-id` tagline is what gives way, which is the
   behaviour its own note above already describes. */
.m-x { min-width: var(--m-tap); justify-content: center; padding: 0; }
/* the chevron and the cross. Both are `<i>` and both are aria-hidden, so the accessible
   name of the back link is `MOBILE.back` and of the X is its `aria-label` -- a glyph
   read aloud as "multiplication sign" is what an unlabelled cross gets. */
.m-back i, .m-x i { font-style: normal; }
.m-back i { margin-right: 6px; font-size: 13px; line-height: 1; }
.m-x i { font-size: 14px; line-height: 1; }
.m-back:focus-visible, .m-x:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: 2px; }

/* ---- main -----------------------------------------------------------------
   `position: relative; z-index: 2` puts the content above `#m-veil`, which is z-index 1
   inside this same stacking context. Without it the veil -- a fixed, full-bleed
   gradient -- paints over every unpositioned box in the tree, and the page renders as a
   header, a background and nothing else. The header (z-index 3) sat above it and was
   the reason that looked like "main is empty" rather than like a paint order bug. */
#m-main { position: relative; z-index: 2; flex: 1 0 auto; padding: 22px var(--m-pad) 28px; }
#m-hero:empty, #m-cmds:empty, #m-body:empty, #m-pn:empty { display: none; }

/* ---- the notched frame (M1) -----------------------------------------------
   THE MOCKUP DRAWS A LINE THROUGH THE CHAMFER. Measured off `mobile-main-page.png` at
   the terminal box's top-right corner: the 1px outline is continuous, it follows the
   45-degree cut, and the interior is barely lighter than the page around it. Neither of
   the two obvious constructions gives that:

     - `border: 1px solid` + `clip-path` (the DESKTOP's idiom, `.ab-quote`, `.ftr-r`,
       `.ab-stat`) clips the border away along the two diagonals. The corner reads as a
       cut IN the frame, which is right for a 1536-wide panel and wrong here.
     - one filled `::before` at `inset: -1px` behind a `clip-path`ed box does not draw a
       frame at all. MEASURED, and it is the kind of wrong that looks deliberate: the
       parent's clip-path clips its own pseudo-element too, so the -1px never shows, and
       because `--m-glass` is TRANSLUCENT the line colour underneath it tints the whole
       box instead. The terminal box came out a pale slab with no outline. A frame that
       paints as a fill.

   So the frame is a RING, made of two pseudo-elements and no border: `::before` is the
   outer polygon in the line colour, `::after` is the same polygon inset 1px and filled
   with the glass, painted over it. Both sit behind the content (`isolation: isolate` and
   `z-index: -1`, in DOM order). What is left of `::before` is a 1px outline on all six
   edges, diagonals included.

   `--m-notch-i` is NOT `--m-notch`, and the arithmetic is the whole trick: a box inset
   1px whose corner is cut by the same 14px has its diagonal on the line `x + y = W - 14`
   -- exactly the outer one, so the ring would be 1px on the four straight edges and ZERO
   on the two diagonals. Shrinking the inner cut by 1.4px moves that line to `x + y =
   W - 12.6`, which is 1.4/sqrt(2) = 0.99px away. One pixel, all the way round. */
.m-fr {
  --m-notch: 14px;
  --m-notch-i: calc(var(--m-notch) - 1.4px);
  position: relative; isolation: isolate; background: none;
}
.m-fr::before, .m-fr::after {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
}
.m-fr::before {
  inset: 0; background: rgba(168,186,224,.30);
  clip-path: polygon(0 0, calc(100% - var(--m-notch)) 0, 100% var(--m-notch),
                     100% 100%, var(--m-notch) 100%, 0 calc(100% - var(--m-notch)));
}
.m-fr::after {
  inset: 1px; background: var(--m-glass);
  clip-path: polygon(0 0, calc(100% - var(--m-notch-i)) 0, 100% var(--m-notch-i),
                     100% 100%, var(--m-notch-i) 100%, 0 calc(100% - var(--m-notch-i)));
}

/* ---- the hero (M1) --------------------------------------------------------
   SITE.nameCaps, DOC.roles, DOC.quote, then the SYSTEM pill. Nothing here animates in:
   the desktop's `mod-in` intro is a panel behaviour and a phone's home screen is the
   first thing a reader sees, so there is no state at opacity 0 for a print or a
   reduced-motion run to have to park (test-mobile's faint probe asserts exactly that). */
#m-hero { margin: 0 0 22px; }
.m-name {
  margin: 0; font-family: var(--m-disp); font-weight: 300;
  /* the mockup's 62px at 852 scales to 27.3pt, which overflows 335px of line at .16em:
     "SVYAT MARUTYAK" measures 350px. 25px/.155em measures 318px and keeps the mockup's
     one-line hero. Measured at 375, not estimated. */
  font-size: 25px; letter-spacing: .155em; line-height: 1.1;
  color: var(--m-ink-b); text-shadow: 0 0 26px rgba(190,212,255,.30);
}
.m-roles {
  margin: 14px 0 0; font-size: 12px; line-height: 19px; letter-spacing: .10em;
  color: #b6c0d3;
}
.m-quote { margin: 16px 0 0; font-size: 12.5px; line-height: 19px; color: #b3bdd1; }
/* the mockup's pill, and the desktop SYSTEM panel's own words. It is a `p`, not a
   button: nothing happens when it is tapped, and a control that does nothing is worse
   than a label. The notch is the small version of the terminal frame's. */
#m-sys {
  --m-notch: 9px;
  margin: 18px 0 0; display: inline-flex; align-items: center; gap: 9px;
  min-height: 34px; padding: 0 14px;
  font-size: 10.5px; letter-spacing: .17em; color: #c3ccdd;
}
#m-sys::after { background: rgba(90,115,165,.07); }

/* ---- telemetry (M1) -------------------------------------------------------
   Three cells, METRICS[0..2]. `minmax(0,1fr)` and not `1fr`: a grid track's automatic
   minimum is its content's min-content width, so the MEMORY label plus a 46px spark
   would push the row past 335px rather than shrink -- which is the shape test-mobile's
   overflow gate exists to catch, and it is cheaper to make impossible here. */
/* A ROUNDED RECTANGLE, and the notch is deliberately not here: in the mockup the
   telemetry row and the feed strip are plain rounded boxes and only the SYSTEM pill and
   the terminal frame are chamfered. Four notched boxes stacked would make the chamfer
   the page's texture rather than its punctuation. */
#m-tele {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  border: 1px solid var(--m-line); border-radius: 3px; background: var(--m-glass);
}
.m-tc { padding: 11px 10px 12px; min-width: 0; }
.m-tc + .m-tc { border-left: 1px solid var(--m-line); }
.m-tc .k {
  display: block; font-size: 8.5px; letter-spacing: .14em; color: #7d8699;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.m-tc .v {
  display: block; margin-top: 5px; font-weight: 400; font-size: 15px;
  letter-spacing: .02em; color: #f2f6fd;
}
/* 100% wide with a fixed height and `preserveAspectRatio="none"`: the path is drawn in
   an 88x24 viewBox by m.js (the desktop's own construction) and stretched to whatever
   the cell has left, so the three cells stay equal at 320 as well as at 430. */
.m-tc svg { display: block; margin-top: 6px; width: 100%; height: 15px; }
.m-tc .ln { fill: none; stroke: rgb(var(--aff5c9e)); stroke-width: 1.4; vector-effect: non-scaling-stroke; }
.m-tc .fl { fill: rgba(var(--aff5c9e),.14); stroke: none; }

/* ---- the agent feed (M1) --------------------------------------------------
   One line that changes, where the desktop has a six-line log and a typing code block.

   `--ag-1..5` ARE emitted into this file by the second tokenize() pass, but only inside
   the four `html[data-theme]` blocks -- the generated `:root` carries the accent list and
   nothing else. So the five Neural Pink values (`v2/styles.css:29-33`) are the fallback
   for a document with no `data-theme`, which is a real case: localStorage throwing in
   private mode. `app.<hash>.css` is linked without a `media` and would supply them too;
   this file must not depend on that, because it is the stylesheet a phone is guaranteed
   to have and that one is not.

   THE FALLBACK IS IN THE var() CALL AND NOT IN A DECLARATION, and that is a specificity
   bug fixed rather than a style. Written as `#m-feed{--ag-1: 246,147,179}` the fallback
   is an ID selector, (1,0,1) against `html[data-theme="blue"]`'s (0,1,1) -- so it WON,
   and the agent names stayed Neural Pink under all four profiles while every other
   accent on the page themed correctly. It shipped one build and was caught by looking at
   the four screenshots, not by a test: the gate asserted the hue CLASS was present, which
   it always was. `var(--ag-1, <literal>)` has no specificity at all and defers to
   whichever block defined the property.

   The index IS the agent IS the hue (test-content 3). Do not renumber. */
#m-feed {
  --ag: var(--ag-1, 246,147,179);
  margin: 12px 0 0; display: flex; align-items: center; gap: 9px;
  min-height: 38px; padding: 0 12px;
  border: 1px solid var(--m-line); border-radius: 3px; background: var(--m-glass);
}
.m-fh {
  font-size: 9.5px; letter-spacing: .15em; color: #9fa9bf; white-space: nowrap;
  padding-right: 9px; border-right: 1px solid var(--m-line);
}
/* `min-width: 0` on the flex item and `nowrap` + ellipsis on the text: without the
   first, a flex child refuses to shrink below its content and the longest FEED_MSGS
   line ("Analyzing portfolio structure...", 32 chars) widens the PAGE rather than
   clipping. That is a horizontal-overflow bug the 320px column would find first. */
.m-fl {
  flex: 1 1 auto; min-width: 0;
  font-size: 10.5px; color: #b3bdd1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.m-fl .ag0 { --ag: var(--ag-1, 246,147,179); } .m-fl .ag1 { --ag: var(--ag-2, 231,168,85); }
.m-fl .ag2 { --ag: var(--ag-3, 131,202,130); } .m-fl .ag3 { --ag: var(--ag-4, 58,201,232); }
.m-fl .ag4 { --ag: var(--ag-5, 178,169,255); }
.m-fl .who { color: rgb(var(--ag)); }

/* the terminal box, on `.m-fr` above. M0's plain 1px border and `--m-r` radius are gone;
   `--m-r` stays declared for the smaller boxes. Pure CSS, no SVG, nothing for m.js to
   draw. The third notched box, M2's `#m-cap`, is at the bottom of this file. */
#m-term { padding: 16px; margin-top: 18px; }
.m-th {
  display: flex; align-items: center; gap: 10px;
  font-size: 11.5px; letter-spacing: .18em; color: var(--m-ink-b);
  padding-bottom: 12px; border-bottom: 1px solid var(--m-line);
}
.m-th i { color: var(--m-acc); font-style: normal; }
.m-tv { margin: 14px 0 0; font-size: 12.5px; line-height: 1.55; color: var(--m-ink); }

/* ---- the command grid (M1) ------------------------------------------------
   SEVEN buttons in two columns, which does not divide. The arrangement is ABOUT full
   width FIRST, then 2 + 2, then STATUS | THEME in the mockup's split box (MOBILE-SPEC 4a).
   The alternative -- six in 3x2 with an empty seventh cell -- left a hole the eye reads as
   a missing button, and the mockup's own split row already establishes that a row may be
   one box with a divider in it.

   WHICH button takes the wide cell is Svyat's call and it moved on 2026-08-25: M1 gave it
   to the fifth button, and on a real phone that read as the odd one out sitting in the
   middle of the list. At the top it reads as the entry point instead. Nothing in the CSS
   knows which button it is -- `.m-cmd.wide` is positional and the grid auto-flows -- so
   moving it is one class in the markup.

   `.m-split` is a grid ITEM spanning both columns and a flex container inside, so the
   two halves share the row's height without a nested grid's track maths. */
#m-cmds { margin-top: 16px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--m-gap); }
.m-cmd {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--m-tap); padding: 0 12px;
  font: inherit; font-size: 11.5px; letter-spacing: .16em; color: #e2e8f5; text-align: left;
  /* PLAIN RECTANGLES, measured off the mockup: its command buttons have a continuous
     1px border and a ~2.5pt radius, no chamfer. A clip-path here would cut that border
     at two corners of each of the seven -- fourteen broken corners inside one frame. */
  border: 1px solid var(--m-line); border-radius: 3px; background: rgba(255,255,255,.022);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.m-cmd i { font-style: normal; font-size: 13px; line-height: 1; color: var(--m-acc); }
.m-cmd.wide { grid-column: 1 / -1; }
.m-cmd:active { background: rgba(var(--aff4e93),.10); border-color: rgba(var(--aff4e93),.40); }
.m-cmd:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: 2px; }
/* the mockup's split box: ONE frame around two commands, divided by a rule. The halves
   drop their own border and radius, or the divider reads as two buttons that happen to
   touch rather than as one box with a line down it. */
.m-split {
  grid-column: 1 / -1; display: flex;
  border: 1px solid var(--m-line); border-radius: 3px; background: rgba(255,255,255,.022);
}
.m-split .m-cmd { flex: 1 1 0; min-width: 0; border: 0; border-radius: 0; background: none; }
.m-split .m-cmd + .m-cmd { border-left: 1px solid var(--m-line); }

/* ============================================================
   THE MODULE PAGE (M2)
   ============================================================

   `#m-mod` is a plain section and NOT a `.m-fr`. Read off both module mockups: the
   chamfered box is the `ABOUT // MODULE` bar alone, and everything under it -- the hero,
   the section heads, the tiles, the bars, the project entries -- sits on the page with
   no frame around it. That keeps MOBILE-SPEC 4a's actual rule (one notched box per
   screen, the chamfer as punctuation rather than texture) while correcting its
   parenthetical "(and M2's #m-mod)", which assumed the frame wrapped the whole page.

   Nothing on this page animates in, for the same reason nothing on the home screen does:
   M2 adds no intro, so there is no state at opacity 0 and no width starting at zero for
   a print or reduced-motion run to have to park. The two lists at the bottom of this
   file are therefore unchanged by this phase, and `test-mobile` 9 is what says so. */
#m-mod { display: block; }
/* programmatic focus only -- `tabindex="-1"` cannot be reached by Tab, so a ring here
   would be drawn on a container the reader never focused deliberately. `:focus-visible`
   is left alone, which is the engine's own judgement about keyboard intent. */
#m-mod:focus { outline: none; }

/* ---- the chip bar --------------------------------------------------------- */
#m-cap { padding: 13px 16px 12px; }
.m-chip {
  margin: 0; display: flex; align-items: center; gap: 9px;
  font-family: var(--m-mono); font-weight: 400;
  font-size: 11.5px; letter-spacing: .18em; color: var(--m-dim);
}
.m-chip b { font-weight: 400; color: var(--m-ink-b); }
/* POSITION IN THE SEVEN, not a loading bar: `m.js` sets the width to (index+1)/7 when
   the module opens, so `about` reads one seventh and `theme` reads full. */
.m-prog { margin-top: 11px; height: 2px; background: var(--m-line); }
.m-prog i {
  display: block; height: 100%; width: 0; background: var(--m-acc);
  box-shadow: 0 0 8px rgba(var(--aff4e93),.55);
}

/* ---- the body ------------------------------------------------------------- */
#m-body { padding-top: 22px; }
/* the About page leads with his name, as the mockup does. Smaller than the home
   screen's, because there it is the first thing on the page and here it sits under a
   chip that has already named the module: 22px/.14em measures 279px against 335 of
   line, and 236 at a 320px viewport. */
.m-name.sm { font-size: 22px; letter-spacing: .14em; }
#m-body .m-roles { margin-top: 13px; }
/* the mockup's quote box. A plain rounded rectangle -- the chamfer belongs to the chip
   bar on this screen, and a second notched box next to it makes the cut the texture. */
.m-quote.box {
  margin-top: 16px; padding: 14px 16px;
  border: 1px solid var(--m-line); border-radius: var(--m-r); background: var(--m-glass);
}
/* the section head, `DOC.head.*`. The `//` is split into its own aria-hidden <i> by
   `m.js` so it can carry the accent the mockups give it -- the STRING is untouched and
   the space after it is preserved, which is what keeps `innerText` equal to the field. */
.m-h {
  margin: 26px 0 12px; font-family: var(--m-mono); font-weight: 400;
  font-size: 10.5px; letter-spacing: .17em; color: #c3ccdd;
}
.m-h i { font-style: normal; color: var(--m-acc); }

/* ---- MISSION LOG ---------------------------------------------------------- */
/* One box per row, where the desktop absolutely-positions six rows on a 48.9 pitch
   inside a fixed panel. Nothing here is positioned: a phone has one column and the row
   heights differ, because "QUALITY ENGINEERING MANAGER" wraps at 375 and "QA AGENT"
   does not. The grid is year+pill on the first line, title and subtitle under them. */
.m-row {
  display: grid; grid-template-columns: 1fr auto; gap: 2px 10px;
  padding: 11px 12px 12px; border: 1px solid var(--m-line); border-radius: 3px;
  background: var(--m-glass);
}
.m-row + .m-row { margin-top: var(--m-gap); }
.m-row .yr { font-size: 9.5px; letter-spacing: .13em; color: #8f99ad; align-self: center; }
.m-row .tt {
  grid-column: 1 / -1; margin-top: 4px; font-weight: 400;
  font-size: 12px; letter-spacing: .06em; color: var(--m-ink-b);
}
.m-row .sb { grid-column: 1 / -1; font-size: 10px; letter-spacing: .08em; color: #8590a6; }
/* ONGOING against COMPLETED, the desktop's own two states (`.pill` / `.pill.done`).
   The live one takes the accent; the rest are quiet, or six pills compete with the
   titles they belong to. */
.m-row .pill {
  padding: 3px 7px; border: 1px solid var(--m-line); border-radius: 2px;
  font-size: 8px; letter-spacing: .13em; color: #8590a6; white-space: nowrap;
}
.m-row .pill.on { color: var(--m-acc); border-color: rgba(var(--aff4e93),.45); }

/* ---- CORE STATS ----------------------------------------------------------- */
/* 2 x 2, which is the mockup's arrangement and `STATS`'s own length. `minmax(0,1fr)`
   for the reason `#m-tele` uses it: a track's automatic minimum is its content's
   min-content width, and "YEARS LEADING TEAMS" would push the row past the viewport
   rather than wrap. */
.m-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--m-gap); }
.m-stat {
  padding: 12px 12px 10px; border: 1px solid var(--m-line); border-radius: 3px;
  background: var(--m-glass);
}
.m-stat .v {
  display: block; font-family: var(--m-disp); font-weight: 400;
  font-size: 22px; letter-spacing: .04em; color: var(--m-acc);
}
.m-stat .k {
  display: block; margin-top: 5px; font-size: 8.5px; letter-spacing: .12em; color: #8590a6;
}
/* The line is DETERMINISTIC (see `spark()` in m.js) and static, unlike the home
   screen's meters: a line that moved under "YEARS IN QUALITY 15+" would claim the
   number was changing. */
.m-stat svg { display: block; margin-top: 9px; width: 100%; height: 11px; }
.m-stat .ln {
  fill: none; stroke: var(--m-acc); stroke-width: 1.2; vector-effect: non-scaling-stroke;
  opacity: .75;
}

/* ---- EXPERTISE MATRIX ----------------------------------------------------- */
/* label, track, percentage -- the desktop's three columns (`.mx`), on a 375px line.
   The label column is fixed so the eight tracks start on the same x, and 138px is
   MEASURED rather than chosen: the widest of the eight, "Mentoring & Coaching", is
   135px at 10.5px/.04em, and at 132 it wrapped to two lines and took its own bar out
   of the row it belongs to. The track is `minmax(0,1fr)` and not a width, so at 320 it
   is the track that gives and never the label. */
.m-mx {
  display: grid; grid-template-columns: 138px minmax(0, 1fr) 30px;
  align-items: center; gap: 10px;
}
.m-mx + .m-mx { margin-top: 11px; }
.m-mx .k { font-size: 10.5px; letter-spacing: .04em; color: #b3bdd1; }
.m-mx .t { height: 3px; background: var(--m-line); border-radius: 2px; overflow: hidden; }
/* the width is written into the markup by `m.js` and there is no transition on it: an
   animated fill is a value that is WRONG until it lands, and killing the animation for
   print or reduced motion is what leaves it at zero. */
.m-mx .t i { display: block; height: 100%; background: var(--m-acc); }
.m-mx .p { font-size: 9.5px; color: #8590a6; text-align: right; }

/* ---- projects ------------------------------------------------------------- */
/* Five entries stacked, separated by a rule rather than boxed: the mockup does it that
   way and five bordered boxes each holding a paragraph, five bullets and three tags
   would put four nested frames on one scroll. */
.m-pj + .m-pj { margin-top: 22px; padding-top: 22px; border-top: 1px solid var(--m-line); }
.m-pj .t {
  margin: 0; font-family: var(--m-disp); font-weight: 400;
  font-size: 15px; letter-spacing: .10em; color: var(--m-ink-b);
}
.m-pj .y { margin: 7px 0 0; font-size: 9.5px; letter-spacing: .13em; color: #8590a6; }
.m-pj .y i { font-style: normal; margin-left: 8px; color: #7d8699; }
.m-pj .y i.on { color: var(--m-acc); }
.m-pj .p { margin: 12px 0 0; font-size: 12px; line-height: 1.62; color: #b3bdd1; }
.m-pj .c { margin: 12px 0 0; padding: 0; list-style: none; }
.m-pj .c li {
  position: relative; padding-left: 16px; font-size: 11.5px; line-height: 1.58; color: #aab4c8;
}
.m-pj .c li + li { margin-top: 7px; }
/* the bullet is a pseudo-element and not a list marker, because `::marker` cannot be
   sized or coloured consistently across the three engines M4 has to pass under. */
.m-pj .c li::before {
  content: ''; position: absolute; left: 3px; top: .62em;
  width: 4px; height: 4px; border-radius: 50%; background: var(--m-acc);
}
.m-pj .f { margin: 13px 0 0; display: flex; flex-wrap: wrap; gap: 6px; }
/* `.tg` carries no style the bare `span` did not -- the class exists so the tag is
   NAMEABLE. `test-mobile-contrast`'s tier-1 list is a list of class tokens, and an
   element whose only selector is `span` cannot be put on it without matching half the
   tree; the alternative was to leave 15 pieces of visible copy outside the contrast
   floor, which is the hole its "TIER1 NOT MEASURED" line found. */
.m-pj .f .tg {
  padding: 4px 8px; border: 1px solid var(--m-line); border-radius: 2px;
  font-size: 8.5px; letter-spacing: .13em; color: #8f99ad;
}

/* ============================================================
   THEME, RESUME, CONTACT, GITHUB (M3)
   ============================================================

   One of the four has a mockup (`mobile-theme-page.png`); the other three are derived
   from its grammar -- accented `//` head, 1px bordered glass box on a 3px radius, a
   label/value pair, one full-width control at the foot.

   NOTHING HERE IS A `.m-fr`. Three boxes carry the notch (`#m-sys`, `#m-term`, `#m-cap`)
   and a fourth makes the chamfer the page's texture rather than its punctuation
   (MOBILE-SPEC 4a). The theme mockup agrees: its four rows and its foot are plain
   rounded rectangles, and the only chamfered box on the page is the chip bar.

   NOTHING HERE ANIMATES IN either -- no intro, nothing at opacity 0, no width starting
   at zero. `test-mobile` 9 is therefore still green without being extended, which is the
   third phase running that it has held. The one transition M3 adds is on `html.theming`,
   a STATE class that is on for 480ms during a switch and off the rest of the time. */

/* ---- the interface theme -------------------------------------------------- */
/* The intro line under the head, shared by theme and contacts: `DOC.themeIntro` and
   `DOC.contactIntro` are both two prose lines the desktop prints under its own heading. */
.m-intro { margin: 0 0 16px; font-size: 12px; line-height: 1.62; color: #b3bdd1; }

.m-th-list { display: block; }
/* Row geometry, MEASURED on the built page at 375: 335 x 100, which is the mockup's
   ~228px at the 852/375 = 2.272 scale. Grid rather than the desktop's absolute
   positioning: its rows are a fixed 708 x 74.6 inside a fixed panel, and here the sub
   line wraps at 320px, so a fixed height would clip "Eucalyptus / muted seafoam".

   FOUR columns, and the fourth is what the first version got wrong: with three, the
   swatch triad and the trace both landed in row 3 of the middle column and the trace and
   the ACTIVE label were painted ON TOP OF each other -- a defect that reads as a font
   rendering fault rather than as a grid one, because both were legible. The mockup puts
   the trace BESIDE the swatches and the label UNDER the radio, which is
   `auto | auto | 1fr | 74px`: number, the name/sub/swatch stack, the trace, the radio. */
.m-th-row {
  position: relative; width: 100%;
  display: grid; grid-template-columns: auto auto minmax(0, 1fr) 74px;
  align-items: center; column-gap: 12px; row-gap: 5px;
  padding: 13px 0 13px 14px; margin: 0 0 var(--m-gap);
  border: 1px solid var(--m-line); border-radius: 3px; background: var(--m-glass);
  font: inherit; text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* the divider the mockup draws down the row, left of the radio column. It is ONE
   absolutely-positioned rule rather than a `border-left` on the radio and another on the
   label: two borders on two grid items in two different rows leave a gap at the row gap,
   which reads as a dashed line nobody asked for. */
.m-th-row::after {
  content: ''; position: absolute; right: 74px; top: 14px; bottom: 14px;
  width: 1px; background: var(--m-line);
}
/* the whole row IS the tap target: 100px against the 44px 2.7 asks for, and the radio at
   the right is a mark rather than a second hit area. */
/* NOT `--m-faint`, and the difference is measured rather than felt: at #5b6478 over this
   row's own backdrop the number reads 3.31, and 19px at weight 400 is not "large" by
   2.7's rule (which needs 24px, or 18.66px at 700), so its floor is 4.5 and not 3. It is
   the smallest lift that clears it on THIS backdrop rather than one grey sized to the
   worst box on the tree -- the call Phase 4 made for every desktop lift, for the reason
   recorded above `.pj-y` there. `test-mobile-contrast` is what found it and what holds
   it: the row number is how the mockup indexes the list and a reader uses it to say
   "the third one". */
.m-th-row .n {
  grid-row: 1 / 3; align-self: start; margin-top: 1px;
  font-family: var(--m-disp); font-weight: 400; font-size: 19px; letter-spacing: .06em;
  color: #7d879c;
}
.m-th-row.on .n { color: var(--m-acc); }
/* the name and the sub run across the two middle columns; only the swatches and the
   trace divide them, on the third row. */
.m-th-row .nm {
  grid-column: 2 / 4;
  font-family: var(--m-mono); font-size: 12.5px; letter-spacing: .13em; color: var(--m-ink-b);
}
.m-th-row .sb { grid-column: 2 / 4; font-size: 10px; letter-spacing: .04em; color: #8590a6; }
/* THE SWATCH TRIAD. `--c` is a per-row inline hex out of `TH_SW` in `m.js`, so these
   three chips are the ONLY colours on this tree that do not come from the accent
   system -- they are a preview of a profile that is not applied yet, and a themed
   token would show all four rows in the current accent. */
.m-th-row .sw { grid-row: 3; grid-column: 2; display: flex; gap: 7px; margin-top: 8px; }
.m-th-row .sw i {
  width: 26px; height: 26px; border-radius: 3px; background: var(--c);
}
/* the trace, DETERMINISTIC (`sparks()` in m.js, one seeded stream for the four rows).
   It takes the row's own first swatch, not the accent, for the same reason the chips do:
   it is a preview. */
.m-th-row .sp {
  grid-row: 3; grid-column: 3; width: 100%; height: 20px; margin-top: 8px;
}
.m-th-row .sp .ln {
  fill: none; stroke: var(--c); stroke-width: 1.2; vector-effect: non-scaling-stroke;
}
/* the radio and its label, stacked at the right the way the mockup stacks them. The
   circle is a border and a dot rather than an `<input>`: the row is the control (it
   carries `role="radio"` and `aria-checked`), and a real input inside a button is not
   valid markup. */
.m-th-row .rd {
  grid-row: 1 / 3; grid-column: 4; justify-self: center;
  width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid var(--m-faint);
  display: flex; align-items: center; justify-content: center;
}
.m-th-row.on .rd { border-color: var(--m-acc); }
.m-th-row .rd i { width: 9px; height: 9px; border-radius: 50%; background: transparent; }
.m-th-row.on .rd i { background: var(--m-acc); }
.m-th-row .st {
  grid-row: 3; grid-column: 4; justify-self: center;
  font-size: 9px; letter-spacing: .13em; color: #8590a6;
}
.m-th-row.on { border-color: rgba(var(--aff4e93),.55); }
.m-th-row.on .st { color: var(--m-acc); }
.m-th-row:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: 2px; }

/* the foot. TRANSITION // SMOOTH 480MS is a LABEL and not a toggle, which is a decision
   and not an omission: the mockup draws a switch beside it identical to AMBIENT SYNC's,
   the desktop has no such control, and a switch that does nothing is invented UI
   (MOBILE-PLAN, 2026-08-22). */
.m-th-foot {
  margin-top: 4px;
  border: 1px solid var(--m-line); border-radius: 3px; background: rgba(255,255,255,.022);
}
.m-th-foot .lab, .m-th-foot .tog {
  display: flex; align-items: center; gap: 0;
  width: 100%; min-height: var(--m-tap); margin: 0; padding: 0 14px;
  font: inherit; font-size: 10.5px; letter-spacing: .13em; color: #b3bdd1;
  background: none; border: 0; text-align: left;
}
.m-th-foot .tog { cursor: pointer; border-top: 1px solid var(--m-line); }
.m-th-foot em { font-style: normal; margin: 0 6px; color: var(--m-acc); }
.m-th-foot b { font-weight: 400; color: var(--m-ink-b); }
.m-th-foot .tog:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: -2px; }
/* the state dot the mockup puts at the left of both foot rows */
.m-th-foot .lab::before, .m-th-foot .tog::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%; margin-right: 11px;
  background: var(--m-acc); flex: none;
}
.m-th-foot .tog[aria-pressed="false"]::before { background: var(--m-faint); }

.m-th-reset {
  width: 100%; min-height: var(--m-tap); margin-top: var(--m-gap); padding: 0 14px;
  display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: 10.5px; letter-spacing: .16em; color: #e2e8f5;
  border: 1px solid var(--m-line); border-radius: 3px; background: rgba(255,255,255,.022);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.m-th-reset:active { background: rgba(var(--aff4e93),.10); }
.m-th-reset:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: -2px; }

/* THE 480MS THE MODULE PROMISES. A state class rather than a standing transition: `m.js`
   puts `theming` on <html> for the length of a switch and takes it off again, so nothing
   on this tree is animating while a reader scrolls. It is NOT an intro -- nothing sits at
   opacity 0 waiting to land -- which is why the print list needs nothing for it and the
   reduced-motion list only has to switch it off.

   THE LIST IS THE DESKTOP'S, ALL NINE PROPERTIES, and `opacity` is the one that must not
   be dropped from it. This selector is (1,1,1) and `#m-bg i` -- the cross-fade layer,
   which lives INSIDE `#m` -- is (1,0,1), so during a switch this rule REPLACES the
   layer's `transition: opacity var(--th-ms)` wholesale. Written without `opacity` it
   took the fade away for exactly the 520ms the fade was supposed to run in, and the
   background snapped between profiles instead. MEASURED on /theme/: the layer's computed
   `transition-property` read `opacity` before a tap, `color, background-color,
   border-color, fill, stroke, box-shadow` 120ms after one, and `opacity` again at 820ms.
   No harness could see it -- every one of them pins `*{transition:none}` and software
   rendering never advances a transition anyway. `v2/styles.css:98` carries the same nine
   for the same reason; keep the two lists identical. */
html.theming #m *, html.theming #m *::before, html.theming #m *::after {
  transition: color var(--th-ms, 480ms) ease, background-color var(--th-ms, 480ms) ease,
              border-color var(--th-ms, 480ms) ease, fill var(--th-ms, 480ms) ease,
              stroke var(--th-ms, 480ms) ease, box-shadow var(--th-ms, 480ms) ease,
              text-shadow var(--th-ms, 480ms) ease, filter var(--th-ms, 480ms) ease,
              opacity var(--th-ms, 480ms) ease;
}

/* ---- resume, github: the card and the one control ------------------------- */
/* Shared, because they are the same shape: a bordered box of facts and a full-width
   action under it. The mockup grammar's own box (`.m-quote.box`, `.m-row`), at the size
   the theme rows use. */
.m-card {
  padding: 16px 16px 14px;
  border: 1px solid var(--m-line); border-radius: var(--m-r); background: var(--m-glass);
}
.m-card .badge {
  display: inline-block; margin: 0; padding: 4px 8px;
  border: 1px solid rgba(var(--aff4e93),.45); border-radius: 2px;
  font-size: 8.5px; letter-spacing: .16em; color: var(--m-acc);
}
/* the file name is the longest string on either page: "SVYATOSLAV_MARUTYAK_RESUME.PDF"
   is 30 characters and measures 208px at 10.5px/.04em against 295px of card. It does not
   wrap, and `overflow-wrap` is what stops it widening the page if it ever grows -- a
   long unbroken token is the one string a flex or grid minimum cannot shrink. */
.m-card .file {
  margin: 13px 0 0; font-size: 10.5px; letter-spacing: .04em; color: var(--m-ink-b);
  overflow-wrap: anywhere;
}
.m-card .meta { margin: 9px 0 0; font-size: 9.5px; letter-spacing: .1em; color: #8590a6; }
.m-card .meta em { font-style: normal; margin: 0 5px; color: var(--m-faint); }
.m-card .state {
  margin: 13px 0 0; display: flex; align-items: center; gap: 9px;
  font-size: 9.5px; letter-spacing: .13em; color: var(--m-acc);
}
.m-card .handle {
  margin: 7px 0 0; font-size: 13px; letter-spacing: .08em; color: var(--m-acc);
}
.m-card .blurb { margin: 13px 0 0; font-size: 12px; line-height: 1.62; color: #b3bdd1; }

/* the one control per page, the mockup's RESET DEFAULT bar in the accent. Both are real
   anchors -- `<a download>` on resume, an external link on github -- so a long press
   offers the phone's own menu and neither depends on a click handler. */
.m-act {
  display: flex; align-items: center; justify-content: center;
  min-height: var(--m-tap); margin-top: 16px; padding: 0 14px;
  border: 1px solid rgba(var(--aff4e93),.45); border-radius: 3px;
  background: rgba(var(--aff4e93),.10);
  font-size: 11px; letter-spacing: .18em; color: rgb(var(--affd7e6)); text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.m-act:active { background: rgba(var(--aff4e93),.18); }
.m-act:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: 2px; }

/* ---- contact -------------------------------------------------------------- */
/* The desktop's two rows stacked, label over value with the action under both. The
   e-mail value is `overflow-wrap: anywhere` for the same reason the file name is. */
.m-row-l {
  display: block; padding: 13px 14px; margin-bottom: var(--m-gap);
  border: 1px solid var(--m-line); border-radius: 3px; background: var(--m-glass);
  text-decoration: none; -webkit-tap-highlight-color: transparent;
}
.m-row-l .k { display: block; font-size: 8.5px; letter-spacing: .16em; color: #8590a6; }
.m-row-l .v {
  display: block; margin-top: 7px; font-size: 12.5px; letter-spacing: .02em;
  color: var(--m-ink-b); overflow-wrap: anywhere;
}
.m-row-l .a { display: block; margin-top: 10px; font-size: 9px; letter-spacing: .16em; }
.m-row-l .a em { font-style: normal; color: var(--m-acc); }
/* the copy receipt: ADDRESS COPIED replaces SEND MESSAGE for 2.2s, the desktop's own
   two-state span (`.a-rest` / `.a-copy`). No layout change -- one is `display:none` and
   the other is not, both in the same box -- so the row does not jump under the finger. */
.m-row-l .a-copy { display: none; }
.m-row-l.copied .a-rest { display: none; }
.m-row-l.copied .a-copy { display: inline; }
.m-row-l:active { background: rgba(var(--aff4e93),.08); }
.m-row-l:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: 2px; }
.m-stat-l {
  margin: 16px 0 0; display: flex; align-items: center; gap: 9px;
  font-size: 9.5px; letter-spacing: .13em; color: #8590a6;
}
.m-stat-l em { font-style: normal; color: var(--m-faint); }

/* ==== STATUS (M4) ==========================================================
   The seventh body, and the only one that moves. No mockup exists for it, so every box
   below is one of the four shapes M2 and M3 already established -- the accented `//`
   head, the bordered glass box on a 3px radius, a label over a value, one full-width
   control at the foot -- and nothing here is a new idea about what this site looks like.

   NOTHING IS NOTCHED. `.m-fr` is `#m-sys`, `#m-term` and `#m-cap` and no more; a fourth
   makes the chamfer a texture rather than a mark. Nothing animates in, nothing sits at
   opacity 0, and no width starts at zero -- so the reduced-motion list at the bottom of
   this file is unchanged and `test-mobile` 9 stays green.

   Every class here is prefixed `m-st-` on purpose: `test-mobile-contrast` matches tier-1
   selectors by CLASS TOKEN, not by CSS selector, so a status label called `.v` or `.st`
   would silently join an existing token's measurements and could drop an unrelated
   line's worst case with nothing naming what moved. */

/* ---- mission, state, flag ---- */
.m-st-mi {
  margin: 14px 0 0; font-family: var(--m-disp);
  font-size: 21px; letter-spacing: .06em; color: var(--m-ink-b);
}
.m-st-sta {
  margin: 9px 0 0; display: flex; align-items: center; gap: 9px;
  font-size: 10.5px; letter-spacing: .16em; color: var(--m-acc);
}
.m-st-pl {
  margin: 18px 0 0; font-size: 9.5px; letter-spacing: .14em; color: #8f99ad;
}
.m-st-pl em { font-style: normal; margin: 0 5px; color: var(--m-faint); }
.m-st-pv { color: var(--m-ink-b); font-weight: 400; }
/* 57 flexed segments, the desktop's construction and its reason: the lit count is a
   percentage of the whole bar, so the bar ends on the content edge whatever the segment
   maths rounds to -- which is what lets one number drive it at 335px and at 280px. The
   width is written into the markup, never transitioned to. */
.m-st-bar {
  margin-top: 8px; display: flex; gap: 1px; height: 9px;
}
.m-st-bar i { flex: 1 1 0; background: rgba(168,186,224,.16); }
.m-st-bar i.lit { background: var(--m-acc); }
/* the flag strip, the desktop's chamfered chip flattened to a bordered line: at 33
   characters the longest flag needs the width more than it needs the chamfer, and it
   wraps to two lines rather than clipping. */
.m-st-fg {
  margin: 12px 0 0; padding: 8px 10px;
  border: 1px solid rgba(var(--aff4e93),.34); border-radius: 3px;
  background: rgba(var(--aff4e93),.07);
  font-size: 9.5px; line-height: 1.5; letter-spacing: .13em; color: rgb(var(--affc2d8));
}

/* ---- the step rail ---- */
/* The desktop draws its connector, rings and arms as one SVG in panel coordinates. Here
   the rail is CSS: a 1px rule down the marker column, drawn by each row except the last,
   and a ring per row. Same reading, no coordinate space to keep in sync with a layout
   that is a flow rather than a fixed 804x556 box. */
.m-st-rail { margin-top: 12px; }
.m-st-step {
  position: relative; display: grid; grid-template-columns: 26px minmax(0, 1fr) auto;
  align-items: center; column-gap: 10px; min-height: 38px;
}
/* the connector: every row but the last runs a rule from its own marker to the next */
.m-st-step:not(:last-child)::before {
  content: ""; position: absolute; left: 12.5px; top: 50%; height: 100%;
  border-left: 1px solid rgba(168,186,224,.20);
}
.m-st-step.done:not(:last-child)::before { border-left-color: rgba(var(--aff4e93),.40); }
.m-st-step .mk {
  position: relative; z-index: 1; width: 17px; height: 17px; margin: 0 auto;
  border: 1px solid rgba(168,186,224,.30); border-radius: 50%;
}
.m-st-step .mk::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 5px; height: 5px; border-radius: 50%; background: transparent;
}
.m-st-step.done .mk { border-color: rgba(var(--aff4e93),.55); }
.m-st-step.done .mk::after { background: var(--m-acc); width: 4px; height: 4px; }
.m-st-step.live .mk { border-color: var(--m-acc); }
.m-st-step.live .mk::after { background: var(--m-acc); width: 8px; height: 8px; }
.m-st-sl { font-size: 10.5px; letter-spacing: .05em; color: #b3bdd1; }
.m-st-step.live .m-st-sl { color: var(--m-ink-b); }
.m-st-sv {
  font-size: 8.5px; letter-spacing: .14em; color: #8f99ad; white-space: nowrap;
}
.m-st-step.live .m-st-sv { color: var(--m-acc); }

/* ---- the bus ---- */
/* Full-width, and the five pips sit at the same even 20.64% pitch the desktop's ST_PIP
   describes in pixels. HTML rather than SVG: a `viewBox` that stretched to the strip's
   width would need `preserveAspectRatio: none`, which turns every pip into an ellipse. */
.m-st-bus {
  position: relative; height: 22px; margin-top: 12px;
}
.m-st-bus .ln {
  position: absolute; left: 0; right: 0; top: 50%;
  border-top: 1px solid rgba(168,186,224,.20);
}
.m-st-pip {
  position: absolute; top: 50%; width: 11px; height: 11px; margin: -5.5px 0 0 -5.5px;
  border-radius: 50%; background: rgb(var(--ag)); opacity: .42;
}
.m-st-pip.hot { opacity: 1; }
.m-st-pkt {
  position: absolute; top: 50%; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 1px; background: rgb(var(--ag)); opacity: 0;
}
/* TWO ROWS, even indices above odd. Five names at 11px are 38 characters and do not fit
   one row at 320px; staggering them clears both rows without shortening a name. Each tag
   is centred on its pip, and the two at the ends are pulled back inside the strip. */
.m-st-tags { position: relative; height: 34px; margin-top: 3px; }
.m-st-tg {
  position: absolute; transform: translateX(-50%); white-space: nowrap;
  font-size: 11px; letter-spacing: .04em; color: rgb(var(--ag)); opacity: .62;
}
.m-st-tg.r0 { top: 0; }
.m-st-tg.r1 { top: 17px; }
.m-st-tg.hot { opacity: 1; }
.m-st-tg[data-i="0"] { left: 0 !important; transform: none; }
.m-st-tg[data-i="4"] { left: auto !important; right: 0; transform: none; }

/* ---- the log ---- */
/* The sender column is space-padded to the longest agent name and needs `white-space:
   pre` to keep that padding, which means the WHOLE line has to fit or it hangs out of
   the box. The budget is 46 characters -- `RESEARCHER` (10) plus " > " plus
   `content.mjs`'s 33-character message ceiling -- and this mono advances .615em per
   character including the tracking, MEASURED on the built page rather than assumed.
   64px is the page's two 20px pads plus the box's two 12px ones, so `100vw - 64px` is
   the line's own width: 311px at 375 and 256px at 320.

   A first cut at `min(11px, 3.1vw)` scaled the TYPE with the viewport and not with the
   space left after a fixed 64px of padding, which is a bigger fraction of a 320px phone:
   it read 9.92px and overflowed the box by 14px at 320 while measuring clean at 375. A
   second `@media` in this file would be a width at which a phone applies the mobile
   stylesheet and downloads the desktop bundle, so the fit is done in one `min()`. */
.m-st-log {
  margin-top: 14px; padding: 12px 12px 11px;
  border: 1px solid var(--m-line); border-radius: 3px; background: var(--m-glass);
}
.m-st-ln {
  font-size: min(11px, calc((100vw - 64px) / 29));
  line-height: 1.68; letter-spacing: .015em;
  color: #b3bdd1; white-space: pre;
}
.m-st-ln .arw { font-style: normal; color: var(--m-faint); }
.m-st-who { color: rgb(var(--ag)); }
/* The index IS the agent IS the hue, the feed's rule (test-content 3) against this
   tree's second consumer of it. Scoped, and the fallback is INSIDE the `var()`: a
   custom property declared on an id is (1,0,1) and beats `html[data-theme]`'s (0,1,1),
   which is the M1 finding that defeated the theme on the feed. */
.m-st-log .ag0, .m-st-bus .ag0, .m-st-tags .ag0 { --ag: var(--ag-1, 246,147,179); }
.m-st-log .ag1, .m-st-bus .ag1, .m-st-tags .ag1 { --ag: var(--ag-2, 231,168,85); }
.m-st-log .ag2, .m-st-bus .ag2, .m-st-tags .ag2 { --ag: var(--ag-3, 131,202,130); }
.m-st-log .ag3, .m-st-bus .ag3, .m-st-tags .ag3 { --ag: var(--ag-4, 58,201,232); }
.m-st-log .ag4, .m-st-bus .ag4, .m-st-tags .ag4 { --ag: var(--ag-5, 178,169,255); }

/* ---- consensus, the button, the incident footer ---- */
.m-st-cl {
  margin: 16px 0 0; font-size: 9.5px; letter-spacing: .14em; color: #8f99ad;
}
.m-st-cl em { font-style: normal; margin: 0 5px; color: var(--m-faint); }
.m-st-cv { font-size: 15px; letter-spacing: .04em; color: var(--m-ink-b); font-weight: 400; }
/* REQUEST CONSENSUS is `.m-act`, resume's and github's control -- the mockup grammar's
   one full-width action at the foot, and the only one on this tree that is a <button>
   rather than an anchor, because it acts on the page instead of leaving it. */
#m-st-btn { width: 100%; font-family: var(--m-mono); }
#m-st-btn.busy { opacity: .62; }
#m-st-btn[disabled] { pointer-events: none; }
.m-st-ft {
  margin: 14px 0 0; font-size: 9px; line-height: 1.6; letter-spacing: .13em;
  color: #8f99ad;
}
.m-st-ft em { font-style: normal; margin: 0 5px; color: var(--m-faint); }

/* ---- prev / next ---------------------------------------------------------- */
/* The mockup's bottom bar, and it is the home screen's split box again (`.m-split`):
   one frame around two controls with a rule down the middle. It is IN FLOW rather than
   fixed -- a bar pinned to the bottom of a phone costs 46px of a 812px viewport on
   every screen, and the About mockup shows it after the content with the page ended
   under it.

   The UPLINK footer is hidden while a module is open (m.js), so this is the page's
   bottom furniture, which is what both module mockups show. */
#m-pn {
  margin-top: 30px; display: flex;
  border: 1px solid var(--m-line); border-radius: 3px; background: rgba(255,255,255,.022);
}
.m-pn-l {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  min-height: var(--m-tap); padding: 0 10px;
  font-size: 10.5px; letter-spacing: .16em; color: #e2e8f5; text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
.m-pn-l + .m-pn-l { border-left: 1px solid var(--m-line); }
.m-pn-l i { font-style: normal; font-size: 13px; line-height: 1; color: var(--m-acc); }
.m-pn-l:active { background: rgba(var(--aff4e93),.10); }
.m-pn-l:focus-visible { outline: 2px solid var(--m-acc-s); outline-offset: -2px; }

/* ---- footer --------------------------------------------------------------- */
#m-ftr {
  position: relative; z-index: 2;
  flex: none; display: flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 44px; padding: 0 var(--m-pad);
  border-top: 1px solid var(--m-line); background: rgba(4,5,12,.72);
}
#m-ftr p { margin: 0; font-size: 10.5px; letter-spacing: .2em; color: var(--m-dim); }

/* PRODUCTION-SPEC.md 2.7. Extend BOTH lists for any element that gains an intro
   animation, or the module renders empty rather than un-animated. */
@media (prefers-reduced-motion: reduce) {
  #m-bg i { transition: none; }
  /* M3. `m.js` already skips the class under `reduced` -- this is the second half, for
     the reader who turns the preference ON while the site is open, where the media query
     re-evaluates and the JS constant does not. Both halves, because either alone is a
     window. */
  html.theming #m *, html.theming #m *::before, html.theming #m *::after { transition: none; }
}
/* M1 adds no intro animation and nothing at opacity 0 -- the home screen is the first
   thing a reader sees and there is no state to land. So this list hides the two things
   that are meaningless on paper (the live dots and the sparklines) and nothing has to be
   PARKED. `test-mobile` asserts it by looking for text under opacity .05 under both
   media, which is `test-print`'s check in the shape this tree needs. */
@media print {
  #m-bg, #m-veil, #m-hdr .m-nav, #m-ftr { display: none !important; }
  .m-live, .m-tc svg { display: none !important; }
  /* M2's three, and they are here because the rule above already decided the principle
     rather than because a test asked: `.m-stat svg` is a SPARKLINE and `.m-tc svg` does
     not match it (different cell, different class), `#m-pn` is NAVIGATION and
     `#m-hdr .m-nav` is hidden for exactly that reason, and `.m-prog` is a 2px accent bar
     with no label, which is the definition of meaningless on paper. Printing /about/
     without these emits four decorative squiggles under the stat values and a
     `< TERMINAL | PROJECTS >` bar at the foot of the sheet.
     `test-mobile` 9 CANNOT catch this: it looks for text at opacity 0, and none of the
     three is text. Extend this list by reading it, not by waiting for a gate. */
  .m-stat svg, #m-pn, .m-prog { display: none !important; }
  /* M3's four, read off the same principle rather than off a failing gate. The theme
     row's TRACE and its RADIO are the `.m-stat svg` case again -- a decorative squiggle
     and a state mark that means nothing once the page cannot be tapped; and the two
     ACTIONS (`.m-act` on resume and github, `.m-th-reset`) are the `#m-pn` case: a
     printed button is a rectangle around a word with nothing behind it.
     WHAT STAYS, deliberately: the swatch chips (they are the profile, and a printed
     theme page with no colour in it is a list of four names), the value in every
     `.m-row-l` -- an e-mail address and a profile URL are the two things on this whole
     tree somebody would print ON PURPOSE -- and every label. */
  .m-th-row .sp, .m-th-row .rd, .m-act, .m-th-reset { display: none !important; }
  /* M4's three, read off the same principle again. The BUS and its packet are the
     `.m-stat svg` case at its purest -- a rail, five dots and a square that only means
     anything while it is moving, and on paper it is a row of dots over a row of names
     that the log already names on every line. The step rail's MARKER is the `.m-th-row
     .rd` case: a state mark whose ring/dot/live distinction is carried in words by the
     `.m-st-sv` beside it. And REQUEST CONSENSUS is already covered -- it is `.m-act`,
     listed above, which is why the button needs no entry of its own here.
     WHAT STAYS, deliberately: the mission BAR (it is the one number on the page a
     printed copy would be read for, and unlike a sparkline it has a percentage beside
     it), every step label and state, all five log lines with their senders, the flag and
     the incident footer. `test-mobile` 9 cannot catch any of this -- it looks for text at
     opacity 0 and none of the three is text. Extend this list by reading it. */
  .m-st-bus, .m-st-tags, .m-st-step .mk { display: none !important; }
  /* the connector rule is the other half of the marker and is a pseudo-element, so it
     survives its own row's `.mk` being hidden and prints as a bare line down the margin */
  .m-st-step::before { display: none !important; }
  /* ...AND THE ROW HAS TO LOSE ITS MARKER COLUMN WITH IT. `display: none` on a GRID ITEM
     does not leave a hole -- it removes the item, and the two remaining children slide
     back into `26px` and `minmax(0,1fr)`. MEASURED on the first print of this page: every
     step label was squeezed into 26px, wrapped to two lines, and the state printed ON TOP
     of it. This is the one entry in either list that is not simply a `display: none`, and
     it is why the print sheet has to be LOOKED AT rather than reasoned about: `test-mobile`
     9 checks for text at opacity 0 and a legible-but-overlapping row is not that. */
  .m-st-step { grid-template-columns: minmax(0, 1fr) auto !important; }
  #m { color: #000; }
}

/* ---- accent profiles (generated by scripts/theme-build.mjs) ---------------- */
:root{
  --aff4e93: 255,78,147;
  --aff5c9e: 255,92,158;
  --aff8cba: 255,140,186;
  --affc2d8: 255,194,216;
  --affd7e6: 255,215,230;
}
html[data-theme="pink"]{
  --aff4e93: 255,78,147;
  --aff5c9e: 255,92,158;
  --aff8cba: 255,140,186;
  --affc2d8: 255,194,216;
  --affd7e6: 255,215,230;
  --sig-fn: 130,216,255;
  --sig-num: 247,178,103;
  --ag-1: 246,147,179;
  --ag-2: 231,168,85;
  --ag-3: 131,202,130;
  --ag-4: 58,201,232;
  --ag-5: 178,169,255;
}
html[data-theme="mint"]{
  --aff4e93: 73,200,172;
  --aff5c9e: 92,204,174;
  --aff8cba: 146,217,192;
  --affc2d8: 199,233,220;
  --affd7e6: 219,241,231;
  --sig-fn: 130,216,255;
  --sig-num: 247,178,103;
  --ag-1: 75,207,176;
  --ag-2: 112,189,255;
  --ag-3: 215,156,233;
  --ag-4: 251,150,134;
  --ag-5: 198,184,82;
}
html[data-theme="amber"]{
  --aff4e93: 213,157,66;
  --aff5c9e: 217,162,79;
  --aff8cba: 229,184,125;
  --affc2d8: 239,216,186;
  --affd7e6: 245,229,210;
  --sig-fn: 130,216,255;
  --sig-num: 134,216,152;
  --ag-1: 228,170,83;
  --ag-2: 126,203,135;
  --ag-3: 63,200,235;
  --ag-4: 182,167,255;
  --ag-5: 247,147,174;
}
html[data-theme="blue"]{
  --aff4e93: 61,152,225;
  --aff5c9e: 68,162,225;
  --aff8cba: 115,196,233;
  --affc2d8: 143,222,254;
  --affd7e6: 205,236,248;
  --sig-fn: 156,222,169;
  --sig-num: 247,178,103;
  --ag-1: 115,188,255;
  --ag-2: 217,155,231;
  --ag-3: 251,151,132;
  --ag-4: 195,185,83;
  --ag-5: 72,207,178;
}
