/* ==========================================================================
   icybearOS — desktop + phone shell.
   Extends style.css and tokens.css. Every custom property lives in tokens.css,
   which the chart page loads too; .holo-surface, .pill, .button, .grid-overlay,
   .sparkle and the cursors come from style.css. Nothing here declares a token.
   ========================================================================== */



/* theme picker swatches */
.swatch[data-swatch="base"] .swatch__chip {
  background: linear-gradient(120deg, #e8d5f7 0%, #d3d3f9 50%, #c0d8f8 100%);
  box-shadow: inset 0 0 0 1.5px #e07bb866;
}
.swatch[data-swatch="base"] .swatch__chip::after {
  background: linear-gradient(160deg, #ffffff 0%, #f6b8e4 33%, #a98ee8 67%, #8fc2f4 100%);
}
.swatch[data-swatch="holo"] .swatch__chip {
  background: linear-gradient(120deg, #fbe6f6 0%, #dfeafd 50%, #dffbf1 100%);
  box-shadow: inset 0 0 0 1.5px #7fe6ff66;
}
.swatch[data-swatch="holo"] .swatch__chip::after {
  background: linear-gradient(160deg, #ffffff 0%, #ffd4f0 25%, #c4f0ff 50%, #d9ffe8 75%, #e0d0ff 100%);
}
.swatch[data-swatch="strawberry"] .swatch__chip {
  background: linear-gradient(120deg, #fde6f1 0%, #fbd7e7 50%, #f6dcf5 100%);
  box-shadow: inset 0 0 0 1.5px #f28fc466;
}
.swatch[data-swatch="strawberry"] .swatch__chip::after {
  background: linear-gradient(160deg, #ffffff 0%, #ffc9e2 33%, #f0a0cc 67%, #ffd9ec 100%);
}
.swatch[data-swatch="arcade"] .swatch__chip {
  background: linear-gradient(120deg, #221c4a 0%, #2b2159 50%, #1c2a5c 100%);
  box-shadow: inset 0 0 0 1.5px #57e8ff66;
}
.swatch[data-swatch="arcade"] .swatch__chip::after {
  background: linear-gradient(160deg, #ffffff 0%, #7de3ff 33%, #b58ded 67%, #ff8ad8 100%);
}
.swatch[data-swatch="archangel"] .swatch__chip {
  background: linear-gradient(120deg, #f6ecff 0%, #e7d3fb 50%, #f2e6ff 100%);
  box-shadow: inset 0 0 0 1.5px #cba9f466;
}
.swatch[data-swatch="archangel"] .swatch__chip::after {
  background: linear-gradient(160deg, #ffffff 0%, #f4eaff 33%, #cba9f4 67%, #8b5cd0 100%);
}


/* --------------------------------------------------------------------------
   2. BASE
   -------------------------------------------------------------------------- */

/* Scoped to .os, which only the desktop's body carries. The chart loads this
   file to borrow the info window's styles, and an unscoped `overflow: hidden`
   would freeze a page that is meant to scroll. Everything else in this file is
   either scoped by a data- attribute or hangs off an id the chart does not
   have, so this was the only rule standing between the two pages. */
html:has(body.os), body.os { height: 100%; overflow: hidden; }

/* any class-level display rule outranks the UA stylesheet's [hidden], which is
   how the portfolio filter silently did nothing. hidden means hidden. */
[hidden] { display: none !important; }

body.os {
  background: var(--os-sky) fixed;
  color: var(--os-ink);
  /* the shared .button from style.css themes itself through these */
  --button-top: var(--t-cta-top);
  --button-bottom: var(--t-cta-bottom);
  user-select: none;
  transition: background var(--motion-slow) var(--ease-standard), color var(--motion-enter) var(--ease-standard);
}
/* one constant for the brand, one reactive to prove the world is alive */
body.os[data-mode="night"] { cursor: url('images/cursor-sparkle-night.svg') 15 15, auto; }

body.os input, body.os textarea, body.os [contenteditable] { user-select: text; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}


/* Prismatic, not gold. Gold belongs to the covenant hour and nothing else, and
   a warm metal was a foreign object in a palette this cool anyway. This is the
   same layered-shadow construction as the base wordmark, split into a
   cyan/magenta chromatic offset with a bloom behind it. */



/* --------------------------------------------------------------------------
   KONAMI MODE — a mode in the weather sense: it does not replace the theme or
   the sky, it layers over whichever of the twenty combinations you are already
   in. So nothing here sets a colour outright; each theme names its own phosphor
   and its own scanline weight, and every effect is written against those two
   tokens. A sixth theme would need two lines here and nothing else.

   Not a theme on purpose. Themes are the badge ladder's currency, earned at
   0/3/6/9/13, and the code is found rather than earned. It also already pays
   into that ladder by granting `unc gamer`, and an angel-coded sixth theme
   would sit straight on top of archangel and dilute the thirteenth.
   -------------------------------------------------------------------------- */

body[data-konami] {
  --k-phos: #7de3ff;        /* the tube's colour */
  --k-scan: 0.16;           /* how hard the scanlines bite */
  --k-roll: 0.1;
}
/* holo already refracts, so its tube is ice rather than a second hue */
body[data-wall="holo"][data-konami]       { --k-phos: #c4f0ff; --k-scan: 0.13; }
body[data-wall="strawberry"][data-konami] { --k-phos: #ff8ad8; --k-scan: 0.14; }
/* midnight arcade runs cyan and magenta already; pushing further in the same
   direction would read as more of the same, so its tube goes green phosphor */
body[data-wall="arcade"][data-konami]     { --k-phos: #7dffbe; --k-scan: 0.22; --k-roll: 0.16; }
body[data-wall="archangel"][data-konami]  { --k-phos: #e6d9ff; --k-scan: 0.12; }
/* a dark sky needs a lighter touch: the same scanline reads twice as heavy */
body[data-mode="night"][data-konami]      { --k-scan: 0.12; }

#crt {
  position: fixed;
  inset: 0;
  z-index: var(--z-crt);
  display: none;
  pointer-events: none;
  overflow: hidden;
}
body[data-konami] #crt { display: block; }
#crt::before {
  content: '';
  position: absolute;
  inset: -20%;
  background-image: repeating-linear-gradient(180deg,
    rgba(0, 0, 0, var(--k-scan)) 0 1px, transparent 1px 3px);
}
/* the phosphor sits in the corners, the way a tube is brightest in the middle */
#crt::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(78% 62% at 50% 46%,
    transparent 40%, color-mix(in srgb, var(--k-phos) 26%, transparent) 100%);
}
/* one refresh bar, slow enough to notice and not so fast it nags */
.crt__roll {
  position: absolute;
  left: 0;
  right: 0;
  height: 22%;
  background: linear-gradient(180deg, transparent,
    color-mix(in srgb, var(--k-phos) calc(var(--k-roll) * 100%), transparent), transparent);
  animation: crt-roll 6.5s var(--ease-linear) infinite;
}
@keyframes crt-roll {
  0%   { transform: translateY(-30vh); }
  100% { transform: translateY(115vh); }
}

/* the 1UP chip: the mode is visible, and clicking it is how you get out
   without having to remember the code twice */
#kchip {
  border: none;
  cursor: pointer;
  padding:  var(--space-hair) var(--space-md);
  border-radius: var(--radius-pill);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wide);
  color: var(--t-bar-ink);
  background: rgba(125, 227, 255, 0.4);
  background: color-mix(in srgb, var(--k-phos, #7de3ff) 40%, transparent);
  box-shadow: inset 0 0 0 1px rgba(125, 227, 255, 0.75);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--k-phos, #7de3ff) 75%, transparent);
  animation: kchip-blink 1.5s steps(2) infinite;
}
@keyframes kchip-blink { 50% { opacity: 0.55; } }

/* the falling glyphs become the arcade sprite: text hearts next to a CRT read
   as emoji, and this is the one moment that should look 8-bit */
body[data-konami] .halo {
  width: 18px;
  height: 18px;
  background-image: var(--motif-1);
  background-repeat: no-repeat;
  background-size: contain;
  filter: drop-shadow(0 0 7px var(--k-phos));
}
/* the pixel heart falls flat and never rotates — a spinning sprite is a blurry
   sprite, which is the same rule the arcade wallpaper motifs follow */
body[data-konami] .halo--pix {
  image-rendering: pixelated;
  animation-name: halo-drop;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 12' shape-rendering='crispEdges'%3E%3Cg fill='%23ff8ad8'%3E%3Crect x='2' y='1' width='4' height='1'/%3E%3Crect x='8' y='1' width='4' height='1'/%3E%3Crect x='1' y='2' width='12' height='3'/%3E%3Crect x='2' y='5' width='10' height='2'/%3E%3Crect x='3' y='7' width='8' height='1'/%3E%3Crect x='4' y='8' width='6' height='1'/%3E%3Crect x='5' y='9' width='4' height='1'/%3E%3Crect x='6' y='10' width='2' height='1'/%3E%3C/g%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) {
  /* .os-chrome was deleted on 23 Aug 2026 (nothing in any page carried the
     class); this override outlived it and matched nothing. */
  .crt__roll, #kchip { animation: none; }
}

/* The system glyph. Every theme punctuates with the sparkle; archangel
   punctuates with the covenant's own mark, and the whole OS changes what it
   is saying, everywhere at once, without touching a single string. */
.glyph::after { content: var(--t-glyph, "\2726"); }

.os-mono {
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wider);
  text-transform: uppercase;
  color: var(--os-accent);
}

/* window-scale prose helpers */
.mini { margin-top:  var(--space-md); font: 700 var(--text-body)/var(--lh-body) var(--font-body); color: var(--os-ink-dim); }
.wbody b { color: var(--os-ink); }

/* Was a fixed pink-on-purple for all twenty theme x weather combinations,
   which on midnight arcade put dark purple on pink over a near-black desktop.
   --t-deco is always a light pastel, so the ink is always the dark one. */
::selection { background: var(--os-selection); color: var(--os-selection-ink); }
:focus-visible {
  /* Two rings, and both from theme tokens. A fixed brand purple vanished on
     midnight arcade and archangel night, which are the two palettes a keyboard
     visitor is most likely to be using deliberately. The inner ring is the
     accent, the outer is the surface, so one of the two always contrasts
     whatever it lands on. */
  outline: 2px solid var(--os-accent);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--os-surface);
}

@media (prefers-reduced-motion: reduce) {
  #motifs { display: none; }
  #sky::before, #sky::after { animation: none !important; }
}











/* ==== ARCHANGEL PULSE =====================================================
   Live: HEARTBEAT. Every glass surface beats lub-dub and then rests, at 2.6s
   (13/5) — around 23bpm, slow enough to read as calm rather than busy. The sky
   keeps breathing at 6.5s underneath, so the machine has a pulse and a breath
   at different rates, which is the right way round.

   The glow is a token, so the covenant hour turns the heartbeat gold without
   restating the animation.

   To go back to the travelling wave: comment this block out, uncomment the one
   below it. Nothing else changes. */
body[data-wall="archangel"] { --pulse: rgba(203, 169, 244, 0.8); }
body[data-wall="archangel"][data-covenant-hour] { --pulse: rgba(255, 236, 176, 0.92); }

body[data-wall="archangel"] .glass::after {
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 30px var(--pulse);
  opacity: 0.26;
  animation: heartbeat 2.6s var(--ease-standard) infinite;  /* 13 / 5 */
}
/* opacity only, so the compositor can carry it on every glass surface at once */
@keyframes heartbeat {
  0% { opacity: 0.26; }
  7% { opacity: 1; }        /* lub */
  15% { opacity: 0.34; }
  23% { opacity: 0.78; }    /* dub */
  36% { opacity: 0.26; }
  100% { opacity: 0.26; }   /* rest */
}

/* ---- and the wave, underneath it -------------------------------------------
   The heartbeat lives on ::after; this rides ::before, so both run at once
   without touching each other. Kept very low: at 13s intervals anything with
   presence reads as a distraction, so this is barely there on purpose. You
   should notice the surface got brighter, not that something crossed it. */
body[data-wall="archangel"] .glass::before {
  background:
    linear-gradient(100deg,
      transparent 0 40%,
      rgba(255, 255, 255, 0.16) 46%,
      rgba(169, 228, 255, 0.13) 50%,
      rgba(255, 255, 255, 0.16) 54%,
      transparent 60% 100%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.5), transparent 42%);
  background-size: 320% 100%, 100% 100%;
  background-position: 320% 0, 0 0;
  animation: rim-travel 13s var(--ease-linear) infinite;
}
@keyframes rim-travel {
  to { background-position: -320% 0, 0 0; }
}

/* Last word on the glyph, so it outranks archangel's "<33" while the mode is
   live. The whole OS says something different everywhere at once, which is the
   same trick archangel plays and the reason the mode feels like a place rather
   than a filter.

   The two rules have IDENTICAL specificity (0,1,1 each: body plus one
   attribute), so "last word" means source order and nothing else. Archangel's
   copy lives in tokens.css with the rest of the theme, and every page links
   tokens.css before os.css, so this line is still the later one. If that link
   order is ever reversed, archangel silently wins and the konami glyph stops
   changing. This is the only token in os.css that competes across the two
   files. */
body[data-konami] { --t-glyph: "\2665"; }

/* chrome type carries covenant's violet glow */
body[data-wall="archangel"] #menubar,
body[data-wall="archangel"] .os-mono,
body[data-wall="archangel"] .icon__label,
body[data-wall="archangel"] .mood__stats,
body[data-wall="archangel"] .swatch__meta,
body[data-wall="archangel"] .mcap {
  text-shadow: 0 0 10px rgba(90, 41, 194, 0.32);
}
body[data-mode="night"][data-wall="archangel"] #menubar,
body[data-mode="night"][data-wall="archangel"] .os-mono,
body[data-mode="night"][data-wall="archangel"] .icon__label,
body[data-mode="night"][data-wall="archangel"] .mood__stats,
body[data-mode="night"][data-wall="archangel"] .swatch__meta,
body[data-mode="night"][data-wall="archangel"] .mcap {
  text-shadow: 0 0 12px rgba(203, 169, 244, 0.55);
}



/* --------------------------------------------------------------------------
   THE COVENANT LAYER — archangel only.

   The mark gets exactly two homes, both of them earned: a quiet watermark that
   is always in the corner, and the ceremony you see once. It never drifts past
   as decoration. A group's mark is not a particle.
   -------------------------------------------------------------------------- */

/* a zero-size anchor pinned to the corner: each piece of art then sizes itself
   from that corner, so the badge and the full mark keep their own proportions */
/* the anchor shares the dock's vertical band, so the badge reads as part of the
   same row of chrome rather than floating in the corner on its own */
#covenant {
  position: fixed;
  right: 28px;
  bottom: 14px;
  height: 46px;
  width: 0;
  z-index: var(--z-decor);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease-standard);
}
#covenant > span {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  background-repeat: no-repeat;
  background-position: right center;
  background-size: contain;
  transition: opacity var(--motion-slow) var(--ease-standard);
}
.cov__watermark {
  width: clamp(84px, 7.5vw, 122px);
  aspect-ratio: 208 / 90;
}
.cov__mark {
  width: clamp(70px, 6vw, 96px);
  aspect-ratio: 1;
}
.cov__watermark { background-image: url('images/os/covenant-watermark.png'); }
.cov__mark {
  background-image: url('images/os/covenant-mark.png');
  opacity: 0;
}

body[data-wall="archangel"] #covenant { opacity: 0.55; }
body[data-mode="night"][data-wall="archangel"] #covenant { opacity: 0.36; }
/* the badge is white artwork and stays white on every sky: a watermark, not ink.
   the full mark ships black, so it inverts to match. */
.cov__watermark { filter: none; }
.cov__mark { filter: invert(1); }

/* --- the covenant hour: 1:33 and 13:33 ---------------------------------- */
/* for one minute the corner watermark blooms into the whole mark and the rim
   light turns gold. nothing announces it twice. */
body[data-wall="archangel"][data-covenant-hour] #covenant { opacity: 0.92; }
body[data-wall="archangel"][data-covenant-hour] .cov__watermark { opacity: 0; }
body[data-wall="archangel"][data-covenant-hour] .cov__mark { opacity: 1; }
/* the hour beats gold, and a little faster */
body[data-wall="archangel"][data-covenant-hour] .glass::after { animation-duration: 1.95s; }
@media (max-width: 880px) { #covenant { display: none; } }

/* ---- footprints, left in the snow ---- */
.step {
  position: fixed;
  width: 4px;
  height: 6px;
  border-radius: var(--radius-circle);
  background: #b9cfe4aa;
  pointer-events: none;
  z-index: var(--z-decor);
  animation: step-fade 3.6s var(--ease-standard) forwards;
}
@keyframes step-fade { to { opacity: 0; } }

/* --------------------------------------------------------------------------
   PROOF OF VISIT

   Rendered at the share resolution the site-spec fixes (1080x1350) and shown
   at no less than 360px wide. The v8 card was illegible for an optical reason,
   not a type one: a 540px canvas displayed at 300px halves every size on it.
   -------------------------------------------------------------------------- */

/* Settings. Also not a .win, for the same three reasons as the key panel. */
#settings {
  position: fixed;
  inset: 0;
  z-index: var(--z-settings);
  display: none;
  align-items: center;
  justify-content: center;
  padding:  var(--space-2xl);
  background: rgba(24, 16, 46, 0.42);
}
#settings[data-open] { display: flex; }
.set__card { width: min(360px, 100%); padding:  var(--space-xl) var(--space-xl) var(--space-xl); border-radius: var(--radius-lg); }
.set__eyebrow {
  margin:  0 var(--space-xs) var(--space-lg);
  font: 700 var(--text-caption)/1 "Space Mono", monospace;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--os-accent);
}
.set__row {
  display: flex;
  align-items: center;
  gap:  var(--space-lg);
  width: 100%;
  padding:  var(--space-lg);
  border-radius: var(--radius-md);
  text-align: left;
  transition: background-color var(--motion-state) var(--ease-standard);
}
/* Transparent on purpose. With no background declared it fell back to the
   browser's default button colour -- an opaque grey that does not participate
   in the theme system -- so on any palette with light ink it was light text on
   light grey. Sitting directly on the themed card means ink and surface always
   come from the same palette, which the contrast audit already verified across
   all twenty. */
.set__row { background: transparent; }
/* Readability must never depend on hover: a phone has no cursor. Hover is a
   desktop nicety, :active is the affordance that works on both. */
.set__row:hover { background: var(--os-glass); }
.set__row:active {
  background: var(--os-glass);
  transform: scale(0.975);
}
.set__row:focus-visible { outline: 2px solid var(--os-accent); outline-offset: 2px; }
.set__ico { flex: 0 0 30px; height: 30px; display: grid; place-items: center; }
.set__ico img { width: 100%; height: 100%; }
.set__txt { display: flex; flex-direction: column; gap:  var(--space-hair); min-width: 0; }
.set__txt b { font: 700 var(--text-body)/1.2 "Space Mono", monospace; color: var(--os-ink); }
.set__txt i {
  font: 700 var(--text-caption)/1.3 "Space Mono", monospace;
  font-style: normal;
  color: var(--os-ink);
  opacity: .68;
}
/* the destructive one reads warm, so it is never confused with the others */
.set__row--warn .set__txt b { color: var(--os-accent); }
.set__say {
  margin:  var(--space-lg) var(--space-xs) 0;
  min-height: 1.2em;
  font: 700 var(--text-caption)/1.4 "Space Mono", monospace;
  color: var(--os-accent);
}

/* The product key panel. Deliberately not a .win: it must not persist, must not
   reach the dock, and must not count toward the `every` badge. */
#pkey {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: none;
  align-items: center;
  justify-content: center;
  padding:  var(--space-2xl);
  background: rgba(24, 16, 46, 0.42);
}
#pkey[data-open] { display: flex; }
.pkey__card {
  width: min(420px, 100%);
  padding:  var(--space-2xl) var(--space-2xl) var(--space-xl);
  border-radius: var(--radius-lg);
  text-align: center;
}
.pkey__eyebrow {
  margin:  0 0 var(--space-lg);
  font: 700 var(--text-caption)/1 "Space Mono", monospace;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--os-accent);
}
/* monospace and generously spaced, because people transcribe this by hand */
.pkey__code {
  margin:  0 0 var(--space-xl);
  font: 700 var(--text-lead)/var(--lh-lead) "Space Mono", monospace;
  letter-spacing: .06em;
  word-break: break-all;
  color: var(--os-ink);
}
.pkey__row { display: flex; gap:  var(--space-md); justify-content: center; flex-wrap: wrap; }
.pkey__note, .pkey__say {
  margin:  var(--space-lg) var(--space-hair) 0;
  font: 700 var(--text-caption)/1.45 "Space Mono", monospace;
  color: var(--os-ink);
  opacity: .78;
}
.pkey__say { min-height: 1.2em; color: var(--os-accent); opacity: 1; }

#capture {
  position: fixed;
  inset: 0;
  z-index: var(--z-capture);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-xl);
  padding:  var(--space-xl);
  background: #1b103ca8;
  backdrop-filter: var(--glass-veil);
  -webkit-backdrop-filter: var(--glass-veil);
}
#capture[data-open] { display: flex; }
/* The card is held, not displayed. #capstage owns the perspective, #capcard
   does the tilt, and .holo-surface brings the same foil the classic page's
   trading cards wear — its sweep animates on its own and takes an extra nudge
   from --holo-x/--holo-y, which script.js is already driving site-wide.

   The foil and the glare are DOM layers ON TOP of the canvas, never painted
   into it: this is the in-hand moment, and the PNG that gets posted stays the
   flat card. Nothing here can taint or alter the export. */
#capstage {
  perspective: 1100px;
  perspective-origin: 50% 50%;
}
/* The shared foil keeps its full size here: shrinking the streaks also shrank
   the distance holo-sweep travels, so the sweep read as fast and jittery rather
   than as light moving across foil. The band scale IS the motion.

   What was actually wrong was the spill — ::after sits at inset -15%, which put
   a rainbow halo outside a 380px card. So it stays full size and gets masked:
   overflow clips it to the rounded card, and a radial mask feathers it out
   before that edge, so a moving rainbow fades off the card instead of being
   guillotined by it. Opacity is down a little because there is detailed art
   underneath, unlike the classic page's cards. */
#capcard::before { opacity: 0.18; }
#capcard::after {
  opacity: 0.42;
  -webkit-mask-image: radial-gradient(116% 108% at 50% 50%, #000 58%, transparent 96%);
  mask-image: radial-gradient(116% 108% at 50% 50%, #000 58%, transparent 96%);
}
#capcard {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  --glare-x: 50%;
  --glare-y: 50%;
  --glare-a: 0;
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform var(--motion-enter) var(--ease-enter);
  will-change: transform;
  cursor: grab;
}
#capcard[data-live] { transition: transform var(--motion-state) var(--ease-linear); }
#capture-cv {
  display: block;
  width: min(380px, 78vw);
  height: auto;
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 70px #000a, 0 0 0 3px #fff;
}
/* the specular that tracks the pointer, over the foil and under nothing */
.capcard__glare {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--glare-a);
  transition: opacity var(--motion-exit) var(--ease-standard);
  background: radial-gradient(38% 30% at var(--glare-x) var(--glare-y),
    rgba(255, 255, 255, 0.55) 0%,
    rgba(255, 235, 255, 0.18) 42%,
    transparent 72%);
  mix-blend-mode: soft-light;
}

@media (prefers-reduced-motion: reduce) {
  #capcard { transform: none !important; transition: none; }
  .capcard__glare { opacity: 0 !important; }
}
/* one control, no modes: whatever is in the field is what goes on the card,
   and an empty field takes it back off */
.cap__id {
  display: flex;
  gap:  var(--space-md);
  align-items: center;
  width: min(380px, 78vw);
}
.cap__id .field { flex: 1; min-width: 0; }
.cap__id .dbtn { flex-shrink: 0; }

.cap__note {
  max-width: 380px;
  font: 700 var(--text-body)/1.6 var(--font-body);
  color: #f0e4ff;
  text-align: center;
}
.cap__btns { display: flex; gap:  var(--space-md); flex-wrap: wrap; justify-content: center; }

/* --------------------------------------------------------------------------
   THE CEREMONY — the thirteenth arrives once.
   -------------------------------------------------------------------------- */

#ceremony {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-hair);
  padding:  var(--space-2xl);
  text-align: center;
  background: radial-gradient(68% 52% at 50% 34%, #3a2170 0%, #1b1030 52%, #0d0720 100%);
  animation: cer-in var(--motion-enter) var(--ease-standard);
}
#ceremony[data-open] { display: flex; }
#ceremony[data-closing] { animation: cer-out var(--motion-slow) var(--ease-standard) forwards; }
@keyframes cer-in { from { opacity: 0; } }
@keyframes cer-out { to { opacity: 0; } }

/* light finds it first */
.cer__rays {
  position: absolute;
  inset: -30% -20% auto -20%;
  height: 150%;
  pointer-events: none;
  background: repeating-conic-gradient(from 188deg at 50% -14%,
    rgba(255, 255, 255, 0.5) 0 0.9deg,
    rgba(203, 169, 244, 0.3) 0.9deg 2.1deg,
    transparent 2.1deg 5.4deg);
  -webkit-mask-image: linear-gradient(to bottom, #000 2%, transparent 72%);
  mask-image: linear-gradient(to bottom, #000 2%, transparent 72%);
  opacity: 0;
  animation: cer-rays 3.4s var(--ease-standard) 0.2s forwards;
}
@keyframes cer-rays {
  0% { opacity: 0; transform: rotate(-4deg) scale(1.14); }
  40% { opacity: 1; }
  100% { opacity: 0.85; transform: rotate(2deg) scale(1); }
}

.cer__mark {
  width: min(46vw, 260px);
  aspect-ratio: 1;
  margin-bottom:  var(--space-sm);
  background: url('images/os/covenant-mark.png') center / contain no-repeat;
  filter: invert(1) drop-shadow(0 0 26px rgba(203, 169, 244, 0.7));
  opacity: 0;
  animation: cer-mark 1.1s var(--ease-enter) 0.5s forwards;
}
@keyframes cer-mark {
  from { opacity: 0; transform: scale(0.84); filter: invert(1) drop-shadow(0 0 4px rgba(203, 169, 244, 0)); }
  to { opacity: 1; transform: scale(1); filter: invert(1) drop-shadow(0 0 30px rgba(203, 169, 244, 0.75)); }
}

#ceremony p, #ceremony h2 { opacity: 0; animation: cer-line var(--motion-slow) var(--ease-standard) forwards; }
.cer__eyebrow {
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-widest);
  color: #cba9f4;
  animation-delay: 1.15s;
}
.cer__title {
  font: 400 clamp(var(--text-display), 8vw, 64px) var(--font-display);
  line-height:  var(--lh-display);
  color: #fff;
  text-shadow: 0 0 24px rgba(203, 169, 244, 0.85), 0 0 60px rgba(90, 41, 194, 0.7);
  animation-delay: 1.4s;
}
.cer__line {
  max-width: 30ch;
  margin-top:  var(--space-md);
  font: 700 var(--text-lead)/1.3 var(--font-body);
  line-height: 1.6;
  color: #f0e4ff;
  animation-delay: 1.85s;
}
.cer__foot {
  margin-top:  var(--space-2xl);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-widest);
  text-transform: uppercase;
  color: #a9e4ff;
  text-shadow: 0 0 14px rgba(169, 228, 255, 0.6);
  animation-delay: 2.4s;
}
.cer__hint {
  position: absolute;
  bottom: 26px;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wider);
  color: #cba9f4;
  opacity: 0;
  animation: cer-line var(--motion-slow) var(--ease-standard) 3.6s forwards;
}
@keyframes cer-line {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  #ceremony *, #ceremony { animation-duration: 0.001ms !important; }
  #ceremony p, #ceremony h2, .cer__mark, .cer__rays { opacity: 1 !important; }
}

/* --------------------------------------------------------------------------
   LIQUID GLASS — one recipe, used by every floating chrome surface.

   Four things have to be true at once or it reads as a flat tinted bar:
     1. the tint is mostly transparent, so the blur has something to show
     2. saturation is pushed hard, so colour behind the glass blooms through it
     3. a bright specular hairline on the top edge, a dim one on the bottom
     4. a hairline rim in the theme's own light, not a grey border
   -------------------------------------------------------------------------- */






@supports not (backdrop-filter: blur(1px)) {
  .glass, .glass--dense { background-color: var(--os-surface); }
}
/* rain on the window glass — the screenshot detail */
body[data-mode="rain"] .win::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0.8;
  background-image:
    radial-gradient(circle at 12% 22%, #ffffffb0 0 2px, #ffffff30 2.6px, transparent 3.4px),
    radial-gradient(circle at 78% 14%, #ffffff90 0 1.3px, transparent 2.2px),
    radial-gradient(circle at 55% 58%, #ffffffa0 0 2.4px, #ffffff2e 3px, transparent 4px),
    radial-gradient(circle at 31% 79%, #ffffff85 0 1.4px, transparent 2.3px),
    radial-gradient(circle at 90% 44%, #ffffff95 0 1.7px, transparent 2.8px),
    radial-gradient(circle at 66% 88%, #ffffff70 0 1.1px, transparent 1.9px),
    radial-gradient(circle at 22% 47%, #ffffff78 0 1.2px, transparent 2.1px);
}

/* a theme change should feel like the room re-lighting, not a hard cut */
#menubar, #dock, #hdock, .mdrop, #theme-sheet,
.win, .tbar, .icon__art, .icon__label, .swatch__chip {
  transition: background-color var(--motion-enter) var(--ease-standard), color var(--motion-enter) var(--ease-standard),
              border-color var(--motion-enter) var(--ease-standard), box-shadow var(--motion-enter) var(--ease-standard);
}
.mood { transition: border-color var(--motion-enter) var(--ease-standard), box-shadow var(--motion-enter) var(--ease-standard), background-color var(--motion-enter) var(--ease-standard); }

/* --------------------------------------------------------------------------
   4. MENU BAR  (z 50 / 55)
   -------------------------------------------------------------------------- */

#menubar {
  position: fixed;
  inset: 0 0 auto 0;
  height: 34px;
  z-index: var(--z-menu);
  display: flex;
  align-items: center;
  gap:  var(--space-hair);
  padding:  0 var(--space-lg);
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  color: var(--os-ink);
  /* .glass owns the surface; the bar only adds its bottom hairline */
  border-bottom: 1px solid var(--glass-edge);
}

.mitem {
  position: relative;
  padding:  var(--space-sm) var(--space-lg);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  color: inherit;
}
.mitem:hover { background: #ffffff8c; }
body[data-mode="night"] .mitem:hover,
body[data-wall="arcade"] .mitem:hover { background: #ffffff1f; }
.mitem--brand { color: var(--os-accent); }

/* Each menu owns its own dropdown. The four used to be placed with hardcoded
   lefts -- 12, 66, 114, 222 -- copied off the buttons at some past font size,
   and by the time anyone looked three of the four had drifted: file was 9px
   out, view 16, help 33. A number that describes another element's position is
   always going to rot, because nothing tells it when that element moves. The
   wrapper makes `left: 0` mean "under my own button", which cannot drift, and
   `top: 100%` means "just below it" rather than 30px from the bar. */
.mgroup {
  position: relative;
  display: flex;
}
.mgroup > .mdrop {
  left: 0;
  top: calc(100% + var(--space-sm));
}
.mdrop {
  position: absolute;
  min-width: 210px;
  display: none;
  flex-direction: column;
  gap:  var(--space-hair);
  padding:  var(--space-md);
  border-radius: var(--radius-lg);
  z-index: var(--z-menu-drop);
}
.mdrop[data-open] { display: flex; }
.mdrop button,
.mdrop a {
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-ink);
  text-decoration: none;
  background: none;
  border: none;
  text-align: left;
  padding:  var(--space-md) var(--space-lg);
  border-radius: var(--radius-sm);
}
.mdrop button:hover,
.mdrop a:hover { background: #ffffff8c; color: var(--os-accent); }
body[data-mode="night"] .mdrop button:hover,
body[data-mode="night"] .mdrop a:hover,
body[data-wall="arcade"] .mdrop button:hover,
body[data-wall="arcade"] .mdrop a:hover { background: #ffffff1f; }

/* one row style for any menu list, in a dropdown or in the phone sheet */
.menu-row {
  display: block;
  width: 100%;
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-ink);
  background: none;
  border: none;
  text-align: left;
  padding:  var(--space-md) var(--space-lg);
  border-radius: var(--radius-md);
}
.menu-row:hover { background: var(--os-pill); }
.menu-row[aria-pressed="true"] { background: var(--os-pill); color: var(--os-accent); }

.theme-list { display: flex; flex-direction: column; gap:  var(--space-hair); }
.theme-list .swatch {
  display: flex;
  align-items: center;
  gap:  var(--space-lg);
  width: 100%;
  padding:  var(--space-sm) var(--space-md);
  border: none;
  border-radius: var(--radius-md);
  background: none;
  text-align: left;
  transition: background var(--motion-state) var(--ease-standard);
}
.swatch:hover { background: var(--os-pill); }
.swatch[aria-pressed="true"] { background: var(--os-pill); }
.swatch__chip {
  position: relative;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: transform var(--motion-state) var(--ease-standard), box-shadow var(--motion-state) var(--ease-standard);
}
/* the foil corner: a slice of the theme's own icon gradient */
.swatch__chip::after {
  content: '';
  position: absolute;
  right: -8px;
  bottom: -8px;
  width: 22px;
  height: 22px;
  border-radius: var(--radius-circle);
  box-shadow: 0 0 0 2px var(--os-surface);
}
.swatch:hover .swatch__chip { transform: scale(1.08) rotate(-4deg); }
.swatch[aria-pressed="true"] .swatch__chip {
  box-shadow: 0 0 0 2px var(--os-surface), 0 0 0 3.5px var(--os-accent);
}
.swatch__text { display: flex; flex-direction: column; gap:  var(--space-hair); min-width: 0; }
.swatch__name {
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-ink);
  white-space: nowrap;
}
.swatch[aria-pressed="true"] .swatch__name { color: var(--os-accent); }
.swatch__meta {
  font: 700 var(--text-micro)/var(--lh-micro) var(--font-mono);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--os-ink-dim);
  white-space: nowrap;
}
.swatch[data-locked] { opacity: 0.5; }
.swatch[data-locked] .swatch__chip { filter: grayscale(0.7); }

/* menu group captions, so the two radio groups never read as one list */
.mcap {
  padding:  var(--space-sm) var(--space-lg) var(--space-xs);
  font: 700 var(--text-micro)/var(--lh-micro) var(--font-mono);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--os-ink-dim);
}
/* selection is a filled row, not a tick: one look for both groups */
.mdrop button[aria-pressed="true"] {
  background: var(--os-pill);
  color: var(--os-accent);
}
.msep { height: 1.5px; margin:  var(--space-xs) var(--space-sm); background: var(--os-border); }

#menubar .right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap:  var(--space-lg);
}
#icy-state { display: flex; align-items: center; gap:  var(--space-sm); }
#icy-state .status-dot { width: 8px; height: 8px; }
#icy-state[data-asleep] .status-dot {
  background: var(--lilac);
  animation: none;
  box-shadow: none;
}
#clock { min-width: 58px; text-align: right; }

/* --------------------------------------------------------------------------
   5. DESKTOP  (z 2-5)
   -------------------------------------------------------------------------- */

#desktop {
  position: fixed;
  inset: 34px 0 0 0;
  z-index: var(--z-chrome);
}

/* THE LOGOMARK
   The classic page's hero logo treatment, wherever the name is set: a flat
   silhouette used as a MASK, with the gradient, the shine and the sparkles all
   applied in CSS behind it. A mask reads alpha and nothing else, which is why
   the asset has to be a shape and never a pre-coloured logo.

   The gradient is the fixed brand ramp rather than the per-theme --t-art, for
   the same reason the share card's wordmark is: a wordmark is a wordmark and
   should be the same object on every theme.

   Three stacked drop-shadows, not one. A chained filter shadows the result of
   the shadow before it, so a tight contact pass plus a wide soft pass builds a
   real blurred outline — which this needs, because it sits on twenty different
   skies and the top of the ramp is nearly white. */
/* The mask and the filter MUST live on different elements. CSS applies filter
   BEFORE mask, so with both on one element the drop-shadow is computed from the
   unmasked rectangle — a rectangle's shadow, hidden behind the rectangle — and
   then the mask cuts it away. The result is no shadow at all, which is exactly
   what shipped. The parent filters the already-masked child instead. */
.logomark {
  position: relative;
  aspect-ratio: 1500 / 367;
  /* Four passes, each compounding on the last. The ramp starts at #fff8fc,
     which is white, and the OS skies are pale — so the near edge needs a hard
     contact pass before the soft halo or the leading letters dissolve. */
  filter:
    drop-shadow(0 0 1px rgba(118, 84, 172, 0.9))
    drop-shadow(0 2px 3px rgba(118, 84, 172, 0.6))
    drop-shadow(0 0 14px rgba(118, 84, 172, 0.55))
    drop-shadow(0 8px 20px rgba(158, 120, 214, 0.45));
}
.logomark__art {
  position: absolute;
  inset: 0;
  isolation: isolate;
  -webkit-mask-image: url('images/os/icybearos-sm.png');
  mask-image: url('images/os/icybearos-sm.png');
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: left center;
  mask-position: left center;
  background: linear-gradient(160deg, #fff8fc 0%, #f8bfe6 35%, #b48ee9 68%, #8fc2f4 100%);
}
.logomark__shine {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: 0.85;
  background-image: linear-gradient(115deg, transparent 15%,
    rgba(255, 255, 255, 0.95) 28%, rgba(255, 180, 235, 0.9) 38%,
    rgba(180, 210, 255, 0.9) 48%, rgba(255, 240, 170, 0.9) 58%,
    rgba(255, 180, 235, 0.9) 68%, transparent 82%);
  background-size: 260% 260%;
  background-position: var(--holo-x) var(--holo-y);
}
.logomark__sparkle {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: soft-light;
  opacity: 0.55;
  animation: holo-sparkle-drift 8s var(--ease-standard) infinite reverse;
  background-image:
    radial-gradient(circle at 12% 30%, rgba(255, 255, 255, 0.95) 0%, transparent 3.5%),
    radial-gradient(circle at 45% 15%, rgba(255, 255, 255, 0.9) 0%, transparent 3%),
    radial-gradient(circle at 70% 60%, rgba(255, 255, 255, 0.95) 0%, transparent 3.2%),
    radial-gradient(circle at 90% 25%, rgba(255, 255, 255, 0.85) 0%, transparent 2.8%),
    radial-gradient(circle at 30% 75%, rgba(255, 255, 255, 0.85) 0%, transparent 2.8%);
  background-size: 220% 220%;
}

#brand {
  --mark-w: clamp(178px, 17vw, 252px);
  position: absolute;
  top: 15px;
  left: 32px;
  pointer-events: none;
}
#wordmark { width: var(--mark-w); }
.brand__row { display: flex; align-items: flex-end; gap:  var(--space-lg); }

/* v1.0 rides beside the logo at half its height, which pulls the mark and the
   subtitle closer to the same width and stops the top-left reading as one long
   thing over one short thing.

   Type, so it uses background-clip rather than a mask — and unlike the mask,
   background-clip happens during paint, so the filter on the same element
   works here and does not need the parent/child split the logomark needs. */
.brand__ver {
  position: relative;
  font-family: var(--font-display);
  font-size: calc(var(--mark-w) / 4.087 * 0.48);
  /* background-clip:text paints only INSIDE the element's box. A line-height
     under 1 made the box shorter than the glyphs, so the bottom of the 1 and
     the 0 had no background to clip against and simply vanished. The box has
     to contain the type; position is the margin's job, not the leading's. */
  line-height: 1.24;
  /* Sit on the logo's BASELINE, not the bottom of its box. The y descender
     runs 22.6% of the mask's height below the baseline (measured off the
     alpha), and flex-end would otherwise drop v1.0 all the way down to it.
     Less the half-leading the line box now adds under the glyphs. */
  margin-bottom: calc(var(--mark-w) / 4.087 * 0.113);
  background: linear-gradient(160deg, #fff8fc 0%, #f8bfe6 35%, #b48ee9 68%, #8fc2f4 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter:
    drop-shadow(0 0 1px rgba(118, 84, 172, 0.9))
    drop-shadow(0 2px 3px rgba(118, 84, 172, 0.6))
    drop-shadow(0 0 10px rgba(118, 84, 172, 0.55))
    drop-shadow(0 6px 14px rgba(158, 120, 214, 0.45));
}
/* the same cursor-tracking shine, clipped to the same glyphs */
.brand__ver::after {
  content: attr(data-ver);
  position: absolute;
  left: 0;
  top: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: 0.85;
  background-image: linear-gradient(115deg, transparent 15%,
    rgba(255, 255, 255, 0.95) 28%, rgba(255, 180, 235, 0.9) 38%,
    rgba(180, 210, 255, 0.9) 48%, rgba(255, 240, 170, 0.9) 58%,
    rgba(255, 180, 235, 0.9) 68%, transparent 82%);
  background-size: 260% 260%;
  background-position: var(--holo-x) var(--holo-y);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

#tagline {
  margin:  var(--space-md) 0 0 var(--space-xs);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-widest);
  color: var(--os-ink-dim);
  pointer-events: none;
}

/* icons sit on the 46px graph grid and wrap into columns above the dock */
#icons {
  position: absolute;
  top: 116px;
  left: 28px;
  bottom: 118px;
  z-index: var(--z-icons);
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  align-content: flex-start;
  gap:  var(--space-md) var(--space-sm);
  max-width: 46vw;
}
.icon {
  /* 112, and the number is measured rather than chosen. The label is a nowrap
     pill of mono type, and the longest of them, `patch_notes.log`, sets the
     cell: at --text-caption with 12px of pill padding it is 103.8px, plus this
     rule's own 8px, so 112 is the first multiple of the 8px space rung that
     clears it. Truncating instead would eat the file extensions, which are the
     joke, and letting it overflow bleeds the pill into the next column and
     breaks the grid rhythm, which is what used to happen at 92.
     Widening this also moves where windows open: placeWin() anchors the cascade
     to #icons.right rather than to a number. */
  width: 112px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap:  var(--space-sm);
  padding:  var(--space-md) var(--space-xs);
  border: none;
  background: none;
  border-radius: var(--radius-lg);
  transition: transform var(--motion-state) var(--ease-standard);
}
.icon:hover { transform: translateY(-3px); }

/* THE ENTRY POINT. Twelve icons at identical weight, named like files, and
   nothing saying which one is the door. read_me already shimmered, which draws
   the eye without telling it what to do -- so the tag names the verb.

   It is shown by body[data-guide], which os.js points at the first stop on a
   three-icon tour -- read_me, then diagnosis, then the guestbook -- skipping
   any that have already been opened and disappearing once all three have. So
   it is a first-run label rather than a permanent decoration, it cannot nag a
   returning visitor, and only one of the three is ever lit at a time.

   Not aria-hidden: it comes after the label in the DOM, so the button reads
   "read_me.txt, start here", which is the whole point of it. Which is also why
   the inactive two are visibility:hidden and not merely transparent -- an
   opacity:0 tag is still read aloud, so three of them would put two labels
   into the accessible name of icons that are not being pointed at. */
.icon__start {
  /* Below the label, not beside the tile. Beside put it into the next column --
     the grid's horizontal gap is 6px and the tag is 80 -- so it landed on
     guestbook.exe and got clipped. Under the label there is 40px of clear air
     and nothing to collide with. */
  position: absolute;
  top: 100%;
  left: 50%;
  margin-top: -6px;
  padding:  var(--space-hair) var(--space-sm);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  font: 700 var(--text-micro)/1.6 var(--font-mono);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--on-accent);
  background: var(--os-accent);
  box-shadow: 0 3px 10px var(--os-shadow);
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(-4px);
  pointer-events: none;
  transition: opacity var(--motion-enter) var(--ease-standard),
              transform var(--motion-enter) var(--ease-standard),
              visibility 0s linear var(--motion-enter);
}
.icon { position: relative; }
body[data-guide='readme'] .icon[data-app='readme'] .icon__start,
body[data-guide='diag'] .icon[data-app='diag'] .icon__start,
body[data-guide='guest'] .icon[data-app='guest'] .icon__start {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition: opacity var(--motion-enter) var(--ease-standard),
              transform var(--motion-enter) var(--ease-standard),
              visibility 0s;
}
/* the little nose pointing back up at the tile */
.icon__start::before {
  content: '';
  position: absolute;
  left: 50%;
  top: -3px;
  width: 6px;
  height: 6px;
  transform: translateX(-50%) rotate(45deg);
  background: var(--os-accent);
}
@media (prefers-reduced-motion: reduce) {
  .icon__start { transition: none; }
}
.icon__art {
  width: 50px;
  height: 50px;
  border-radius: var(--radius-lg);
  display: grid;
  place-items: center;
  font-size: var(--text-title);
  /* Glass rather than a solid --t-art tile, on both shells. On the phone it
     lets the standee read through the grid instead of being hidden behind it,
     and on the desktop it puts the icons in the same material as the bar and
     the dock. The glyphs are flat colour with white outlines, so they hold up
     over anything the blur puts behind them. */
  background-color: var(--os-glass);
  background-image: linear-gradient(180deg,
    rgba(255, 255, 255, 0.34) 0%,
    rgba(255, 255, 255, 0.04) 55%,
    rgba(255, 255, 255, 0.14) 100%);
  backdrop-filter: var(--glass-tile);
  -webkit-backdrop-filter: var(--glass-tile);
  border: none;
  box-shadow:
    inset 0 1px 0 var(--glass-spec), inset 0 0 0 1px var(--glass-edge), var(--elev-raised) var(--os-shadow);
}
.icon__label {
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  color: var(--os-ink);
  background: var(--os-pill);
  /* 6, not 8: the pill and the cell above are one measurement, and this is the
     rung that lets the longest label sit inside a 112px cell. */
  padding:  var(--space-hair) var(--space-sm);
  border-radius: var(--radius-pill);
  white-space: nowrap;
}
/* The shimmer rides with the tag rather than being pinned to read_me in the
   markup. Two attention magnets pointing at different icons is worse than one,
   and a read_me that shimmers forever is still asking a returning visitor to
   start somewhere they have already been. */
body[data-guide='readme'] .icon[data-app='readme'] .icon__art,
body[data-guide='diag'] .icon[data-app='diag'] .icon__art,
body[data-guide='guest'] .icon[data-app='guest'] .icon__art {
  animation: icon-shimmer 2.6s var(--ease-standard) infinite;
}
@keyframes icon-shimmer {
  0%, 100% { box-shadow: var(--elev-raised) var(--os-shadow), inset 0 2px 5px #ffffffcc; }
  50% { box-shadow: 0 4px 22px var(--t-deco), inset 0 2px 5px #ffffffcc; }
}
/* Rebuilt on the glass tile's own shadow stack: this rule still carried the
   solid-tile values and was quietly undoing the glass. The glow takes the
   theme's accent, so the easter egg composes with all five themes instead of
   forcing one colour over them. */
body[data-konami] .icon__art {
  box-shadow:
    inset 0 1px 0 var(--glass-spec), inset 0 0 0 1px var(--os-accent), var(--elev-raised) var(--os-shadow), 0 0 22px var(--t-deco);
}
/* dragging rearranges the list: the icon lifts, a ghost holds the slot it will
   land in, and the rest reflow. Positions can only ever be real grid slots. */
.icon[data-dragged] {
  position: fixed;
  z-index: var(--z-drag);
  pointer-events: none;
  opacity: 0.92;
  transform: scale(1.06) rotate(-3deg);
}
.icon--ghost {
  border-radius: var(--radius-lg);
  background: var(--os-pill);
  opacity: 0.4;
  box-shadow: inset 0 0 0 2px var(--os-border);
}

/* the others get nervous while you drag one */
#icons[data-dragging] .icon:not([data-dragged]):not(.icon--ghost) { animation: icon-jiggle 0.28s var(--ease-standard) infinite; }
@keyframes icon-jiggle {
  0%, 100% { transform: rotate(-1.6deg); }
  50% { transform: rotate(1.6deg); }
}

/* --------------------------------------------------------------------------
   6. MOOD RING  (one component, both shells)
   -------------------------------------------------------------------------- */

.mood {
  width: 236px;
  padding:  var(--space-lg) var(--space-xl);
  border-radius: var(--radius-xl);
  border: 1.5px solid var(--mood-tint, #ffffffe6);
  --glass-glow: var(--mood-glow, transparent);
  --glass-glow-size: 22px;
  transition: border-color var(--motion-enter);
}
/* the right rail: who icy is, then what she does, stacked and self-sizing so
   the panel always opens directly under the mood ring */
#right-rail {
  position: absolute;
  top: 16px;
  right: 24px;
  z-index: var(--z-rail);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap:  var(--space-md);
  /* 238, not 300. The rail's widest nowrap line is 202px and the panel adds 34
     of padding, so it renders at 236 and the old cap was 62px of headroom that
     no content ever reached -- a constraint that was not constraining anything,
     which is the kind that silently absorbs growth nobody notices. */
  max-width: min(238px, 40vw);
}
.mood__title { margin-bottom:  var(--space-sm); }
.mood__row { display: flex; align-items: center; gap:  var(--space-md); margin-bottom:  var(--space-md); }
.mood__who { font: 700 var(--text-body)/var(--lh-body) var(--font-mono); }
.mood__who b { color: var(--os-accent); }
.mood__stats {
  font: 400 var(--text-caption)/var(--lh-caption) var(--font-mono);
  line-height: 1.9;
  color: var(--os-ink);
}
.mood__stats > span { display: block; white-space: nowrap; }
.mood__stats b { white-space: nowrap; }
.mood__gauge {
  color: var(--os-accent);
  letter-spacing: var(--track-wide);
  background: none;
  border: none;
  font: inherit;
  padding: 0;
}
.mood__stats > .mood__patch {
  display: block;
  font-size: var(--text-caption);
  font-style: italic;
  color: var(--os-ink-dim);
  white-space: normal;
  line-height: 1.5;
  margin-top:  var(--space-xs);
}

/* the stone: a literal rotating conic mood ring, hue-shifted per mode */
.stone {
  position: relative;
  display: inline-block;
  padding:  var(--space-xs);
  border-radius: var(--radius-circle);
  isolation: isolate;
}
.stone::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: var(--radius-circle);
  background: var(--t-stone);
  filter: var(--stone-shift, none) blur(0.5px);
  animation: stone-spin 6s var(--ease-linear) infinite;
}
@keyframes stone-spin { to { transform: rotate(360deg); } }
.stone img {
  display: block;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-circle);
  object-fit: cover;
  border: 2px solid var(--mood-tint, #fff);
  background: #fff;
}

body[data-mode="day"] { --mood-tint: #f4e0b8; --mood-glow: #f4e9bf77; --stone-shift: none; }
body[data-mode="night"] { --mood-tint: #8fa2e8; --mood-glow: #8fa2e844; --stone-shift: hue-rotate(190deg); }
body[data-mode="rain"] { --mood-tint: #a8b4e0; --mood-glow: #8fa2e866; --stone-shift: hue-rotate(240deg) saturate(0.7); }
body[data-mode="snow"] { --mood-tint: #bfe0f4; --mood-glow: #a8d8f088; --stone-shift: hue-rotate(160deg) saturate(1.3) brightness(1.1); }

/* --------------------------------------------------------------------------
   7. STANDEE  (z 3 — she stands on the desktop, below windows)
   -------------------------------------------------------------------------- */

#standee {
  position: fixed;
  /* The canvas carries its own margin: her head sits 47.4% across it, so about
     a third of its width is transparent to her right, and that padding scales
     with her instead of with the viewport. The old clamp() existed to hold a
     tight crop off the edge and would now be counted twice. */
  right: 60px;
  bottom: 74px;
  z-index: var(--z-standee);
  text-align: center;
  border: none;
  background: none;
  padding: 0;
}
#standee img {
  display: block;
  /* ONE height for all seventeen poses, standing and seated alike. Every pose
     ships on the same 1492x1702 canvas with the ground on the bottom edge and
     the head on a shared vertical line (test/poses.sh), so bounding box and
     body scale are the same measurement here and cycling poses cannot change
     her size or shift her sideways. Before the shared canvas this rule sized
     her by whatever her bounding box happened to be, which ran 0.37 to 0.39 of
     the file across the standing poses and 0.43 to 0.54 across the seated ones:
     her head changed size when the pose changed.

     max-width is a guard for a wide, short window, not a second scale. The
     canvas is 0.877 wide per unit of height, so below 0.877x the height cap it
     takes over the sizing -- which is harmless now, because it shrinks every
     pose by the same factor, but it does make her smaller than intended. */
  max-height: min(78vh, 830px, calc(100vh - 274px));
  max-width: 52vw;
  width: auto;
  height: auto;
  margin: 0 auto;
  /* Two passes: a tight contact shadow, then the soft cast. Same reasoning as
     the wordmark's four -- a chained filter shadows the RESULT of the shadow
     before it, so a hard near pass plus a wide soft one reads as an object
     standing on something instead of a sticker floating over it. This could not
     be done before the shared canvas: each pose used to be cropped to its own
     bounding box, so "the bottom of the image" was her feet in one pose and her
     hip in the next, and a tight pass at that offset landed in a different
     place on every pose. Now every pose puts the ground on the canvas edge. */
  filter:
    drop-shadow(0 2px 1px #4a2f7f3d)
    drop-shadow(0 14px 26px #5a3f8f2b);
  transform: perspective(900px) rotateY(var(--standee-tilt, 0deg));
  transition: opacity var(--motion-exit), transform var(--motion-enter) var(--ease-standard);
}
body[data-mode="night"] #standee img {
  filter:
    drop-shadow(0 2px 1px rgba(8, 4, 26, 0.55))
    drop-shadow(0 14px 30px rgba(10, 6, 30, 0.55));
}
#standee figcaption {
  display: inline-block;
  margin-top:  var(--space-sm);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wide);
  color: var(--os-ink-dim);
  background: var(--os-pill);
  border-radius: var(--radius-pill);
  padding:  var(--space-xs) var(--space-lg);
}
@media (max-width: 1120px) { #standee img { max-height: min(58vh, calc(100vh - 300px)); max-width: 48vw; } }
/* 700, not 880. 880 was the width below which there is no room for her BESIDE a
   left column of icons -- but that column only exists on the desktop, and every
   iPad, Surface and folding laptop in portrait was landing under it and losing
   her entirely. Section 16 gives the portrait slab a layout that has somewhere
   to put her; below 700 the phone shell hides her along with the whole desktop,
   so this only ever needed to reach the phone breakpoint. */
@media (max-width: 700px), (max-height: 500px) { #standee { display: none; } }

/* read_me's two closing sections, paired.

   The panel is one column that ends in its two shortest, densest blocks: a
   six-row spec list and the contact pills, both about 120px tall and both
   narrow. Stacked, they pushed 291px of a 701px panel below the fold, and the
   291px was the wrong half: `WHERE I AM` is the only conversion point in the
   window a hiring manager opens first, and the request-a-quote button under it
   was hidden too. Paired, the panel is 536px against a body that can now be
   559px, so nothing is hidden and there is no scrollbar to discover.

   auto-fit rather than a media query, because the thing that varies is the
   WINDOW, which the visitor can drag to any width, and a media query only knows
   about the viewport. Under about 460px of body the columns become one, which
   is the right answer on a narrow window and on the phone.

   Left-aligned against the centred intro above it: ragged-right is easier to
   scan in a 280px column, and it gives the tail a different texture from the
   prose, which is what tells you the introduction has ended. */
.readme-tail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0 var(--space-2xl);
  /* stretch, not start: both columns become the height of the taller one, which
     gives the shorter one a floor to align its last line against. Without it the
     two columns simply ended wherever their content ran out, seven pixels apart,
     and no amount of tuning the gaps above would hold that closed as the copy
     changes. */
  align-items: stretch;
  text-align: left;
  margin-top: var(--space-lg);
}
.readme-tail > section { display: flex; flex-direction: column; }
/* The vital sign sits on the column's floor, so it lines up with the last row of
   the spec list opposite however either side grows. The 4px is measured, not
   chosen: the uptime is set in a caption line box (15px) and the spec row it has
   to sit level with is a body one (17px), so aligning the two BOXES leaves the
   two lines of text 4px apart. This aligns what a reader actually sees. */
.readme-tail .vitals { margin-top: auto; margin-bottom: 4px; }
/* ---- the resume tab: a document, shown as one ----
   It used to be an icon, a joke and a button, 262px of content in a window the
   other tabs fill to 565. Showing page one lets someone judge the CV before
   spending a download, and it makes the tab a document viewer, which is the
   thing the OS is pretending to be everywhere else. */
.doc {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2xl);
  align-items: start;
  text-align: left;
}
/* a page, not a thumbnail: a hairline, a shadow and a tiny lift on hover, so it
   reads as a sheet of paper you could pick up */
.doc__page {
  display: block;
  width: 268px;
  border-radius: var(--radius-xs);
  overflow: hidden;
  box-shadow: var(--elev-raised) var(--os-shadow), inset 0 0 0 1px var(--glass-edge);
  transition: transform var(--motion-state) var(--ease-standard),
              box-shadow var(--motion-state) var(--ease-standard);
}
.doc__page img { display: block; width: 100%; height: auto; }
.doc__page:hover { transform: translateY(-3px); box-shadow: var(--elev-floating) var(--os-shadow); }
.doc__side { display: flex; flex-direction: column; }
/* the display face again, one step down: this is a tab, not the front door */
.doc__title { margin: 0 0 var(--space-sm); font-size: var(--text-title); }
/* The button and the line under it are ONE block. The line qualifies the
   download -- it is the small print on the thing you are about to click -- and
   left-aligned under a centred button it read as a stray sentence that had
   come loose from the paragraph above. */
.doc__cta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  margin-top: auto;
  padding-top: var(--space-lg);
  text-align: center;
}
/* Small print, set small. `.mini` is body size at 700 weight -- dim, but not
   quiet -- and under a button that reads as a second call to action rather
   than as the footnote it is. */
.doc__cta .mini {
  margin: 0;
  font: 600 var(--text-caption)/var(--lh-caption) var(--font-body);
  max-width: 34ch;
}
@media (max-width: 620px) {
  .doc { grid-template-columns: 1fr; justify-items: center; text-align: center; }
  .doc__side { align-items: center; }
  /* THE PREVIEW STOPS PRETENDING. Its job on a desktop is "judge the document
     before spending a download", and on a phone that job is impossible: a
     one-page CV at 393px puts the body type at about four pixels, so a full
     width preview is not a document, it is a rectangle of grey noise sitting
     above everything worth reading. At thumbnail size it says the true thing
     instead -- that there is a real, designed, one-page CV behind the tap. */
  .doc__page { width: min(196px, 52%); }
  /* Centred is right for a title and a button and wrong for four-line
     paragraphs: every line starts in a different place and the eye has to find
     the left edge again on each one. */
  .doc__side .lede { text-align: left; align-self: stretch; }
}

/* The line the window opens on, and the only place the display face appears on
   the surface a hiring manager reads first. It was set in the body face at
   15.4px, which made it typographically identical to the paragraph beneath it:
   the best sentence on the site, weighted exactly like the explanation of it.
   The panel had four sizes in a 5px band and no focal point at all. */
.readme-hook {
  margin: var(--space-xs) 0 var(--space-sm);
  font: 400 var(--text-display)/var(--lh-display) var(--font-display);
  color: var(--os-ink);
  text-wrap: balance;
}

/* The panel now leads with a 34px line, so the small type under it does not need
   as much air to separate itself: the hook is doing that work. */
[data-panel="readme"] .lede + .lede,
[data-panel="readme"] .mini { margin-top: var(--space-sm); }
[data-panel="readme"] .readme-tail { margin-top: var(--space-md); }

/* Theme decoration, borrowed wholesale from the lock screen: --motif-N is
   defined per wall, so strawberry gets berries and archangel gets wings without
   a rule being added here. Behind the text, out of the pointer's way, and hidden
   from assistive tech because it is not saying anything. */
.wpanel { position: relative; }
.wpanel__deco {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}
.wpanel__deco i {
  position: absolute;
  background: center/contain no-repeat;
  opacity: 0.5;
  animation: lock-float 9s var(--ease-standard) infinite;
}
.wpanel__deco i:nth-child(1) { left: -4px;  top: 4px;   width: 26px; height: 26px; background-image: var(--motif-1); animation-delay: -1.1s; }
.wpanel__deco i:nth-child(2) { right: 2px;  top: 22px;  width: 19px; height: 19px; background-image: var(--motif-2, var(--motif-1)); animation-delay: -4.3s; }
.wpanel__deco i:nth-child(3) { right: 40px; top: -2px;  width: 13px; height: 13px; background-image: var(--motif-3, var(--motif-1)); animation-delay: -6.8s; }
/* the prose has to sit above the decoration, not beside it */
.wpanel > :not(.wpanel__deco) { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .wpanel__deco i { animation: none; } }

/* ---- the two instruments in read_me ----
   The panel's headings are already a system readout: PROCESSOR, MEMORY, POWER
   SUPPLY as a list of what she does. These finish the joke rather than decorate
   around it. The signal meter belongs with the addresses because it is about
   reachability; the vital sign spans both columns because it is measuring the
   whole machine. Neither invents a fact: the bars and the ping are atmosphere
   and are hidden from assistive tech, and the only real number, the uptime, is
   computed from one date rather than written down. */
.readout {
  /* one line, both readings: the column is narrow enough that two rows of two
     words each read as a list of nothing */
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
}
.readout__bars { display: flex; gap: 3px; }
.readout__bars i {
  width: 4px;
  height: 9px;
  border-radius: 1px;
  background: var(--os-accent);
  opacity: 0.22;
  animation: bar-fill var(--motion-slow) var(--ease-enter) forwards;
}
/* they fill left to right, like something connecting */
.readout__bars i:nth-child(1) { animation-delay: 0.10s; }
.readout__bars i:nth-child(2) { animation-delay: 0.22s; height: 11px; }
.readout__bars i:nth-child(3) { animation-delay: 0.34s; height: 13px; }
.readout__bars i:nth-child(4) { animation-delay: 0.46s; height: 15px; }
.readout__bars i:nth-child(5) { animation-delay: 0.58s; height: 17px; }
@keyframes bar-fill { to { opacity: 1; } }
.readout__k--2 { margin-left: var(--space-md); }
/* the poke: bars stutter like a real one re-acquiring, number flashes the accent */
.readout { cursor: pointer; }
.readout[data-pinged] .readout__bars i { animation: readout-ping 0.62s var(--ease-standard); }
.readout[data-pinged] .readout__v { animation: readout-flash 0.62s var(--ease-standard); }
@keyframes readout-ping {
  0% { opacity: 1; }
  14% { opacity: 0.2; }
  38% { opacity: 1; }
  56% { opacity: 0.28; }
  100% { opacity: 1; }
}
@keyframes readout-flash {
  0% { color: var(--os-accent); }
  100% { color: var(--os-ink); }
}
.readout__v { color: var(--os-ink); font-variant-numeric: tabular-nums; }

.vitals {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-top: var(--space-sm);
}
/* the scope is a window; the trace inside it is twice as wide as the window and
   slides by exactly half its width, so the loop has no seam */
.vitals__scope { flex: 1 1 auto; min-width: 40px; overflow: hidden; height: 16px; }
.vitals__scope svg {
  display: block;
  width: 200%;
  height: 100%;
  animation: vitals-scroll 2.2s var(--ease-linear) infinite;
}
.vitals__scope path {
  fill: none;
  stroke: var(--os-accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.85;
  transition: transform var(--motion-exit) var(--ease-standard);
  transform-origin: center;
}
@keyframes vitals-scroll { to { transform: translateX(-50%); } }
/* lean in and it quickens, the way a pulse does when someone is watching */
.vitals:hover .vitals__scope svg { animation-duration: 0.9s; }
.vitals:hover .vitals__scope path { transform: scaleY(1.5); opacity: 1; }
.vitals__label {
  flex-shrink: 0;
  margin: 0;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
}
.vitals__label b { color: var(--os-ink); font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
  /* a flat line would say the wrong thing, so it holds a waveform instead */
  .vitals__scope svg { animation: none; }
  .vitals:hover .vitals__scope path { transform: none; }
  .readout__bars i { animation: none; opacity: 1; }
}

/* The mood ring's contact row is a row; the read_me tail's is a column. That is
   the whole of this rule now.
   It used to be twice this size, and two of its four declarations were undoing
   style.css rather than saying anything: `.contact` was declared unscoped there
   for the classic landing page AND here for the mood ring, both sheets load on
   this page, and the OS rule overrode `display` and `align-items` but not
   `flex-direction`, `text-align` or `padding` -- so what rendered was half of
   each and this rule had to name all four to win. style.css scopes its version
   to `.classic` now, which is what the rest of that component already did, so
   `text-align` and `padding-top` have nothing left to undo. */
.readme-tail .contact {
  flex-direction: column;
  align-items: flex-start;
}
.readme-tail .contact__row {
  justify-content: flex-start;
  align-items: center;
  /* the glyph row and the sentence explaining it are one thought; --space-lg
     under a 38px row of tiles opened a hole the column could not justify */
  margin-bottom: var(--space-xs);
}
.readme-tail .contact__note { margin-top: 0; }
.readme-tail .contact__handle,
.readme-tail .contact__note { text-align: left; }

/* --------------------------------------------------------------------------
   8. WINDOWS  (z 10-39)
   -------------------------------------------------------------------------- */

.win {
  position: absolute;
  display: none;
  flex-direction: column;
  width: 440px;
  min-width: 300px;
  max-width: 92vw;
  border-radius: var(--radius-xl);
  overflow: hidden;
  z-index: var(--z-win);
  background: var(--os-surface);
  backdrop-filter: var(--glass-window);
  -webkit-backdrop-filter: var(--glass-window);
  border: 1.5px solid var(--glass-edge);
  box-shadow: var(--elev-window) var(--os-shadow), inset 0 1px 0 var(--glass-spec), inset 0 0 0 1px #ffffff1f;
  color: var(--os-ink);
}
.win[data-open] { display: flex; animation: win-pop var(--motion-enter) var(--ease-standard); }
.win[data-closing] { animation: win-close var(--motion-exit) var(--ease-standard) forwards; }
/* 0.7, not 0.94, and an ease that overshoots slightly. At 0.94 the pop was a
   twitch; now that it grows out of the icon it is travelling a real distance and
   wants a real curve. transform-origin is set per open in os.js. */
@keyframes win-pop { from { transform: scale(0.7); opacity: 0; } }
.win[data-open] { animation-timing-function: cubic-bezier(.2, .8, .25, 1); }
@keyframes win-close { to { transform: scale(0.9); opacity: 0; } }

/* Windows behind the front one step back.
   Marking the focused window was not enough on its own: with four cards open at
   the same weight, a gradient border 1.5px wide is not a signal anyone reads.
   So the ones behind yield instead. The title bar loses its colour, the shadow
   tightens from 46px of spread to 14px, and the card dims a touch. Stack depth
   becomes legible at a glance rather than on inspection.

   Deliberately on .tbar rather than on .win: .win carries backdrop-filter, and
   stacking a filter on the same element makes the blur sample the wrong
   backdrop in WebKit. The shadow and the dim are properties that compose
   safely. */
.win:not([data-focus]) .tbar {
  filter: saturate(0.28) brightness(0.97);
}
.win:not([data-focus]) {
  box-shadow: var(--elev-window-dim) var(--os-shadow), inset 0 1px 0 var(--glass-spec);
}
.win:not([data-focus]) .wbody { opacity: 0.88; }

/* focus ring: the site's own card frame gradient as a border, plus a glow */
.win[data-focus] {
  border-color: transparent;
  background-image:
    linear-gradient(var(--os-surface), var(--os-surface)),
    var(--t-art);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow: var(--elev-window) var(--os-shadow), var(--glow-focus) var(--t-deco), inset 0 0 0 2px var(--os-inset);
}

.tbar {
  display: flex;
  align-items: center;
  gap:  var(--space-md);
  flex-shrink: 0;
  padding:  var(--space-md) var(--space-xl);
  cursor: grab;
  background: var(--t-bar);
  border-bottom: 1px solid var(--t-bar-chip);
}
/* Three lights, left, the way a window has looked since 2001. The decorative
   dot that used to sit here said nothing and the close button sat alone at the
   other end; a title bar that offers one verb is a title bar people do not
   read. Each of these does something real -- close, collapse, maximise --
   because a control that does nothing is a lie about the software, the same
   rule that keeps a mute button off the chart page.

   Not red/amber/green. The hues are the site's own strawberry, peach and mint;
   they read as traffic lights from the arrangement, not from the colours. */
.tbar__lights {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-shrink: 0;
}
.tlight {
  position: relative;
  width: 13px;
  height: 13px;
  padding: 0;
  border: none;
  border-radius: var(--radius-circle);
  cursor: pointer;
  box-shadow: inset 0 0 0 1px #00000014, inset 0 1px 0 #ffffff8c;
  transition: filter var(--motion-state) var(--ease-standard);
}
.tlight--close { background: var(--tl-close); }
.tlight--shade { background: var(--tl-shade); }
.tlight--zoom  { background: var(--tl-zoom); }
/* the glyph appears only while the pointer is over the cluster, which is the
   convention and keeps three symbols out of a title bar at rest */
.tlight::after {
  content: '';
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font: 800 9px/1 var(--font-body);
  color: #00000078;
  opacity: 0;
  transition: opacity var(--motion-state) var(--ease-standard);
}
.tbar__lights:hover .tlight::after,
.tlight:focus-visible::after { opacity: 1; }
.tlight--close::after { content: '\00d7'; font-size: 11px; }
.tlight--shade::after { content: '\2013'; }
.tlight--zoom::after  { content: '\002b'; }
.win[data-zoom] .tlight--zoom::after { content: '\2013'; }
.tlight:hover { filter: brightness(1.08); }
.tlight:focus-visible { outline: 2px solid var(--t-bar-ink); outline-offset: 2px; }

/* Collapsed to its own title bar: a real classic-OS verb, and the cheapest way
   to see what a window is sitting on top of without closing it. */
.win[data-shaded] .wbody,
.win[data-shaded] .wtabs,
.win[data-shaded] .resize { display: none; }
.win[data-shaded] { height: auto !important; }

/* Maximised. The geometry is set inline by os.js from the actual furniture --
   the menu bar's bottom, the dock's top -- because those move between
   breakpoints and a percentage does not. This rule only removes what would
   otherwise stop it. */
.win[data-zoom] { max-width: none; border-radius: var(--radius-lg); }
.win[data-zoom] .resize { display: none; }
.tbar__title {
  flex: 1;
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--t-bar-ink);
}


/* foil rides the focused titlebar only (restraint system) */
.win[data-focus] .tbar { position: relative; isolation: isolate; overflow: hidden; }
.win[data-focus] .tbar::after {
  content: '';
  position: absolute;
  inset: -15%;
  pointer-events: none;
  opacity: 0.8;
  mix-blend-mode: overlay;
  background-image: repeating-linear-gradient(100deg,
    transparent 0 8%, rgba(255, 255, 255, 0.5) 11%, transparent 14% 30%,
    rgba(196, 240, 255, 0.55) 33%, transparent 36% 55%,
    rgba(255, 214, 240, 0.55) 58%, transparent 61% 80%);
  background-size: 240% 240%;
  background-position: var(--holo-x, 50%) var(--holo-y, 50%);
  transition: background-position var(--motion-exit) var(--ease-standard);
}

/* flex body so terminal and any other flexible app fill a resized window */
/* .button is display:flex, so it is a block-level box and text-align on the
   wrapper cannot move it. Shrink it to its content and let auto margins do the
   centring: a single CTA sitting hard against the left edge reads as a bug even
   when the copy above it is centred. */
.wbody .button {
  width: fit-content;
  margin-left: auto;
  margin-right: auto;
}

/* ---- the tabbed window ---------------------------------------------------
   A tab is the visible EDGE of the surface behind it, not a button parked near
   it. Three things make that read, and all three were missing: the active tab
   must be CONTINUOUS with the panel (one surface, no seam), a baseline must run
   under the strip for the active tab to BREAK THROUGH, and the inactive ones
   must SIT BACK so the set reads as layered rather than as equals in a row. */
.wtabs {
  position: relative;
  display: flex;
  gap:  var(--space-xs);
  padding:  var(--space-md) var(--space-xl) 0;
  flex-shrink: 0;
  /* Its own recessed band, so the strip reads as chrome the tabs are cut out
     of rather than as buttons floating on the body. Mixed from --os-ink, which
     is dark on light palettes and light on dark ones, so one declaration
     darkens or lightens correctly across all twenty. */
  background: rgba(80, 60, 130, 0.10);
  background: color-mix(in srgb, var(--os-ink) 9%, transparent);
}
/* the baseline. The active tab covers its own slice of it, which is the whole
   trick -- that break is what says "this tab owns the surface below". */
.wtabs::after {
  content: '';
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 0;
  height: 1px;
  background: var(--glass-edge);
}
.wtab {
  position: relative;
  z-index: 1;                       /* over the baseline */
  flex: 0 1 auto;
  white-space: nowrap;
  padding:  var(--space-sm) var(--space-lg) var(--space-md);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: transparent;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-tight);
  color: var(--os-ink);
  opacity: .5;
  transform: translateY(3px);       /* sitting behind */
  transition: opacity var(--motion-state) var(--ease-standard), transform var(--motion-state) var(--ease-standard), background-color var(--motion-state) var(--ease-standard);
}
.wtab:hover { opacity: .78; transform: translateY(1px); }
.wtab.is-on {
  opacity: 1;
  transform: none;                  /* comes forward, onto the baseline */
  background: var(--os-glass);
  overflow: hidden;
  /* the lit top edge of a glass surface, and a hairline down each side so the
     tab reads as a raised lip rather than a filled rectangle */
  box-shadow:
    inset 0 1px 0 var(--glass-spec), inset 1px 0 0 var(--glass-edge), inset -1px 0 0 var(--glass-edge);
  margin-bottom:  calc(var(--space-hair) * -1);              /* overlaps the body, hiding the seam */
  padding-bottom:  var(--space-md);
}
/* the foil catches only on the active tab: state you can see without reading */
.wtab.is-on::after {
  content: '';
  position: absolute;
  /* Oversized and clipped by the tab, because the loop below TRANSLATES this
     layer. A background-position loop cannot be seamless here: the gradient
     tile is the element's own box, so any offset drags the tile's edge into
     view and the next tile restarts the ramp -- that edge was the horizontal
     seam. Moving the whole layer has no tile edge to expose. */
  inset: -60px;
  pointer-events: none;
  /* Stops in px, not %, so the repeat period is a known constant (48px)
     independent of how wide the tab renders. */
  background-image: repeating-linear-gradient(
    112deg,
    transparent 0 5px,
    rgba(255, 138, 224, 0.30) 8px,
    rgba(255, 255, 255, 0.42) 10px,
    rgba(120, 190, 255, 0.32) 12px,
    rgba(150, 255, 235, 0.22) 15px,
    transparent 19px 48px);
  animation: wtab-foil 7.8s var(--ease-linear) infinite;
  will-change: transform;
}
/* Exactly one period along the gradient's own axis: 48 x (sin 112 deg,
   -cos 112 deg). The end state is pixel-identical to the start, so the
   restart is invisible rather than a jump. */
@keyframes wtab-foil {
  from { transform: translate(0, 0); }
  to   { transform: translate(44.505px, 17.981px); }
}
@media (prefers-reduced-motion: reduce) {
  .wtab.is-on::after { animation: none; }
}
.wtab > span { position: relative; z-index: 1; }   /* label above the sheen */

.wtab:focus-visible { outline: 2px solid var(--os-accent); outline-offset: -3px; }

/* the panel is a surface, so the active tab has something to be continuous WITH */
.wbody--tabbed {
  background: var(--os-glass);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  box-shadow: inset 0 1px 0 var(--glass-edge);
  padding-top:  var(--space-xl);                /* tabs were sitting on the first line */
}

.wtab__short { display: none; }     /* desktop keeps the .exe joke */

/* Phone: scoped to the borrowed nodes rather than a media query, so it tracks
   where the tabs actually ARE. Four labels do not fit at full length. */
#appbody .wtab {
  flex: 1 1 0;
  min-width: 0;
  padding:  var(--space-md) var(--space-hair) var(--space-md);
  text-align: center;
  font-size: clamp(8px, 2.6vw, 10px);
  letter-spacing: 0;
}
#appbody .wtab.is-on { padding-bottom:  var(--space-md); }
/* The body is a rounded, inset surface on the desktop because it sits inside a
   window. In the phone shell it IS the screen, so the radius and the inset read
   as a stray pale border along the bottom. */
/* Both classes, not just --tabbed: `#appbody .wbody` further down sets
   `padding: 0` at the same specificity and would win on document order. */
#appbody .wbody.wbody--tabbed {
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  /* #appbody pads 16/18; the strip and body bleed back out through it so the
     chrome runs edge to edge like a real tab bar, then re-pad their own text. */
  margin:  0 calc(var(--space-xl) * -1);
  padding:  var(--space-xl) var(--space-xl) 0;
}
#appbody .wtabs {
  gap:  var(--space-hair);
  border-radius: 0;
  margin:  calc(var(--space-xl) * -1) calc(var(--space-xl) * -1) 0;
  padding:  var(--space-lg) var(--space-lg) 0;
}
#appbody .wtabs::after { left: 12px; right: 12px; }
#appbody .wcta { margin-left:  calc(var(--space-xl) * -1); margin-right:  calc(var(--space-xl) * -1); }
#appbody .wtab__full { display: none; }
#appbody .wtab__short { display: inline; }

/* The read_me intro is the one block on the site people actually read as
   prose, and a greedy wrap left it with a long line over a short one. `balance`
   asks the browser to even the line lengths out instead, which is the thing
   that was actually wanted -- and unlike a hard break or an &nbsp, it stays
   right at every window width and on the phone. Ignored by browsers that do
   not have it, which just get today's wrap. */
.lede { text-wrap: balance; }

/* A kaomoji is assembled from Thai, Armenian and Arabic codepoints, so every
   glyph in it comes from a different fallback face at a different optical
   size. Nudging the run as a whole is all that can be done without splitting
   it per character, and it settles the paws against the surrounding text. */
.kao {
  font-size: 1.1em;
  vertical-align: -.04em;
  letter-spacing: .02em;
  white-space: nowrap;
}

/* ---------- faq ----------
   A <details> row. The marker is drawn as a rotating chevron on ::after so the
   platform's own triangle (which is a sharp one, and inconsistent between
   Safari and Chrome) never appears. */
#faq-list { margin-top:  var(--space-xl); }
#faq-list .section-heading { margin-top:  var(--space-2xl); }
#faq-list .section-heading:first-child { margin-top: 0; }

.faq {
  border-bottom: 1px solid var(--glass-edge);
}
.faq:last-of-type { border-bottom: 0; }

.faq__q {
  position: relative;
  list-style: none;                 /* firefox */
  padding:  var(--space-lg) var(--space-3xl) var(--space-lg) var(--space-hair);
  font-weight: 800;
  color: var(--os-ink);
  cursor: pointer;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q::after {
  content: '';
  position: absolute;
  right: 8px;
  top: 50%;
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--os-accent);
  border-bottom: 2px solid var(--os-accent);
  border-radius: 0 0 var(--radius-xs) 0;
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--motion-exit) var(--ease-standard);
}
.faq[open] .faq__q::after { transform: translateY(-30%) rotate(225deg); }
.faq__q:hover { color: var(--os-accent); }
.faq__q:focus-visible { outline: 2px solid var(--os-accent); outline-offset: 2px; border-radius: var(--radius-sm); }

.faq__a {
  margin: 0;
  padding:  0 var(--space-3xl) var(--space-lg) var(--space-hair);
  color: var(--os-ink-dim);
  line-height: 1.55;
}
.faq__a b { color: var(--os-ink); }

@media (prefers-reduced-motion: reduce) {
  .faq__q::after { transition: none; }
}

/* the conversion action, pinned under every tab rather than living in one */
/* A rule above the persistent CTA, so it reads as "and here is how to talk"
   rather than as a second button competing with whatever the panel offers --
   which is what made the resume tab look like two equal CTAs stacked. */
.wcta {
  /* 8 and 8, not 24 and 16. The rule above the button already separates it from
     the panel, so the 40px of air on top of that rule was doing the same job
     twice and reading as the panel having run out rather than having arrived at
     its point. */
  margin:  var(--space-md) calc(var(--space-xl) * -1) 0;
  padding:  var(--space-md) var(--space-xl) var(--space-hair);
  text-align: center;
  border-top: 1px solid var(--glass-edge);
}

/* ---- where i am ---------------------------------------------------------- */
.contact__row {
  display: flex;
  justify-content: center;
  gap:  var(--space-lg);
  margin-bottom:  var(--space-lg);
}
.contact__glyph {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--radius-md);
  background: var(--os-glass);
  box-shadow: inset 0 0 0 1px var(--glass-edge);
  color: var(--os-ink);
  transition: transform var(--motion-state) var(--ease-standard), background-color var(--motion-state) var(--ease-standard);
}
.contact__glyph svg { width: 17px; height: 17px; fill: currentColor; }
.contact__glyph:hover { transform: translateY(-2px); background: var(--os-surface); }
.contact__glyph:active { transform: translateY(0) scale(.95); }
.contact__handle {
  display: block;
  text-align: center;
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  color: var(--os-ink);
}
.contact__note {
  margin:  var(--space-sm) 0 0;
  text-align: center;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  color: var(--os-ink);
  opacity: .66;
}

.wbody {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  padding:  var(--space-xl) var(--space-2xl) var(--space-xl);
  /* As tall as the desktop actually allows, not a flat 410px.
     410 was capping five of the nine windows while 641px of desktop sat empty
     between the menu bar and the dock, and on read_me the 231px it hid was the
     contact block: the one conversion point in the window a hiring manager
     opens first was below a fold with nothing to say it was there.

     The calc is the furniture, measured: menu bar 34 + the 26 placeWin() drops
     below it + a 102 dock + 81 of title bar and tabs, less the slack that turned
     out to be there when the window was measured against the dock rather than
     computed: 18px of desk was still going spare at 240. Earlier values of 260
     and 240 were arithmetic; this one is what fits. The
     72vh term does nothing at ordinary sizes and exists so a very tall display
     does not produce a 900px wall of window. Whichever is smaller wins, so the
     rule is "fit the desk, but never get silly". */
  max-height: min(72vh, calc(100vh - 228px));
  overflow: auto;
  font-size: var(--text-lead);
  font-weight: 400;
  line-height: 1.65;
}
/* AN UNTABBED WINDOW DOES NOT PAY FOR A TAB STRIP. The number above was
   measured against read_me, the one window that has one, and then applied to
   all twelve -- so the other eleven were being capped 39px shorter than the
   desk actually allows, and quote.exe was scrolling on any display under about
   810px tall for room that was sitting there unused. Measured: tab strip 39,
   which is the whole difference between the two expressions. */
.win .wbody:not(.wbody--tabbed) { max-height: min(78vh, calc(100vh - 189px)); }
.win[data-resized] .wbody { max-height: none; }
/* A TABBED body is a fixed height, not a maximum. Four tabs of different lengths
   in one window meant the window changed shape every time you clicked one, and
   the thing that moved with it was the request-a-quote button, which lives at
   the bottom of the body and is shared by all four. A conversion point that
   jumps 300px when you switch tabs is the one element that must not move. The
   height is the same expression the cap computes, so nothing new is decided
   here; a short tab now gets space to compose into instead of a collapse. */
/* `.win` in the selector on purpose. On the phone os.js MOVES this element into
   #appbody rather than cloning it, and there the app view is full screen: a
   fixed desktop-window height would follow the node across and cap the phone's
   app at 590px inside an 852px screen. Scoping it to the window means the
   element only carries the height while it is in one. */
.win .wbody--tabbed { height: min(72vh, calc(100vh - 228px)); }
.win[data-resized] .wbody--tabbed { height: auto; }
/* A MAXIMISED WINDOW HAS AN EXPLICIT HEIGHT, so the body must fill it rather
   than keep the height it was given for a desktop-sized window. Without this the
   tabbed body stopped at its own 72vh and left a bare strip of window between
   the end of the content and the bottom edge -- the one place a maximised window
   is guaranteed to be looked at closely. flex, not height:100%, because .win is
   a flex column and the title bar above it is a real row. */
.win[data-zoom] .wbody { max-height: none; }
.win[data-zoom] .wbody--tabbed { height: auto; flex: 1 1 auto; min-height: 0; }
/* Now that the body is a fixed height, a short tab has slack, and where that
   slack goes is a decision. grow-but-never-shrink on the panel and auto on the
   button puts it at the bottom: the quote button lands on the same pixel in all
   four tabs, which was the whole reason for fixing the height, and the spare
   space becomes margin inside the panel rather than a hole beneath it. */
.win .wbody--tabbed .wpanel:not([hidden]) { flex: 1 0 auto; }
.win .wbody--tabbed .wcta { margin-top: auto; }
/* the resume is one object, so it sits in the middle of whatever room it has
   rather than clinging to the top of it */
.win .wbody--tabbed [data-panel="resume"] { display: flex; flex-direction: column; justify-content: center; }
.wbody::-webkit-scrollbar { width: 9px; }
.wbody::-webkit-scrollbar-thumb {
  background: var(--lilac);
  border-radius: var(--radius-pill);
  border: 2px solid #ffffff88;
}
.wbody img { max-width: 100%; }
.wbody > * { flex-shrink: 0; }
.wbody .section-heading__label { font-size: var(--text-caption); color: var(--os-accent); }
.wbody .section-heading { margin:  var(--space-xl) 0 var(--space-md); }
.wbody .section-heading:first-child { margin-top: 0; }
.wbody p + p { margin-top:  var(--space-md); }

.resize {
  position: absolute;
  right: 2px;
  bottom: 2px;
  width: 18px;
  height: 18px;
  z-index: 5;
  opacity: 0.7;
  cursor: nwse-resize;
  border-right: 3px solid var(--lilac);
  border-bottom: 3px solid var(--lilac);
  border-radius: 0 0 var(--radius-md) 0;
}

/* sticky notes — file > new delusion. The visitor's own notes persist. */
.sticky {
  position: fixed;
  z-index: var(--z-note);
  width: 152px;
  padding:  var(--space-xl) var(--space-lg) var(--space-xl);
  border-radius: var(--radius-xs) var(--radius-xs) var(--radius-lg) var(--radius-xs);
  background: linear-gradient(180deg, #fff6fb, #fde7f4);
  box-shadow: 0 10px 24px #5a3f8f40;
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  color: #a15a8e;
  transform: rotate(var(--tilt, -2deg));
}
.sticky::before {
  content: '';
  position: absolute;
  top: -7px;
  left: 50%;
  width: 54px;
  height: 14px;
  transform: translateX(-50%) rotate(2deg);
  background: rgba(211, 238, 251, 0.85);
  border-radius: var(--radius-xs);
  box-shadow: 0 1px 3px #5a3f8f30;
}
.sticky button {
  position: absolute;
  top: 3px;
  right: 7px;
  border: none;
  background: none;
  color: #d8a5c8;
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
}
.sticky .txt { display: block; margin-top:  var(--space-hair); min-height: 16px; outline: none; }

/* --------------------------------------------------------------------------
   9. DOCK  (z 40)
   -------------------------------------------------------------------------- */

#dock {
  position: fixed;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: var(--z-dock);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap:  var(--space-md);
  max-width: 96vw;
  padding:  var(--space-md) var(--space-xl);
  border-radius: var(--radius-pill);
  /* .glass owns the surface */
}


.dbtn[data-tip]:hover::after {
  content: attr(data-tip);
  position: absolute;
  bottom: 125%;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  background: #4c4a70;
  color: #fff;
  padding:  var(--space-xs) var(--space-md);
  border-radius: var(--radius-pill);
}
.dsep { width: 2px; height: 20px; border-radius: var(--radius-xs); background: var(--os-border); }
#dock-dm {
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--on-cta);
  text-decoration: none;
  background: var(--t-cta);
  padding:  var(--space-md) var(--space-xl);
  border-radius: var(--radius-pill);
  box-shadow: var(--elev-raised) var(--os-shadow), inset 0 2px 4px #ffffff80;
  transition: transform var(--motion-state) var(--ease-standard);
}
#dock-dm:hover { transform: scale(1.08); }

/* open-window chips */
#dock-open { display: flex; gap:  var(--space-sm); }
#dock-open:empty { display: none; }
#dock-open:empty + .dsep { display: none; }
.chip {
  position: relative;
  font-size: var(--text-lead);
  background: var(--os-pill);
  border: 1.5px solid var(--os-border-strong);
  border-radius: var(--radius-md);
  padding:  var(--space-xs) var(--space-md);
  transition: transform var(--motion-state) var(--ease-standard);
}
.chip:hover { transform: scale(1.15) translateY(-2px); }
.chip::after {
  content: '';
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  height: 4px;
  border-radius: var(--radius-circle);
  background: var(--os-accent);
}

/* A reduced glass tier, the way .holo-surface--pill is a reduced .holo-surface.
   A full 34px blur inside an already-glass panel samples glass through glass
   and mushes both; a chip wants the hairline and the specular, not the depth. */




/* 13/13. The copy promises "permanent halo privileges", and until now nothing
   rendered it: the .angel rules that used to do this became body[data-konami]
   when konami mode took them over, leaving earn() adding a class with no styles
   behind it.

   PLATINUM, NOT GOLD. This block used to argue the opposite -- "warm rather
   than platinum on purpose", on the grounds that the copy said "the os is
   GOLDEN now". That string says "blessed" now, so the reason had outlived
   itself, and the halo was the last gold left in a palette that had already
   decided against it: the card and all thirteen patches ship the PLAT ramp in
   os.js, under a comment that reads "warm gold is a foreign object in a palette
   this cool, and the system already reserves gold for the covenant hour itself
   (--pulse goes warm only at 13:33)". Two files, opposite conclusions, and the
   halo was the one that was alone.

   Same ramp as the card, composed as light rather than as a gradient: a
   near-white core so it separates on the dark skies, a violet bloom so it
   separates on the pale ones. On archangel it half-dissolves into the theme,
   which is correct -- that is the same achievement wearing itself. */
body.angel #wordmark {
  filter:
    drop-shadow(0 0 1px rgba(118, 84, 172, 0.9))
    drop-shadow(0 2px 3px rgba(118, 84, 172, 0.6))
    drop-shadow(0 0 10px rgba(247, 243, 255, 0.92))
    drop-shadow(0 6px 20px rgba(186, 150, 238, 0.7));
}
body.angel .icon__art {
  box-shadow:
    inset 0 1px 0 var(--glass-spec), inset 0 0 0 1px rgba(197, 180, 228, 0.75), var(--elev-raised) var(--os-shadow), 0 0 18px rgba(186, 150, 238, 0.45);
}
body.angel .hire { box-shadow: inset 0 0 0 1px rgba(197, 180, 228, 0.85); }

/* AVAILABLE FOR WORK
   The middle of the bar, alone. The left is navigation and the right is
   machine status (her clock, the sound, the time); this is neither. It is the
   only element on the site addressed to an employer rather than to a visitor,
   and clustering it with the status icons would file it as one more of them.
   An empty centre is also the most-looked-at spot on any menu bar.

   It opens resume.exe, because someone who clicks "available for work" wants
   to know what she does and how to reach her, and that window holds both. */
/* The look is on the class so the phone chip inherits it; only the desktop
   centring stays on the id, because the phone bar centres it with flex. */
#hire {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
#hire:hover { transform: translateX(-50%) translateY(-1px); }

.hire {
  display: flex;
  align-items: center;
  gap:  var(--space-md);
  padding:  var(--space-xs) var(--space-lg) var(--space-xs) var(--space-lg);
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  /* --os-ink, not --t-bar-ink. The latter dates from when the menu bar painted
     its own gradient; the bar now sits on .glass and takes --os-ink, which
     flips with the MODE as well as the theme. The chip kept the old token, so
     on four of five themes it stayed dark while the bar around it went light.
     #menubar and #pstatus both read --os-ink, so this is literally "whatever
     the menu text is". */
  color: var(--os-ink);
  background: color-mix(in srgb, #7ee0ab 22%, transparent);
  background: rgba(126, 224, 171, 0.22);
  box-shadow: inset 0 0 0 1px rgba(126, 224, 171, 0.55);
  transition: background var(--motion-state) var(--ease-standard), transform var(--motion-state) var(--ease-standard);
}
.hire:hover { background: rgba(126, 224, 171, 0.34); }
.hire .status-dot { width: 8px; height: 8px; flex-shrink: 0; }
#hire[data-tip]:hover::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  padding:  var(--space-sm) var(--space-lg);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  color: #fff;
  background: #3a2a63e6;
  pointer-events: none;
}
/* There WAS a rule here dropping the chip to a bare dot below 1080px, on the
   theory that the bar gets crowded before it gets narrow. Measured, the bar
   does not: its in-flow children come to 505px at every width, so at 1000px the
   label was being hidden with 495px of the bar standing empty. The chip is the
   most important thing in this bar -- the whole site is a hire-me -- so its
   label should be the last thing to go, not the first. It now survives all the
   way to 880, where the chip itself steps aside for the phone shell's own. */
/* was `#hire { display: none }` here -- the desktop chip is genuinely not
   wanted at phone widths, but the phone shell now carries its own. */
/* 800, not 880. The chip is the one conversion point in the menu bar and an
   820pt iPad was inside the old cut by 60px. Measured rather than guessed: at
   800 the bar still holds the five menus, the chip and the clock without the
   chip touching either neighbour. Below that it does, so below that it goes. */
/* 700, not 800. Below 700 the phone shell takes over and `.hire--phone` carries
   this, and above 800 the menu bar has always had room -- but between the two
   the desktop shell renders with no hire affordance at all, which is exactly
   the tablet band. The site is an argument for hiring her; the button that
   closes it should not be the first thing responsive design throws away. */
@media (max-width: 700px), (max-height: 500px) { #hire { display: none; } }

/* THE RECEIPT. Set like one: mono throughout, a dashed rule top and bottom,
   labels left and values right, and a paper that is lighter than the window it
   is printed into. An OS that hands you a docket is more memorable than a green
   checkmark, and it turns "replies within 24h" from a claim in the FAQ into a
   promise made at the point of sale. */
.receipt {
  margin-top:  var(--space-lg);
  padding:  var(--space-2xl) var(--space-xl);
  border-radius: var(--radius-lg);
  background: var(--os-inset);
  box-shadow: inset 0 0 0 1px var(--os-border), 0 8px 24px var(--os-shadow);
  text-align: center;
  font: 400 var(--text-body)/1.7 var(--font-mono);
  color: var(--os-ink);
}
.receipt__head {
  font: 700 var(--text-lead)/1.4 var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-accent);
}
.receipt__sub { color: var(--os-ink-dim); margin-bottom:  var(--space-lg); }
.receipt__rows {
  display: grid;
  grid-template-columns: auto 1fr;
  gap:  var(--space-xs) var(--space-lg);
  text-align: left;
  margin: 0;
  padding:  var(--space-lg) 0;
  border-top: 1px dashed var(--os-border-strong);
  border-bottom: 1px dashed var(--os-border-strong);
}
.receipt__rows dt {
  font-size: var(--text-caption);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
  white-space: nowrap;
}
/* the note can be a paragraph, so it wraps rather than stretching the column */
.receipt__rows dd { margin: 0; overflow-wrap: anywhere; }
.receipt__foot { margin-top:  var(--space-lg); color: var(--os-ink-dim); font-size: var(--text-caption); }
.receipt__acts {
  display: flex;
  align-items: center;
  justify-content: center;
  gap:  var(--space-lg);
  margin-top:  var(--space-lg);
  flex-wrap: wrap;
}
/* three doors, weighted the same */
.quote-doors {
  display: flex;
  align-items: center;
  justify-content: center;
  gap:  var(--space-sm);
  font: 400 var(--text-caption)/1.6 var(--font-mono);
  color: var(--os-ink-dim);
}
/* The marks themselves, at the size a tertiary control wants. Smaller and
   flatter than read_me's .contact__glyph, which is the primary contact row and
   earns a 38px tile; here they sit beside a sentence. */
.door-row { display: flex; gap: var(--space-xs); }
.door {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  color: var(--os-ink-dim);
  transition: transform var(--motion-state) var(--ease-standard),
              background-color var(--motion-state) var(--ease-standard),
              color var(--motion-state) var(--ease-standard);
}
.door svg { width: 15px; height: 15px; fill: currentColor; }
.door:hover { background: var(--os-pill); color: var(--os-accent); transform: translateY(-2px); }
.door:active { transform: translateY(0) scale(.94); }

.field--note { resize: vertical; min-height: 68px; font-family: var(--font-body); }

/* THE CAMERA, ASKED FOR. openCapture had exactly one caller -- this button,
   labelled PRINT SCREEN, which is what the machine does rather than what the
   visitor wants. When something worth keeping happens, the toast says so and
   the button answers by ringing for a few seconds. Nothing opens on its own. */
.dbtn[data-nudge] { animation: dock-ring 1.1s var(--ease-standard) 4; }
@keyframes dock-ring {
  0%, 100% { box-shadow: 0 0 0 0 var(--t-deco); }
  40% { box-shadow: 0 0 0 7px transparent; }
}
/* Same gesture, one row further in. The theme panel opens this menu, and a
   menu that drops open with five swatches in it has not actually pointed at
   anything; this is the pointing. */
.swatch[data-nudge] { animation: dock-ring 1.1s var(--ease-standard) 3; border-radius: var(--radius-sm); }
#toast[data-action] { cursor: pointer; }
@media (prefers-reduced-motion: reduce) {
  .dbtn[data-nudge] { animation: none; outline: 2px solid var(--t-deco); outline-offset: 3px; }
  .swatch[data-nudge] { animation: none; outline: 2px solid var(--t-deco); outline-offset: 2px; }
}

/* ---- the phone status bar: decoration left, the offer centred, controls right */
.pstatus__left { display: flex; align-items: center; gap:  var(--space-md); min-width: 0; }
.pstatus__deco { opacity: .8; letter-spacing: var(--track-wide); font-size: var(--text-caption); }
/* True centring: absolute to the bar itself. Flex space-between only centres a
   middle item when its neighbours are equal widths, and here they never are --
   a clock and four buttons. */
.hire--phone {
  /* #pstatus is pointer-events:none so the wallpaper stays draggable behind it;
     .right opted back in and the chip had not, so it rendered but could not be
     clicked. */
  pointer-events: auto;
}
.hire--phone:active { transform: scale(.97); }
#pstatus .right { justify-self: end; }
/* The offer is the same component here as in the menu bar, so it keeps the same
   padding, tracking and dot. It used to halve them -- 2px against 4, 8px against
   12, tracking tightened -- at the same 10px type, which read as cramped beside
   its desktop twin and put the dot almost on the pill's edge. That squeeze was
   inherited from when the preview shell was 320px wide; the shell is a real
   handset width now and the bar has the room, which the media query below
   measures rather than assumes. */
.hire--phone { white-space: nowrap; }
/* Measured, not guessed: left 75 + offer 97 + controls 98 + padding 32 = 302,
   so the bar fits a 320 screen and no narrower. Two tiers of graceful loss --
   decoration first, then type -- because the offer is the one thing in this bar
   trying to get her hired and it must never be the thing that yields.

   A CONTAINER query, not a media query. The thing that runs out of room is the
   phone's screen, and on a real handset that is the viewport, so a media query
   worked. It stopped working when the preview shell started deriving its width
   from the window's HEIGHT: a short desktop window makes a narrow phone inside a
   1440px viewport, the media query never fires, and the bar collides with itself
   at about 305px of shell. Asking the screen about its own width covers both. */
@container (max-width: 310px) {                                    /* -> ~264 */
  #pstatus { padding:  0 var(--space-lg); }
  #pstatus .right { gap:  var(--space-xs); }
  .hire--phone { font-size: var(--text-micro); padding:  var(--space-hair) var(--space-md) var(--space-hair) var(--space-sm); letter-spacing: var(--track-tight); }
  .hire--phone .status-dot { width: 5px; height: 5px; }
}
/* Below this the decoration is the next thing to go. It is three dots and a
   sparkle; the clock, the offer and the controls all mean something. */
@container (max-width: 264px) {
  .pstatus__deco { display: none; }
  #pstatus .right { gap: 0; }
  #pstatus { padding:  0 var(--space-md); }
}

/* --------------------------------------------------------------------------
   10. TRANSIENT MESSAGES  (bug log #3)
   desktop: toast + character speech bubbles. phone: one notification banner.
   -------------------------------------------------------------------------- */

#toast {
  position: fixed;
  top: 44px;
  left: 50%;
  z-index: var(--z-toast);
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-label);
  /* Glass, so a message belongs to the same material as the bar and the dock.
     That also retires the --t-bar ink workaround: like every other glass
     surface it just takes --os-ink. Clickable, because a message you have
     already read should go when you say so. */
  /* Ink and halo both flip with the palette. White text was readable on the
     five dark combinations and measured 1.14:1 on the fifteen light ones — a
     sheer panel means the text really is sitting on the sky. --os-surface is
     the opposite of --os-ink by construction (a surface is what holds ink), so
     using it as the halo needs no new tokens and covers all twenty. The
     treatment is unchanged: a blurred backdrop, never a hard line. */
  color: var(--os-ink);
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface),
               0 0 16px var(--os-surface);
  padding:  var(--space-md) var(--space-xl);
  border-radius: var(--radius-pill);
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  transform: translate(-50%, -12px);
  transition: opacity var(--motion-exit) var(--ease-standard), transform var(--motion-exit) var(--ease-standard);
}
#toast[data-show] { opacity: 1; transform: translate(-50%, 0); }

/* ---- the badge moment ----
   Same anchor as the toast, because there is only one place on this desktop
   where the machine speaks. Everything else about it is different: a panel
   rather than a line, the patch at full size on the left, and a count on the
   right where a console puts the score.

   It enters from BEHIND the menu bar. The bar is opaque glass at a higher
   z-index, so a panel sliding down from -70px appears to come out of the
   chrome rather than fading in on top of the wallpaper, which is the whole
   trick of the thing it is quoting. */
#achpop {
  overflow: hidden;
  position: fixed;
  top: 44px;
  left: 50%;
  z-index: var(--z-toast);
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  padding: var(--space-md) var(--space-lg) var(--space-md) var(--space-md);
  border-radius: var(--radius-pill);
  color: var(--os-ink);
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, -70px) scale(0.94);
  transition: opacity 320ms var(--ease-standard), transform 420ms var(--ease-enter);
}
#achpop[data-show] { opacity: 1; transform: translate(-50%, 0) scale(1); }
/* THE HALO, shared with the card. Borrowed wholesale from .cer__rays: same
   conic fan, same downward mask, so a badge, a one-of-one card and the 13/13
   ceremony all read as one kind of moment rather than three effects.
   Behind everything and inside the pill, so it lights the panel without
   touching the patch, the type or the count. */
.achpop__rays {
  position: absolute;
  inset: -40% -18% auto -18%;
  height: 190%;
  z-index: 0;
  pointer-events: none;
  border-radius: inherit;
  opacity: 0;
  background: repeating-conic-gradient(from 196deg at 50% -22%,
    rgba(255, 255, 255, 0.5) 0 0.9deg,
    rgba(203, 169, 244, 0.28) 0.9deg 2.1deg,
    transparent 2.1deg 5.4deg);
  -webkit-mask-image: linear-gradient(to bottom, #000 4%, transparent 74%);
  mask-image: linear-gradient(to bottom, #000 4%, transparent 74%);
}
#achpop[data-show] .achpop__rays { animation: achpop-rays 2.6s var(--ease-standard) forwards; }
@keyframes achpop-rays {
  0%   { opacity: 0; transform: rotate(-3deg) scale(1.1); }
  36%  { opacity: 1; }
  100% { opacity: 0.42; transform: rotate(0deg) scale(1); }
}
/* the plate and the words ride above the fan */
.achpop__plate, .achpop__text, .achpop__score { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) {
  #achpop[data-show] .achpop__rays { animation: none; opacity: 0.42; }
}
#achpop[hidden] { display: none; }
/* The panel is pointer-events:none so it never eats a click meant for the
   wallpaper on the way past. The badge variant opts back in, because it is a
   door to badges.sav; the theme variant stays inert. Only while it is actually
   on screen -- a box mid-fade that still swallows clicks is worse than one that
   never took them. */
#achpop[data-open][data-show] { pointer-events: auto; cursor: pointer; }
#achpop[data-open][data-show]:active { transform: translate(-50%, 0) scale(0.98); }

.achpop__plate {
  position: relative;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.achpop__art { display: block; width: 52px; height: 52px; }
.achpop__art[hidden] { display: none; }

/* THE THEME VARIANT. The plate holds the sky it just opened rather than a
   patch: the theme's own swatch gradient -- the exact one you pick it by in the
   view menu -- under the theme's own drifting glyph, borrowed from [data-motif]
   so no palette is written twice. A round window onto a place, which is what a
   theme is here.

   The glyph is a ::after so it can carry a drop-shadow of its own. Two of the
   five motifs are near-white on a near-white sky (prismagical's prism is the
   worst), and on a flat disc they simply vanished; the shadow gives every glyph
   an edge without tinting the four that never needed one. */
.achpop__sky { display: none; }
#achpop[data-theme-pop] .achpop__sky {
  position: relative;
  display: block;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px var(--glass-edge), var(--elev-raised) var(--os-shadow);
}
#achpop[data-theme-pop] .achpop__sky::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--motif-1);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 28px auto;
  filter: drop-shadow(0 1px 2px rgba(61, 42, 99, 0.42));
}
/* the swatch gradients, verbatim from .swatch__chip: the disc IS the chip */
#achpop .achpop__sky[data-motif="base"]       { background: linear-gradient(120deg, #e8d5f7 0%, #d3d3f9 50%, #c0d8f8 100%); }
#achpop .achpop__sky[data-motif="holo"]       { background: linear-gradient(120deg, #fbe6f6 0%, #dfeafd 50%, #dffbf1 100%); }
#achpop .achpop__sky[data-motif="strawberry"] { background: linear-gradient(120deg, #fde6f1 0%, #fbd7e7 50%, #f6dcf5 100%); }
#achpop .achpop__sky[data-motif="arcade"]     { background: linear-gradient(120deg, #221c4a 0%, #2b2159 50%, #1c2a5c 100%); }
#achpop .achpop__sky[data-motif="archangel"]  { background: linear-gradient(120deg, #f6ecff 0%, #e7d3fb 50%, #f2e6ff 100%); }
.achpop__score[hidden] { display: none; }
/* the light that comes off a thing you just won */
.achpop__plate::before {
  content: '';
  position: absolute;
  inset: -10px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--t-deco) 0, transparent 68%);
  opacity: 0;
}
#achpop[data-show] .achpop__plate::before { animation: ach-glow 1500ms var(--ease-standard); }
@keyframes ach-glow {
  0%   { opacity: 0;   transform: scale(0.5); }
  22%  { opacity: .85; transform: scale(1.06); }
  100% { opacity: 0;   transform: scale(1.5); }
}

/* ---- the burst ----
   Sparkles thrown off the patch, on the site's own two glyphs. Each carries a
   bearing and a distance as custom properties, so one keyframe moves all twelve
   and the twelve differ only in the numbers. They are pointer-events:none and
   sit in a zero-size box at the centre of the plate, which means they can
   overflow the panel without giving it a scroll axis or changing its layout.

   Bearings are hand-spread rather than evenly divided: eighteen at exactly 20
   degrees apart reads as a clock face, and a burst should not have a rhythm.
   Distances and sizes are deliberately uncorrelated with the bearing for the
   same reason -- the moment any two of the three line up, the eye finds the
   pattern and it stops being confetti. */
.achpop__burst {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
  pointer-events: none;
}
.achpop__burst i {
  position: absolute;
  left: 0;
  top: 0;
  line-height: 1;
  font-style: normal;
  color: var(--t-deco);
  text-shadow: 0 0 6px var(--os-surface);
  opacity: 0;
  transform: translate(-50%, -50%);
}
#achpop[data-show] .achpop__burst i {
  animation: ach-spark 1150ms var(--ease-standard) forwards;
}
@keyframes ach-spark {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.2) rotate(0deg); }
  18%  { opacity: 1; }
  100% { opacity: 0;
         transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y)))
                    scale(1) rotate(140deg); }
}
.achpop__burst i:nth-child(1) { --x: -3.4px; --y: -95.9px; font-size: 13px; animation-delay: 0ms; }
.achpop__burst i:nth-child(2) { --x: 25.2px; --y: -56.6px; font-size: 9px; animation-delay: 60ms; }
.achpop__burst i:nth-child(3) { --x: 93.0px; --y: -72.6px; font-size: 16px; animation-delay: 20ms; }
.achpop__burst i:nth-child(4) { --x: 76.3px; --y: -16.2px; font-size: 10px; animation-delay: 100ms; }
.achpop__burst i:nth-child(5) { --x: 134.6px; --y: 38.6px; font-size: 12px; animation-delay: 40ms; }
.achpop__burst i:nth-child(6) { --x: 63.3px; --y: 61.1px; font-size: 8px; animation-delay: 130ms; }
.achpop__burst i:nth-child(7) { --x: 36.9px; --y: 101.5px; font-size: 15px; animation-delay: 10ms; }
.achpop__burst i:nth-child(8) { --x: -9.2px; --y: 65.4px; font-size: 9px; animation-delay: 80ms; }
.achpop__burst i:nth-child(9) { --x: -74.1px; --y: 101.9px; font-size: 11px; animation-delay: 50ms; }
.achpop__burst i:nth-child(10) { --x: -74.2px; --y: 39.4px; font-size: 13px; animation-delay: 150ms; }
.achpop__burst i:nth-child(11) { --x: -151.9px; --y: 5.3px; font-size: 10px; animation-delay: 30ms; }
.achpop__burst i:nth-child(12) { --x: -65.8px; --y: -29.3px; font-size: 8px; animation-delay: 110ms; }
.achpop__burst i:nth-child(13) { --x: -66.9px; --y: -74.3px; font-size: 14px; animation-delay: 70ms; }
.achpop__burst i:nth-child(14) { --x: -23.6px; --y: -53.0px; font-size: 9px; animation-delay: 20ms; }
.achpop__burst i:nth-child(15) { --x: 22.9px; --y: -130.0px; font-size: 12px; animation-delay: 90ms; }
.achpop__burst i:nth-child(16) { --x: 56.6px; --y: -72.5px; font-size: 16px; animation-delay: 140ms; }
.achpop__burst i:nth-child(17) { --x: 104.1px; --y: -46.4px; font-size: 10px; animation-delay: 0ms; }
.achpop__burst i:nth-child(18) { --x: 69.3px; --y: 9.7px; font-size: 11px; animation-delay: 60ms; }

.achpop__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* Sheer glass is the notification tier: what is behind stays legible, so the
   type has to carry its own halo rather than lean on a panel colour. Same
   treatment #toast uses, and for the same reason -- --os-surface is the
   opposite of --os-ink by construction, so one rule covers all twenty theme and
   weather combinations without a new token. */
.achpop__eyebrow,
.achpop__name,
.achpop__score {
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface),
               0 0 16px var(--os-surface);
}
.achpop__eyebrow {
  font: 700 var(--text-micro)/1.5 var(--font-mono);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--os-accent);
}
.achpop__name {
  font: 700 var(--text-body)/1.35 var(--font-body);
  color: var(--os-ink);
  white-space: nowrap;
}
.achpop__score {
  flex: 0 0 auto;
  padding-left: var(--space-lg);
  border-left: 1px solid var(--glass-edge);
  font: 700 var(--text-lead)/1 var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--os-ink-dim);
}
@media (prefers-reduced-motion: reduce) {
  #achpop { transition: opacity 200ms linear; transform: translate(-50%, 0) scale(1); }
  #achpop[data-show] .achpop__plate::before,
  #achpop[data-show] .achpop__burst i { animation: none; }
}

/* one anchor strategy only: right edge at --bx, bottom edge at --by (bug log #1) */
.bubble {
  position: fixed;
  left: var(--bx, 0px);
  top: var(--by, 0px);
  transform: translate(-100%, -100%);
  z-index: var(--z-bubble);
  width: max-content;
  max-width: 250px;
  padding:  var(--space-md) var(--space-lg);
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  line-height:  var(--lh-body);
  text-align: left;
  color: var(--purple-text);
  background: #fff;
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-xs) var(--radius-lg);
  box-shadow: 0 6px 18px #5a3f8f38;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--motion-exit);
}
.bubble[data-show] { opacity: 1; }

/* --------------------------------------------------------------------------
   11. FULL-SCREEN STATES
   -------------------------------------------------------------------------- */

#boot {
  position: fixed;
  inset: 0;
  z-index: var(--z-boot);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-2xl);
  background: var(--sky-day);
  transition: opacity var(--motion-enter);
}
#boot[data-done] { opacity: 0; pointer-events: none; }
/* THE FIRST FRAME. This was a 400px mark on an empty field of flat lilac, on
   what is the only screen every single visitor sees. Three changes, no new
   assets: the sky renders behind it instead of a flat fill, the wordmark takes
   the room it was leaving empty, and the bar holds long enough to be read. */
#boot { background: none; }
/* Not an opaque fill. #sky is already standing behind this -- sky.js builds it
   at load, well under --z-boot -- so the clouds, stars and drifting motifs are
   there the whole time and a solid background was the only thing hiding them.
   This is a bloom instead: white where the wordmark needs contrast, clearing to
   nothing at the edges so the theme's own sky reads around it. */
#boot::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: radial-gradient(74% 60% at 50% 46%,
    rgba(255, 255, 255, 0.74) 0%,
    rgba(255, 255, 255, 0.34) 58%,
    rgba(255, 255, 255, 0) 86%);
}
/* A translucent boot screen cannot tell the sky from the furniture: drop the
   backdrop and the icons, the dock and the standee come through with it, which
   spoils the reveal rather than deepening it. So the chrome is held back for
   the length of the boot and the sky is all that shows. It is released before
   the fade, so the desktop is already there as the screen clears. */
body[data-booting] #desktop,
body[data-booting] #windows,
body[data-booting] #menubar,
body[data-booting] #dock,
body[data-booting] #standee { visibility: hidden; }
/* THE SIGNPOST TAG, AND WHY THIS ONE IS !important.
   It survives a hidden ancestor because it sets `visibility: visible` on itself
   -- and it sets it from `body[data-guide=...] .icon[data-app=...] .icon__start`,
   which is (0,4,1). A plain `body[data-booting] .icon__start` is (0,2,1) and
   loses, which is why the tag kept showing through the boot screen after the
   rest of the chrome went.
   Matching that specificity would work today and break the moment either
   selector grows a part. This is a STATE override rather than a style -- while
   the machine is booting, nothing on the desktop is on screen, full stop -- so
   it says so instead of competing. It applies to both shells, since the tag
   lives on the phone grid too. */
body[data-booting] .icon__start { visibility: hidden !important; }
#boot-mark { width: min(620px, 82vw); }
#boot .flex { font: 700 var(--text-body)/var(--lh-body) var(--font-mono); color: var(--purple-text); min-height: 20px; }
#boot .bar {
  width: min(260px, 66vw);
  height: 14px;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: #ffffff99;
  box-shadow: inset 0 2px 6px #9d74d655, 0 0 0 2px #fff;
}
#boot .bar > i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--radius-pill);
  background: linear-gradient(90deg, #c4f0ff, #e07bb8, #c58ae0);
  /* paced to the 4.2s hold, so the bar fills as the screen ends rather than
     completing a second and a half before anything happens */
  animation: boot-load 3.9s var(--ease-standard) forwards;
}
@keyframes boot-load { to { width: 100%; } }
#boot .press {
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-widest);
  color: var(--pink);
  animation: boot-blink 1.1s steps(2) infinite;
}
@keyframes boot-blink { 50% { opacity: 0; } }

#gn {
  position: fixed;
  inset: 0;
  z-index: var(--z-toast);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-lg);
  background: #1d1838;
}
#gn[data-open] { display: flex; }
#gn .w { font-size: var(--text-display); }
#gn .t { font: 400 var(--text-lead)/var(--lh-lead) var(--font-mono); letter-spacing: var(--track-wider); color: #b99df0; }
#gn .t--hint { font-size: var(--text-caption); opacity: 0.6; }

/* ---- hack: the tube, green on black ----
   Not themed, and that is the joke. Every other full-screen state on this OS
   wears the palette; this one is doing an impression of a nineties movie, so
   it is the one place the design system deliberately steps aside. Phosphor
   green, one mono face, and a caret that blinks whether or not anything is
   still being typed. */
#hack {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: none;
  padding: clamp(20px, 6vw, 72px);
  background: #000;
  cursor: pointer;
  overflow: hidden;
}
#hack[data-open] { display: block; }
#hack::after {
  /* the scanline wash, so it reads as a screen rather than as a text file */
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, 0.5) 2px 4px);
}
#hack-out {
  margin: 0;
  font: 700 clamp(12px, 1.7vw, 18px)/1.7 var(--font-mono);
  color: #35ff6d;
  text-shadow: 0 0 8px rgba(53, 255, 109, 0.65);
  white-space: pre-wrap;
  word-break: break-word;
}
#hack-out::after {
  content: '\2588';
  animation: hack-caret 1s steps(2, start) infinite;
}
@keyframes hack-caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
/* the punchline gets the screen to itself: everything above it dims out */
#hack[data-jk] #hack-out { color: #35ff6d; }
#hack[data-jk] #hack-out::after { animation: none; opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  #hack-out::after { animation: none; }
}

/* --------------------------------------------------------------------------
   12. PHONE SHELL  (z 60)
   -------------------------------------------------------------------------- */

#phone-frame {
  position: fixed;
  inset: 0;
  z-index: var(--z-phone);
  display: none;
  align-items: center;
  justify-content: center;
  background: #2a215099;
  backdrop-filter: var(--glass-veil);
}
body[data-preview] #phone-frame { display: flex; }
#phone {
  position: relative;
  /* A real handset's proportions, as large as the window allows.
     It used to be a flat 340x700, which is a 320x680 screen: shorter than any
     phone made and the wrong shape at 0.47. That matters because the lock
     screen's furniture is a FIXED 344px of clock, date and notifications. On a
     real 852 to 998px screen that is 35 to 40% of the height and the wallpaper
     reads fine; squeezed into 680 it became 51% and swallowed the artwork's
     focal point, so the preview lied about what a phone actually shows.
     Deriving both axes from the available height keeps the shape at 0.461, the
     iPhone 15 Pro / Galaxy S24 ratio, and gives the furniture the same share of
     the screen it would really have. 872 is that phone at 1:1; below it the
     preview is a smaller phone, never a squatter one. */
  height: min(872px, 92vh);
  width: calc(min(872px, 92vh) * 0.4735);
  padding:  var(--space-lg);
  border-radius: 40px;
  background: #1d1838;
  box-shadow: 0 30px 80px #000b, 0 0 0 3px #4c3f80;
}
#screen {
  position: absolute;
  /* the phone's screen is what its contents should be measuring themselves
     against, not the browser window: see the container query on #pstatus */
  container-type: inline-size;
  inset: 10px;
  border-radius: 32px;
  overflow: hidden;
  background: var(--os-sky);
  transition: background var(--motion-slow) var(--ease-standard);
}
#phone-close {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 34px;
  height: 34px;
  border: none;
  border-radius: var(--radius-circle);
  background: #fff;
  color: var(--os-accent);
  font: 800 var(--text-lead)/1.3 var(--font-body);
  box-shadow: 0 4px 14px #0007;
  z-index: 5;
}

/* real phone: the shell IS the site */
@media (max-width: 700px), (max-height: 500px) {
  #desktop, #dock, #menubar, #standee, #toast { display: none !important; }
  #phone-frame { display: flex; background: none; backdrop-filter: none; }
  #phone {
    width: 100%;
    height: 100%;
    max-height: none;
    padding: 0;
    border-radius: 0;
    background: none;
    box-shadow: none;
  }
  #screen { inset: 0; border-radius: 0; }
  #phone-close { display: none; }
}

/* scrollbars: hidden inside the phone shell (bug log #4) */
@media (hover: none) {
  #screen *, #screen { scrollbar-width: none; }
  #screen *::-webkit-scrollbar { display: none; }
}

#pstatus {
  position: absolute;
  inset: 0 0 auto 0;
  height: 30px;
  z-index: 6;
  /* 1fr auto 1fr, which is the layout primitive this bar always wanted. The
     offer has to be centred on the BAR, and its neighbours are a clock and four
     buttons that will never be the same width, so space-between put it off
     centre and absolute positioning put it on top of the wifi icon when things
     got tight. Two equal fr columns centre the middle item exactly, absorb the
     difference between the neighbours evenly, and cannot overlap, because the
     offer is still in the flow. It also makes the gap on each side of the pill
     the same by construction rather than by arithmetic. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  /* A guaranteed gap on both sides of the offer. The three groups were 6px
     apart, the same as the gap between two icons, but an icon is a glyph with
     2px of padding around it while the offer is a filled pill with a hard edge,
     so the same 6px reads as tighter next to the pill than between the icons.
     This puts 12px between the pill and its neighbours against the icons 10px. */
  column-gap: var(--space-lg);
  align-items: center;
  padding:  0 var(--space-xl);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  /* The status bar sits straight on the sky rather than on glass, so it needs
     this most of all. Ink and halo both flip with the palette. White text was readable on the
     five dark combinations and measured 1.14:1 on the fifteen light ones — a
     sheer panel means the text really is sitting on the sky. --os-surface is
     the opposite of --os-ink by construction (a surface is what holds ink), so
     using it as the halo needs no new tokens and covers all twenty. The
     treatment is unchanged: a blurred backdrop, never a hard line. */
  color: var(--os-ink);
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface),
               0 0 16px var(--os-surface);
  pointer-events: none;
}
/* On the lock screen the status bar is not sitting on the theme's sky, it is
   sitting on the wallpaper, and the wallpaper is one fixed image whose top is
   dark in every theme. So the ink here is keyed to the artwork rather than to
   the palette: a theme token would follow the sky it can no longer see, which is
   how dark ink ended up on a dark night sky. Literal values, because the thing
   underneath is literal. */
#screen:has(#lock:not([data-unlocked])) #pstatus,
#screen:has(#lock:not([data-unlocked])) .hire--phone {
  color: #f6f2ff;
  text-shadow: 0 1px 3px #1a1240cc, 0 0 10px #1a124099, 0 0 20px #1a124066;
}
/* .hire sets its own colour, so inheriting from the bar was never going to reach
   it: the offer kept --os-ink and stayed dark on the wallpaper's dark top. On
   the home screen that dark ink is correct and stays, because there the bar
   really is sitting on the theme's sky. */
#screen:has(#lock:not([data-unlocked])) .hire--phone {
  background: #ffffff1f;
  box-shadow: inset 0 0 0 1px #ffffff45;
}
#pstatus .right { display: flex; align-items: center; gap:  var(--space-sm); pointer-events: auto; }
/* :not(.hire--phone), because the offer is not one of these. This rule dresses
   the four little icon buttons on the right, and it out-specifies .hire (an id
   plus an element beats a class), so it was stripping the offer's padding down
   to 0 2px and its font down to the icon size. .hire--phone then patched some of
   it back with a second padding of its own, which is why the pill read as
   cramped and unevenly spaced next to its desktop twin: it was a component being
   overwritten by a neighbour and then partly repaired. */
#pstatus button:not(.hire--phone) {
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--text-body);
  padding:  0 var(--space-hair);
  text-shadow: inherit;
}

/* lock screen */
#lock {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding:  44px var(--space-xl) var(--space-2xl);
  /* Full-bleed art cut for this job: 1350x3000, ratio 0.450, opaque. It used to
     be a 556x680 cutout on transparency, which meant the flat screen behind
     showed through her left third and `cover` cropped 42% of the width.

     Ratio 0.450 sits in the middle of the phone cluster, which runs 0.429 (21:9
     Android) to 0.462 (iPhone 15/16 Pro, Galaxy S24), so the flagships crop
     almost nothing. The widest case is not a phone at all: this shell is also
     the desktop preview, 320px wide and as tall as the window allows, which on
     a 500px-tall window shows only 62% of the art.

     TOP, not bottom, and that is the whole decision. `cover` has to throw away
     the same number of pixels either way; the only question is which end. Her
     face sits in the upper third, so anchoring to the bottom cut the face off
     first on exactly the screens with the least room. Anchored to the top, a
     short screen loses her legs instead, which costs nothing: the clock, her
     face and the notifications all live up there and all survive.

     So the safe zone in the artwork is the TOP 65% and the central 95%. The
     bottom third is atmosphere that only tall screens see. Re-export to those
     numbers or this stops holding. */
  background-image: url('images/os/icy-lock.webp');
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
}

/* Theme glyphs on the lock screen, drawn from the same --motif-N custom
   properties the desktop wallpaper uses, so every theme decorates its own lock
   screen for free and a new theme needs nothing added here. Placed around the
   clock and over the art, never where the notifications or the unlock button
   land. */
.lock__deco {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}
.lock__deco span {
  position: absolute;
  background-repeat: no-repeat;
  background-size: contain;
  background-position: center;
  opacity: 0.82;
  filter: drop-shadow(0 2px 7px rgba(75, 30, 110, 0.4));
  animation: lock-float 7s var(--ease-standard) infinite;
}
.lock__deco span:nth-child(1) { left: 7%;  top: 8%;  width: 30px; height: 30px; background-image: var(--motif-1); }
.lock__deco span:nth-child(2) { right: 8%; top: 12%; width: 24px; height: 24px; background-image: var(--motif-2, var(--motif-1)); animation-delay: -1.4s; }
.lock__deco span:nth-child(3) { left: 16%; top: 20%; width: 18px; height: 18px; background-image: var(--motif-3, var(--motif-1)); animation-delay: -3.1s; }
.lock__deco span:nth-child(4) { right: 12%; top: 58%; width: 34px; height: 34px; background-image: var(--motif-4, var(--motif-1)); animation-delay: -2.2s; }
.lock__deco span:nth-child(5) { left: 9%;  top: 68%; width: 26px; height: 26px; background-image: var(--motif-2, var(--motif-1)); animation-delay: -4.6s; }
.lock__deco span:nth-child(6) { right: 22%; top: 80%; width: 20px; height: 20px; background-image: var(--motif-1); animation-delay: -5.5s; }

@keyframes lock-float {
  0%, 100% { transform: translateY(0) rotate(-5deg); }
  50%      { transform: translateY(-11px) rotate(6deg); }
}

@media (prefers-reduced-motion: reduce) {
  .lock__deco span { animation: none; }
}
#lock[data-unlocked] { display: none; }
#lock::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 42%;
  pointer-events: none;
  background: linear-gradient(180deg, transparent, #4b1e6e55);
}
#lock .lclock {
  animation: wake-in var(--motion-enter) var(--ease-enter) both;
  margin-top:  var(--space-hair);
  font: 400 var(--text-hero)/var(--lh-hero) var(--font-display);
  color: #fff;
  text-shadow: 0 2px 0 #b48ccc, 0 6px 22px #4b1e6e66;
}
#lock .ldate {
  animation: wake-in var(--motion-enter) var(--ease-enter) 0.1s both;
  margin-bottom:  var(--space-xl);
  font: 400 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wider);
  color: #fff;
  text-shadow: 0 2px 8px #4b1e6e88;
}
.notif {
  position: relative;
  z-index: 2;
  animation: notif-in var(--motion-enter) var(--ease-enter) both;
  width: 100%;
  display: flex;
  gap:  var(--space-lg);
  align-items: flex-start;
  text-align: left;
  padding:  var(--space-lg) var(--space-lg);
  margin-bottom:  var(--space-md);
  border-radius: var(--radius-lg);
}
/* the badge follows the app tiles: glass, not a solid --t-art chip */
/* icy's own notifications carry her face, not an app glyph */
/* A pre-cut square, opaque edge to edge, face already centred. The old asset
   was a circle on transparency -- 78.3% opaque, which is pi/4 exactly -- so it
   could never fill a rounded square without being scaled up AND offset, and
   offsetting a circle uncovers a corner. All that arithmetic disappears when
   the artwork is simply the right shape. */
.notif__icon--icy { overflow: hidden; padding: 0; }
.notif__icon--icy.notif__icon--icy img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/* The bear's notification shows the bear, from the same 40px sheet the standee
   uses. Whole-number scale only -- 40px source at 40px is 1x, so the pixels
   stay square. */
/* A pre-cut asset rather than a background-position window into the sprite
   sheet. Cropping a sheet through CSS meant every adjustment was arithmetic
   against a frame I could not see; a flat square is verifiable by looking at
   it, and the tile just fills. */
.notif__icon--patch { background: none; box-shadow: none; padding: 0; }
.notif__icon--patch.notif__icon--patch img { width: 100%; height: 100%; }
.notif__icon--bear { overflow: hidden; padding: 0; }
.notif__icon--bear.notif__icon--bear img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  image-rendering: pixelated;
}

.notif__icon {
  flex-shrink: 0;
  /* 40, because the bear is pixel art and pixel art only survives whole-number
     scaling. The sprite is 40px on its sheet; at a 30px box the browser resampled
     it to 0.75x, and at 2x DPR to 1.5x, both of which drop or double pixels
     unevenly and chew the sprite. 40 is 1x here, 2x on a retina display and 3x on
     a phone, so every pixel stays square at every density. It was also shipping as
     a 256px file, which is a 6.4x upscale of the same 40px art: two bad resamples
     stacked. The source frame is what ships now. */
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  font-size: var(--text-lead);
  background-color: var(--os-glass);
  backdrop-filter: var(--glass-tile);
  -webkit-backdrop-filter: var(--glass-tile);
  box-shadow: inset 0 1px 0 var(--glass-spec), inset 0 0 0 1px var(--glass-edge);
}
/* Sheer glass over her portrait, so the text takes the status bar's treatment:
   white with a blurred halo, never a hard edge. The app label keeps its theme
   colour rather than going white too, because --t-deco is light and saturated
   in all five themes and the halo carries it over anything underneath. */
.notif__body {
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  line-height: 1.45;
  /* Ink and halo both flip with the palette. White text was readable on the
     five dark combinations and measured 1.14:1 on the fifteen light ones — a
     sheer panel means the text really is sitting on the sky. --os-surface is
     the opposite of --os-ink by construction (a surface is what holds ink), so
     using it as the halo needs no new tokens and covers all twenty. The
     treatment is unchanged: a blurred backdrop, never a hard line. */
  color: var(--os-ink);
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface),
               0 0 16px var(--os-surface);
}
.notif__body b {
  display: block;
  margin-bottom:  var(--space-hair);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  /* --os-accent, not --t-deco. The deco tokens are light pastels by definition;
     as a label on a sheer panel over a light sky that measured 2.14:1, against a
     3:1 floor, and it read as unreadable on every combination except the dark
     ones. --os-accent is the ink-family token that already flips with the
     palette: dark magenta on the light themes, light pink or cyan on the dark
     ones. Same halo as the body beneath it, for the same reason: a sheer panel
     means the text really is sitting on the sky. */
  color: var(--os-accent);
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface);
}
/* Same trap the status bar fell into, one element over. These panels are sheer,
   so what is really behind the title is the lock wallpaper, and that is one
   fixed bright image in every theme. On the night palettes --os-accent is a pale
   pink or cyan chosen for a dark surface that is not there, and it landed at
   2.45:1 on a panel the wallpaper had lit up. Arcade fails the same way in all
   four weathers because its accent is pale by design.

   So the title takes a fixed ink here, dark enough for the brightest part of the
   wallpaper, with the light halo the body already uses. It costs the title its
   per-theme colour on the lock screen only; the notification is a piece of the
   phone, not a piece of the palette. */
#lock .notif__body b {
  color: #8a1f6e;
  text-shadow: 0 1px 2px #ffffffe6, 0 0 9px #ffffffcc;
}
/* they land one after another, like a phone you left face down */
#lock .notif:nth-of-type(1) { animation-delay: 0.35s; }
#lock .notif:nth-of-type(2) { animation-delay: 0.55s; }
#lock .notif:nth-of-type(3) { animation-delay: 0.75s; }
@keyframes wake-in { from { opacity: 0; transform: translateY(-10px) scale(0.96); } }
@keyframes notif-in { from { opacity: 0; transform: translateY(-16px) scale(0.94); } }

#unlock {
  animation: wake-in var(--motion-enter) var(--ease-standard) 1s both;
  margin-top: auto;
  z-index: 2;
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-wider);
  color: #fff;
  background: #ffffff40;
  border: 2px solid #ffffffb3;
  padding:  var(--space-lg) var(--space-2xl);
  border-radius: var(--radius-pill);
  backdrop-filter: var(--glass-veil);
}
#chev {
  z-index: 2;
  margin-top:  var(--space-sm);
  font-size: var(--text-lead);
  color: #fff;
  text-shadow: 0 2px 8px #4b1e6e;
  animation: chev-bob 1.6s var(--ease-standard) infinite;
}
@keyframes chev-bob {
  0%, 100% { transform: translateY(3px); opacity: 0.5; }
  50% { transform: translateY(-3px); opacity: 1; }
}

/* home screen */
#home {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: none;
  flex-direction: column;
  padding:  38px var(--space-xl) var(--space-lg);
}
#home[data-on] { display: flex; }
#home::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg, var(--grid-line) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 34px);
}
#pmoon {
  position: absolute;
  top: 42px;
  right: 18px;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-circle);
  background: radial-gradient(circle at 62% 40%, #fdf6d8, #f4e9bf 70%);
  box-shadow: 0 0 22px #f4e9bf77;
  opacity: 0;
  transition: opacity var(--motion-slow);
  -webkit-mask-image: radial-gradient(circle at 30% 42%, transparent 46%, #000 48%);
  mask-image: radial-gradient(circle at 30% 42%, transparent 46%, #000 48%);
}
body[data-mode="night"] #pmoon { opacity: 1; }
#home .sparkle { z-index: 1; }
#mood-phone { position: relative; z-index: 2; width: auto; margin-bottom:  var(--space-xl); }
#mood-phone .mood__stats { font-size: var(--text-caption); }
#hgrid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap:  var(--space-lg) var(--space-hair);
}
#hgrid .icon__label { font-size: var(--text-micro); padding:  var(--space-hair) var(--space-sm); }
#hgrid .icon { width: auto; }
#hgrid .icon:hover { transform: none; }
#hgrid .icon__art { width: 46px; height: 46px; }

/* ---------- jiggle ----------
   The wobble owns `transform`; the reflow owns `left`/`top`. A running CSS
   animation beats inline style in the cascade, so a FLIP written with
   transform would be eaten by the wobble on every icon it tried to move.
   Keeping them on separate properties is what makes both work at once. */
#hgrid .icon {
  position: relative;
  left: 0;
  top: 0;
  transition: left var(--motion-exit) var(--ease-standard), top var(--motion-exit) var(--ease-standard);
  -webkit-touch-callout: none;      /* the hold IS iOS's callout gesture */
  -webkit-user-select: none;
  user-select: none;
}

#hgrid.is-jiggling .icon {
  animation: jiggle-a .28s var(--ease-standard) infinite alternate;
  touch-action: none;               /* the drag must not become a scroll */
}
/* out of phase, or fourteen icons wobbling in lockstep reads as one object */
#hgrid.is-jiggling .icon:nth-child(even) {
  animation-name: jiggle-b;
  animation-duration: var(--motion-exit);
}
#hgrid.is-jiggling .icon:nth-child(3n) { animation-delay: -.09s; }

@keyframes jiggle-a {
  from { transform: rotate(-1.5deg) translateY(-.4px); }
  to   { transform: rotate(1.5deg) translateY(.4px); }
}
@keyframes jiggle-b {
  from { transform: rotate(1.7deg) translateY(.4px); }
  to   { transform: rotate(-1.7deg) translateY(-.4px); }
}

/* the one being carried: animation off, so transform is free again */
#hgrid.is-jiggling .icon[data-lift] {
  animation: none;
  transition: none;
  z-index: 3;
  transform: translate(var(--jx, 0), var(--jy, 0)) scale(1.14);
  filter: drop-shadow(0 8px 14px rgba(40, 20, 80, .34));
}

#jiggle-done {
  display: block;
  margin:  var(--space-xl) auto 0;
  position: relative;
  z-index: 3;
}

@media (prefers-reduced-motion: reduce) {
  #hgrid.is-jiggling .icon { animation: none; outline: 2px dashed var(--os-accent); outline-offset: 2px; border-radius: var(--radius-md); }
  #hgrid .icon { transition: none; }
}
#hdock {
  position: relative;
  z-index: 2;
  margin-top: auto;
  display: flex;
  justify-content: center;
  gap:  var(--space-lg);
  padding:  var(--space-md);
  border-radius: var(--radius-pill);
  /* .glass owns the surface */
}
#hdock .button { padding:  var(--space-md) var(--space-xl); font-size: var(--text-body); }

/* icy peeks in from behind the grid, one anchor for every pose */
#peek {
  position: absolute;
  /* One offset for all seventeen poses. This used to be a measured offset per
     pose plus a sixth for the seated pair: each pose was cropped to its own
     outline, so anchoring them to a shared right edge put every face somewhere
     different. The canvas puts every head on the same vertical line, 47.4%
     across, so it only has to be solved once.

     Where the head lands is now measured against the APP GRID rather than
     picked. #hgrid is four columns whose centres sit at 15.5, 38.5, 61.5 and
     84.5 percent of the screen, so its gutters are at 27, 50 and 73. At -33.4%
     the head landed at 63.3% -- within two points of column three's centre,
     which is to say directly on top of an app and its label. -43.1% puts it at
     73%, in the gutter between columns three and four, so she leans through a
     gap instead of over an icon. She is still cropped by the screen edge, which
     is the point: 68% of her is visible rather than 75%.
     Redo it against the grid, not against the canvas, if either changes. */
  right: -43.1%;
  bottom: -28%;
  z-index: 1;
  border: none;
  background: none;
  padding: 0;
  filter: drop-shadow(0 8px 16px var(--os-shadow));
}
/* She leans into frame at half length and is cropped by the screen edge, which
   is the point: a whole figure shrunk to fit reads as a sticker, a cropped one
   reads as someone standing there. Two knobs: this height sets how big she is,
   the negative bottom on #peek sets where she is cut. Percentages, not vh, so
   the preview shell and a real handset agree.

   ONE height for every pose, standing and seated. The poses share a canvas
   (test/poses.sh), which is what makes that possible: fit-to-box sizing reads
   the bounding box, and before the canvas the widest standing pose rendered
   ~18% shorter than the narrowest while the seated pair needed a size and a
   position of their own. Now the canvas is the bounding box, so the box is the
   body and there is nothing left to special-case.

   The height lives on #peek, NOT on the image. A percentage max-height on the
   image resolves against #peek, whose own height is auto, and a percentage
   against an auto-height containing block computes to `none`, so she rendered
   at her intrinsic size. #peek is absolutely positioned inside #home, which has
   a definite height, so the percentage resolves there. */
#peek { height: 70%; }
#peek img {
  display: block;
  height: 100%;
  width: auto;
  max-width: none;
}
/* Standing is cropped at the thigh by the screen edge. Seated is short enough
   to sit inside it, so she is lifted clear of the edge instead of cut by it.
   Her size is not touched: that is the canvas's job now, not this rule's. */
body[data-icy="seated"] #peek { bottom: 4%; }
/* the light on her still belongs to the visitor's sky */
body[data-mode="night"] #peek { filter: drop-shadow(0 8px 20px rgba(10, 6, 30, 0.5)); }

/* app view: the real window body moves here, never a clone */
#appview {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: none;
  flex-direction: column;
  background: var(--os-sky);
  color: var(--os-ink);
}
#appview[data-on] { display: flex; animation: app-slide var(--motion-exit) var(--ease-standard); }
@keyframes app-slide { from { transform: translateY(40px); opacity: 0; } }
#apphead {
  display: flex;
  align-items: center;
  gap:  var(--space-lg);
  padding:  38px var(--space-xl) var(--space-lg);
  background: var(--t-bar);
}
#apphead .tbar__title { flex: 1; color: var(--t-bar-ink); }
#apphead button {
  border: none;
  border-radius: var(--radius-pill);
  background: var(--t-bar-chip);
  color: var(--t-bar-ink);
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
  padding:  var(--space-sm) var(--space-lg);
}
#appbody {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: auto;
  padding:  var(--space-xl) var(--space-xl);
}
#appbody .wbody { flex: 1 1 auto; max-height: none; padding: 0; overflow: visible; }
#appbody #term { max-height: none; }

/* themes on the phone: the view menu has no home here, so it gets a sheet */
#theme-sheet {
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: 10px;
  z-index: 7;
  padding:  var(--space-xl) var(--space-xl) var(--space-lg);
  border-radius: var(--radius-xl);
  transform: translateY(125%);
  transition: transform var(--motion-exit) var(--ease-standard);
}
#theme-sheet[data-open] { transform: translateY(0); }
#theme-sheet .os-mono { margin-bottom:  var(--space-sm); }
#theme-sheet .mcap { padding:  var(--space-xs) var(--space-lg) var(--space-hair); }
#theme-sheet .msep { margin:  var(--space-sm) var(--space-sm); }
#theme-sheet { max-height: 78%; overflow: auto; }
#theme-sheet .dbtn { width: 100%; margin-top:  var(--space-md); }

/* phone notification banner — the single transient-message component (bug log #3) */
#pnotif {
  position: absolute;
  top: 6px;
  left: 10px;
  right: 10px;
  z-index: 8;
  display: flex;
  gap:  var(--space-lg);
  align-items: center;
  padding:  var(--space-md) var(--space-lg);
  border-radius: var(--radius-lg);
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(-130%);
  transition: opacity var(--motion-exit) var(--ease-standard), transform var(--motion-exit) var(--ease-standard);
}
#pnotif[data-show] { opacity: 1; transform: translateY(0); }
#pnotif[data-show]:active { transform: translateY(0) scale(0.985); }
/* Sheer glass means the text sits on whatever happens to be underneath, so it
   takes the status bar's treatment: white with a blurred halo, never a hard
   edge. Same reason, same fix. */
#pnotif .notif__body {
  font-size: var(--text-body);
  /* Ink and halo both flip with the palette. White text was readable on the
     five dark combinations and measured 1.14:1 on the fifteen light ones — a
     sheer panel means the text really is sitting on the sky. --os-surface is
     the opposite of --os-ink by construction (a surface is what holds ink), so
     using it as the halo needs no new tokens and covers all twenty. The
     treatment is unchanged: a blurred backdrop, never a hard line. */
  color: var(--os-ink);
  text-shadow: 0 1px 2px var(--os-surface), 0 0 9px var(--os-surface),
               0 0 16px var(--os-surface);
}

/* --------------------------------------------------------------------------
   13. APPS

   The shared components from style.css are built for a 1060px page. Rather than
   fork them or hang modifier classes off every element, one window-scale tier
   re-tunes them inside .wbody — so app markup stays identical to the live site's
   and the lightbox, cards and tags all work as they already do.
   -------------------------------------------------------------------------- */


.center { text-align: center; }

/* ---- shared components, at window scale ---- */
.wbody .card-grid { grid-template-columns: 1fr; gap:  var(--space-md); }
.wbody .experience-card {
  gap:  var(--space-xs);
  padding:  var(--space-lg) var(--space-lg);
  border-radius: var(--radius-lg);
  border: 1.5px solid var(--os-border);
  background: var(--os-pill);
  box-shadow: none;
}
.wbody .experience-card__title { font-size: var(--text-lead); color: var(--os-ink); }
.wbody .experience-card__title span { font-size: var(--text-body); }
.wbody .experience-card__dates { font-size: var(--text-caption); color: var(--os-ink-dim); }
.wbody .experience-card__role { font-size: var(--text-caption); letter-spacing: var(--track-label); color: var(--os-accent); }
.wbody .experience-card__description { font-size: var(--text-body); line-height: 1.55; color: var(--os-ink); }
.wbody .experience-card--locked { border-style: dashed; opacity: 0.75; }

.wbody .skill-tree { display: flex; flex-direction: column; gap:  var(--space-md); }
.wbody .tag-row { gap:  var(--space-xs); }
.wbody .tag {
  font-size: var(--text-caption);
  padding:  var(--space-xs) var(--space-md);
  border-radius: var(--radius-sm);
  background: var(--os-pill);
  border: 1px solid var(--os-border);
  color: var(--os-ink);
}
.wbody .pills { display: flex; flex-wrap: wrap; gap:  var(--space-sm); }
.pill--compact { border: 1.5px solid var(--os-border-strong); background: var(--os-pill); color: var(--os-ink); }
.pills.pick .pill { transition: border-color var(--motion-state) var(--ease-standard), color var(--motion-state) var(--ease-standard); }
.pills.pick .pill.is-on { border-color: var(--os-accent); color: var(--os-accent); background: var(--os-surface); }




/* ---- diagnosis ---- */
.diag-name {
  margin-top:  var(--space-xs);
  font: 400 clamp(var(--text-subhead), 5vw, 27px) var(--font-display);
  line-height: 1.2;
  color: var(--os-ink);
}
.diag-line { font-size: var(--text-body); font-weight: 700; color: var(--os-accent); }

/* ---- what she actually did ----
   The claims were buried in a paragraph at body size: "$250K", "50K+", "400+
   attendee" all set exactly like the words around them. A hiring manager scans
   a role in about three seconds and a paragraph does not survive that.
   Bulleted, with the number pulled out in the accent. Deliberately NOT a
   separate stat row: these are the same sentences, given a shape, rather than a
   second treatment of facts already on the card. */
.deed {
  list-style: none;
  margin:  var(--space-sm) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap:  var(--space-xs);
}
.deed li {
  position: relative;
  padding-left: 1.35em;
  font: 400 var(--text-caption)/1.55 var(--font-body);
  color: var(--os-ink);
}
/* the same mark that ends every call to action, here as the bullet */
.deed li::before {
  content: '\27E2';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--os-accent);
  opacity: .72;
}
.deed__n {
  font-weight: 800;
  color: var(--os-accent);
  font-variant-numeric: tabular-nums;
}

/* ---- portfolio ---- */
/* Two up, not four. At auto-fill/120px this grid put four tiles across a 470px
   window body, so each piece of work rendered about 116px wide. Ten thumbnails
   at a size where nothing is readable is worse than five you can actually see,
   on the one panel whose entire job is showing the work. */
/* MASONRY, BECAUSE THE PIECES ARE NOT ONE SHAPE. The ten range from 0.82 to
   1.90 in aspect, and a fixed 4:3 frame with object-fit:contain letterboxed
   every single one of them -- ten images floating in ten boxes that fit none of
   them. Columns instead: each tile keeps its own ratio, so nothing is padded
   and every piece gets the height its composition asks for.
   CSS columns rather than grid-masonry, which is still not shipping anywhere.
   The cost is reading order down each column instead of across, which for a
   contact sheet of work is not a cost. */
.folio-grid {
  columns: 2;
  column-gap:  var(--space-lg);
  margin:  var(--space-sm) 0 var(--space-xs);
}
.wbody .work-item {
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin-bottom:  var(--space-lg);
}
.wbody .work-item { display: flex; flex-direction: column; gap:  var(--space-xs); }
.wbody .work-item__frame {
  position: relative;
  padding:  var(--space-hair);
  overflow: hidden;
  border: none;
  border-radius: var(--radius-md);
  /* --t-art, not the base ramp written out: every portfolio tile used to stay
     factory-default on all five themes */
  background: var(--t-art);
  box-shadow: var(--elev-raised) var(--os-shadow);
}
/* contain, not cover. The ten pieces range from 0.82 to 1.90 in aspect, so no
   single frame ratio fits them and `cover` was cutting the difference off every
   one of them. The mat behind is --t-art, a designed gradient, so letterboxing
   reads as a frame rather than as a gap. A portfolio should not crop the work. */
/* height:auto and no object-fit: the frame now takes the image's shape rather
   than the image being fitted into the frame's. `contain` was the right answer
   to a fixed ratio and is the wrong one once the ratio is gone -- it would leave
   the letterboxing behind in a box that no longer needs it. */
.wbody .work-item__frame img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}
.wbody .work-item__expand {
  position: absolute;
  inset: 2px;
  display: grid;
  place-items: center;
  font-size: var(--text-subhead);
  border-radius: var(--radius-md);
  background: #2a215070;
  opacity: 0;
  transition: opacity var(--motion-state) var(--ease-standard);
}
.wbody .work-item__frame:hover .work-item__expand { opacity: 1; }
.wbody .work-item__label {
  font: 700 var(--text-micro)/var(--lh-micro) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
  text-align: center;
}

/* ---- sticker album ---- */
/* decora.exe's drop line, and the one label on the sheet that has to out-read a
   number. It is .os-mono, which is the OS's own small label already: accent
   colour, mono face, uppercase, tracked. Four things change at once between the
   label and the sentence it sits in, which is more separation than a pill or a
   rule would buy -- and a pill here would out-shout the four cards underneath
   it, which are the actual content of the window. Inline, on the sentence's own
   baseline, one word wide. */
.drop-new { margin-right:  var(--space-sm); }

.stk-sheet { display: grid; grid-template-columns: repeat(2, 1fr); gap:  var(--space-md); margin:  var(--space-sm) 0 var(--space-hair); }
.stk {
  padding:  var(--space-lg) var(--space-md);
  text-align: center;
  border-radius: var(--radius-lg);
  border: 2px dashed var(--os-border-strong);
  background: var(--os-pill);
}
.stk__art { display: block; font-size: var(--text-title); }
.stk__t {
  display: block;
  margin-top:  var(--space-sm);
  font: 700 var(--text-caption)/1.45 var(--font-body);
  color: var(--os-ink);
}
.stk--seal { border-style: solid; border-color: #fff; background: var(--t-art); }
/* the ramp ends dark on arcade and archangel, so fixed dark ink muddied there */
.stk--seal .stk__t { color: var(--t-bar-ink); }
.stk--live { display: block; text-decoration: none; transition: transform var(--motion-state) var(--ease-standard); }
.stk--live:hover { transform: translateY(-3px) rotate(-2deg); }
.stk--live b { display: inline-block; margin-top:  var(--space-xs); color: #3d2a63; }
.stk--soon { opacity: 0.55; }
.stk--soon .stk__art { color: var(--os-ink-dim); font-size: var(--text-title); }
.stk--soon .stk__t { color: var(--os-ink-dim); letter-spacing: var(--track-wide); }

/* the drop tile that is a real drop -- a button, not an anchor, because it
   opens a window on this machine rather than leaving for one */
.stk--live { border: 0; font: inherit; cursor: pointer; }
button.stk--live { background: var(--t-art); border-style: solid; border-color: #fff; }
button.stk--live .stk__t { color: var(--t-bar-ink); }
button.stk--live b { color: inherit; }

/* ---- wallpapers.exe ----
   Two columns of 16:9 previews. `auto-fill` with a floor rather than a fixed
   count, so the same grid is one column on a phone and three in a window
   someone has dragged wide, with nothing to re-declare at a breakpoint. */
/* THE HEADER IS A LOCKUP, NOT THREE PARAGRAPHS. Each line steps down in weight,
   size and volume -- pitch, price, punchline -- so the last one lands as an
   aside rather than as another claim. The steps are tight (a hair between the
   lines) and the space is spent below the block instead, which is what keeps
   the three reading as one thing. */
.wall-head {
  margin-bottom: var(--space-md);
  text-align: center;
}
/* SIZED OFF THE SCALE, THREE STEPS APART: subhead, lead, body. The first pass
   set these at body/caption/micro, which put the drop's headline at 12px --
   SMALLER than the plain intro line every other window carries, since .wbody
   sets --text-lead and those inherit it. A shop window cannot open quieter than
   a text file.
   The bottom of the scale is where legibility goes first, and micro mono is the
   worst case of it: 8px of Space Mono is a texture, not a sentence. */
/* BAGEL, AND AT 400 AND A STEP LARGER. Two things follow from the family, not
   from taste. The face ships one weight, so asking for 800 would get a
   synthetic bold -- the browser smearing the outline sideways -- on a display
   face whose entire character is its drawn weight. And it is optically smaller
   than Montserrat at the same px: fat strokes, tiny counters, so 17 congealed
   where the sans read fine. Title, not subhead. Colour goes to full ink for the
   same reason -- dim on strokes that heavy looks washed rather than quiet. */
.wall-head__t {
  font: 400 var(--text-title)/var(--lh-title) var(--font-display);
  color: var(--os-ink);
  /* it wraps to two lines in a narrow window; balance makes that look chosen */
  text-wrap: balance;
}
.wall-head__free {
  margin-top: var(--space-xs);
  font: 700 var(--text-lead)/var(--lh-lead) var(--font-body);
  color: var(--os-accent);
}
/* Mono and quiet: the fine print of a thing that has no fine print. It is a
   step below the price rather than three, and stays the aside on weight, family
   and colour instead of on size -- 400 against 700, mono against Montserrat,
   dim ink against the accent. That is enough to be read last without being
   hard to read. */
.wall-head__fine {
  margin-top: var(--space-hair);
  font: 400 var(--text-body)/var(--lh-body) var(--font-mono);
  letter-spacing: var(--track-wide);
  color: var(--os-ink-dim);
  opacity: 0.72;
}
.wall-grid {
  display: grid;
  /* One theme per row now, not two tiles. The pair inside is wide, so a
     two-column grid would halve it back to thumbnails. */
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: var(--space-xl);
  margin: var(--space-md) 0 var(--space-sm);
}
.wtile { margin: 0; }
/* DESKTOP AND PHONE AT THE SAME HEIGHT. The row sets the height and each
   preview keeps its own aspect, so 16:10 and 9:19.5 sit as equals instead of
   the tall one being scaled to a wide one's box. align-items:stretch does the
   work; the widths fall out of the aspect ratios. */
/* The column ratio IS the two aspect ratios: 16:10 is 1.6 wide per unit of
   height and 9:19.5 is 0.4615, so columns in that ratio come out the same
   height by construction and together fill the row. A fixed pixel height left
   the pair sitting in half a row of empty space. */
.wtile__pair {
  display: grid;
  grid-template-columns: 1.6fr 0.4615fr;
  gap: var(--space-sm);
}
.wtile__art {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
  border: 1px solid var(--os-border-strong);
  background: var(--os-pill);
}
.wtile__art--desk  { aspect-ratio: 16 / 10; }
.wtile__art--phone { aspect-ratio: 9 / 19.5; }
.wtile__art img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* A locked tile is a shut door, not a blurred picture: there is no preview
   element at all, so nothing to peel back with a devtools inspector.

   IT KEEPS ITS SHAPE. It used to squash to 16:5 to save vertical room, which
   did save it and cost more: a first visit became a column of letterbox slots
   with two real wallpapers wedged in among them, and nothing about that column
   told you the slots were the same size as the prizes. At the true 16:10 and
   9:19.5 the locked box IS the unlocked box, so what you earn is exactly the
   shape of the hole it fills. Both tiles in a pair lock and unlock together, so
   the row still comes out even.

   The cost is real and worth naming: a first visit has four locked themes, so
   that is four full rows of scroll where it used to be four strips. That is the
   trade -- the scroll is the size of what you have not got yet. */
.wtile__art[data-locked] {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-hair);
  padding: var(--space-sm);
  border-style: dashed;
  text-align: center;
}
/* Bagel is the OS's own display face, so the question marks read as the system
   speaking rather than as placeholder art. */
.wtile__q {
  font: 400 var(--text-title)/1 var(--font-display);
  color: var(--os-ink-dim);
  letter-spacing: var(--track-wide);
  /* letter-spacing adds after the LAST glyph too, so a centred run sits half a
     track left of true centre. Pull it back. */
  margin-right: calc(var(--track-wide) * -1);
}
.wtile__free {
  max-width: 22ch;
  font: 600 var(--text-caption)/1.4 var(--font-body);
  font-style: normal;
  color: var(--os-ink-dim);
  opacity: 0.85;
  text-wrap: balance;
}
.wtile__cap {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  margin-top: var(--space-sm);
  font: 700 var(--text-caption)/1.3 var(--font-body);
  color: var(--os-ink);
}
.wtile__mode {
  font-weight: 600;
  color: var(--os-ink-dim);
  letter-spacing: var(--track-wide);
}
.wtile__get { display: flex; gap: var(--space-xs); margin: var(--space-xs) 0 0; }
.wtile__dl {
  flex: 1 1 0;
  min-width: 0;
  padding: var(--space-xs) var(--space-sm);
  border-radius: var(--radius-sm);
  border: 1px solid var(--os-border-strong);
  background: var(--os-pill);
  color: var(--os-ink);
  text-align: center;
  text-decoration: none;
  transition: background var(--motion-state) var(--ease-standard),
              border-color var(--motion-state) var(--ease-standard);
}
.wtile__dl:hover { background: var(--os-accent); border-color: var(--os-accent); color: var(--on-accent); }
.wtile__dl b { display: block; font: 700 var(--text-caption)/1.25 var(--font-body); }
.wtile__dl i {
  display: block;
  font: 400 var(--text-micro)/1.35 var(--font-mono);
  font-style: normal;
  opacity: 0.82;
}
.wtile__lock {
  flex: 1 1 auto;
  padding: var(--space-xs) var(--space-sm);
  font: 600 var(--text-micro)/1.35 var(--font-mono);
  color: var(--os-ink-dim);
  text-align: center;
}

/* the way in, sitting under the swatches it belongs to */
.mwall {
  display: block;
  width: 100%;
  margin-top: var(--space-hair);
  padding: var(--space-sm) var(--space-md);
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--os-accent);
  font: 700 var(--text-caption)/1.4 var(--font-body);
  text-align: left;
  cursor: pointer;
}
.mwall:hover { background: var(--os-pill); }

/* ---- quote.exe, which is now a door rather than a form ----
   Centred on purpose. The window has one thing in it and one thing to do, so
   there is no column of fields for the eye to run down and nothing to align a
   left edge to. */
.wbody--dm { text-align: center; align-items: center; }
.dm__title { margin: 0; font-size: var(--text-title); }
.dm__line { margin: var(--space-lg) 0 0; max-width: 38ch; }
/* the two halves of the sentence, each on its own line at any window width */
.dm__line span { display: block; }
.dm__cta { margin: var(--space-2xl) 0 0; }
.dm__note { margin: var(--space-md) 0 0; font: 600 var(--text-caption)/var(--lh-caption) var(--font-body); }
/* the mark rides INSIDE the button, ahead of the label */
.dm__btn { display: inline-flex; align-items: center; gap: var(--space-sm); }
.dm__glyph { display: block; width: 17px; height: 17px; }
.dm__glyph svg { display: block; width: 100%; height: 100%; fill: currentColor; }

/* ---- guestbook ---- */
#wall {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap:  var(--space-md);
}
.gstamp {
  padding:  var(--space-md) var(--space-xs);
  text-align: center;
  border-radius: var(--radius-lg);
  border: 2px dashed var(--os-border-strong);
  background: var(--os-pill);
  transition: transform var(--motion-state) var(--ease-standard);
}
.gstamp:hover { transform: rotate(-3deg) scale(1.05); }
.gstamp--fresh { border-style: solid; border-color: var(--os-accent); box-shadow: 0 0 14px var(--t-deco); }
.gstamp__e { font-size: var(--text-title); }
.gstamp__n {
  margin-top:  var(--space-xs);
  font: 700 var(--text-micro)/var(--lh-micro) var(--font-mono);
  color: var(--os-ink-dim);
  word-break: break-all;
}
.stk-row { display: flex; flex-wrap: wrap; gap:  var(--space-sm); margin:  var(--space-xs) 0 var(--space-md); }
.stk-pick {
  padding:  var(--space-sm) var(--space-md);
  font-size: var(--text-subhead);
  line-height: 1;
  background: var(--os-pill);
  border: 2px solid var(--os-border-strong);
  border-radius: var(--radius-md);
}
.stk-pick.is-on { border-color: var(--os-accent); box-shadow: 0 0 10px var(--t-deco); }

/* ---- achievements: a locked tile has to be worth clicking too ---- */
.ach-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap:  var(--space-md);
  margin:  var(--space-md) 0 var(--space-xs);
}
.ach {
  position: relative;
  padding:  var(--space-lg) var(--space-md);
  min-height: 88px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-sm);
  text-align: center;
  border: 2px dashed var(--os-border-strong);
  border-radius: var(--radius-lg);
  background: var(--os-pill);
  transition: transform var(--motion-state) var(--ease-standard), border-color var(--motion-state) var(--ease-standard);
}
.ach:hover { transform: translateY(-2px); border-color: var(--os-accent); }
/* The tile used to become the full --t-art gradient when earned, which was the
   loudest thing it could do at a time when the only difference inside was a
   filled sparkle instead of a hollow one. The patch is the reward now, so the
   tile steps back to a plain solid surface and lets it be seen. */
.ach[data-got] { border-style: solid; border-color: var(--os-border); background: var(--os-surface); }
.ach__mark {
  display: block;
  width: 52px;
  height: 52px;
  /* A patch is a physical object lying on a surface, so it gets an object's
     shadow: tight, offset down, and not a glow. */
  filter: drop-shadow(0 2px 3px var(--os-shadow));
}
.ach:not([data-got]) .ach__mark { opacity: .72; }
.ach--all .ach__mark { width: 62px; height: 62px; }
.ach__name {
  font: 700 var(--text-caption)/1.35 var(--font-body);
  color: var(--os-ink);
}
.ach[data-got] .ach__name { color: var(--os-ink); }
.ach__hint {
  font: 700 var(--text-caption)/1.4 var(--font-body);
  font-style: italic;
  color: var(--os-ink-dim);
}
.ach[data-got] .ach__hint { color: var(--os-ink-dim); opacity: 0.9; }
.ach--all { grid-column: 1 / -1; }


/* ---- patch notes ---- */
.patch {
  padding:  var(--space-lg) var(--space-lg);
  margin-bottom:  var(--space-md);
  border-radius: var(--radius-lg);
  border: 1.5px solid var(--os-border);
  background: var(--os-pill);
}
.patch__v {
  display: flex;
  justify-content: space-between;
  gap:  var(--space-lg);
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  color: var(--os-ink);
}
.patch__v span { font-size: var(--text-caption); font-weight: 700; color: var(--os-ink-dim); }
.patch p { margin-top:  var(--space-xs); font-size: var(--text-body); line-height: 1.55; }

/* THE TIME BETWEEN RELEASES. Genesis to beta was most of the build, and a
   changelog that lists two cards back to back says it took no time at all.

   Not a .patch: no border, no plate. The two either side are things that
   shipped and this is the space where nothing did, so giving it a box would
   make it read as a third release. The rule is dotted rather than solid for
   the same reason -- solid is a connector, dotted is an ellipsis.

   The segments are pseudo-elements, so they stay out of the accessibility
   tree on their own and the words are simply read. Set in the house mono at
   caption size, which is exactly what `beta` and `genesis` are wearing eight
   pixels above and below it, so the label rhymes with the timeline it is
   annotating instead of sitting on top of it. */
.elapsed {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap:  var(--space-sm);
  margin: 0 0 var(--space-md);
}
.elapsed::before,
.elapsed::after {
  content: '';
  width: 0;
  height: 18px;
  border-left: 2px dotted var(--os-border-strong);
  opacity: 0.55;
}
.elapsed span {
  font: 700 var(--text-caption)/1 var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
}

/* ---- bags ---- */
/* ---- bags: the error dialog ----
   The old one cropped a full-body seated drawing into an 88px circle with
   `object-fit: cover`. Two things were wrong with that. The art is 394x520 of
   mostly figure, so a square crop of it is her face and a lot of hair at a 4.5x
   downscale -- a bad avatar made out of good art. And the drawing is
   transparent, so inside the circle you saw the window's own surface, which is
   why it read as "no background".

   She is a bystander now, not an avatar: her own aspect ratio, no crop, tucked
   into the bottom right reacting to the dialog. Nothing new had to be drawn. */
.err-tape {
  height: 8px;
  flex: 0 0 auto;
  background: repeating-linear-gradient(-45deg,
    var(--tl-shade) 0 9px, transparent 9px 18px);
  opacity: .5;
  box-shadow: inset 0 1px 0 var(--glass-spec);
}
.wbody.err { position: relative; overflow: hidden; }
.err-msg {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-lg);
  align-items: start;
  /* room for her, so the copy never runs under the figure */
  padding-right: 104px;
}
.err-sign {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  background: var(--os-pill);
  box-shadow: inset 0 0 0 1px var(--os-border-strong);
  color: var(--tl-shade);
  font-size: var(--text-subhead);
  line-height: 1;
}
.err-msg p { margin: 0; }
.err-icy {
  position: absolute;
  right: -14px;
  bottom: -18px;
  width: 128px;
  height: auto;
  pointer-events: none;
  filter: drop-shadow(-6px 2px 12px var(--os-shadow));
}
.err-btns {
  display: flex;
  gap: var(--space-md);
  margin-top: var(--space-xl);
  padding-right: 86px;
}
.err-btns button {
  flex: 1 1 0;
  min-width: 0;
  padding:  var(--space-md) var(--space-md);
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-ink);
  background: var(--os-pill);
  border: 2px solid var(--os-border-strong);
  border-radius: var(--radius-pill);
}
.err-btns button:hover { background: var(--os-accent); border-color: var(--os-accent); color: var(--on-accent); }

/* ---- terminal ----
   .wbody is a flex column, so #term taking flex:1 is what makes the terminal
   fill a resized window instead of leaving a gap under it. */
#term {
  flex: 1 1 auto;
  min-height: 300px;
  max-height: 420px;
  /* VERTICAL ONLY. A terminal is a column of lines: a long one should keep
     going onto the next line, not push a bar under the window and hide itself
     off to the right where nobody scrolls. overflow-wrap catches the one thing
     white-space alone will not break -- an unbroken run of characters longer
     than the window, which is exactly what a url or a stack trace is. */
  overflow-y: auto;
  overflow-x: hidden;
  overflow-wrap: anywhere;
  padding:  var(--space-lg);
  border-radius: var(--radius-lg);
  background: #221a44;
  color: #d8cdf5;
  font: 400 var(--text-body)/1.75 var(--font-mono);
}
.win[data-resized] #term { max-height: none; }
#term b { color: #ff9ecb; font-weight: 700; }
#term .term__err { color: #ffb3b3; }
#term .term__hint { color: #9d8fd0; font-style: italic; }
/* ---- the high-score table ----
   An arcade board is a fixed grid with the rank hard left, the score hard
   right, and dot leaders carrying the eye across. The leaders are a repeating
   gradient rather than a row of typed periods, so they stay put at any name
   length and never wrap onto the next line. */
.hiscore {
  margin: var(--space-md) 0;
  padding: var(--space-md) var(--space-lg) var(--space-lg);
  border: 1px solid #4b3d86;
  border-radius: var(--radius-md);
  background: #1b1438;
}
.hiscore__head {
  display: block;
  margin-bottom: var(--space-sm);
  color: #ffd6a5 !important;   /* beats `#term b`, which is the terminal pink */
  letter-spacing: .12em;
  text-align: center;
}
.hiscore__list { margin: 0; padding: 0; list-style: none; }
.hiscore__list li {
  display: grid;
  grid-template-columns: 2ch minmax(0, auto) 1fr auto;
  align-items: baseline;
  line-height: 1.55;
  /* The leaders run the whole row and the name and score cover the parts they
     occupy. Drawing them the other way round -- a rule sized to the gap -- has
     to know the gap, and the gap is whatever the name did not use. */
  background:
    repeating-linear-gradient(to right, #4b3d86 0 1px, transparent 1px 5px)
    left calc(1.55em - .34em) / 100% 1px no-repeat;
}
.hiscore__list i {
  color: #8f7fd0;
  font-style: normal;
  text-align: right;
  background: #1b1438;
}
.hiscore__list span {
  overflow: hidden;
  padding: 0 .75ch;
  background: #1b1438;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.hiscore__list em {
  grid-column: 4;
  padding-left: .75ch;
  color: #d8cdf5;
  font-style: normal;
  font-variant-numeric: tabular-nums;
  background: #1b1438;
}
.hiscore__list .is-top i,
.hiscore__list .is-top span,
.hiscore__list .is-top em { color: #ffd6a5; }
.hiscore__you {
  display: block;
  margin-top: var(--space-sm);
  color: #9d8fd0 !important;
  font-weight: 400;
  font-style: italic;
  text-align: right;
}

#term-in {
  flex-shrink: 0;
  width: 100%;
  margin-top:  var(--space-md);
  padding:  var(--space-md) var(--space-lg);
  font: 400 var(--text-body)/var(--lh-body) var(--font-mono);
  color: var(--os-ink);
  background: var(--os-pill);
  border: 2px solid var(--os-border-strong);
  border-radius: var(--radius-pill);
  outline: none;
}
#term-in:focus { border-color: var(--os-accent); }
#term-in[readonly] { opacity: 0.5; }
#snake {
  display: block;
  margin:  var(--space-md) auto var(--space-hair);
  border-radius: var(--radius-md);
  background: #150f2e;
  image-rendering: pixelated;
}

/* halos: what `matrix` and angel mode rain down */
.halo {
  position: fixed;
  top: -30px;
  z-index: var(--z-overlay);
  font-size: var(--text-subhead);
  pointer-events: none;
  animation: halo-fall var(--ease-linear) forwards;
}
@keyframes halo-fall {
  to { transform: translateY(112vh) rotate(240deg); opacity: 0.2; }
}
@keyframes halo-drop {
  to { transform: translateY(112vh); opacity: 0.2; }
}

/* ---- credits ---- */
#credits {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: none;
  overflow: hidden;
  background: #150f2ef2;
}
#credits[data-open] { display: block; }
/* it sees itself out */
#credits { opacity: 1; transition: opacity var(--motion-slow) var(--ease-standard); }
#credits[data-fade] { opacity: 0; }
.credits__roll {
  position: absolute;
  inset: 100% 0 auto 0;
  text-align: center;
  font: 400 var(--text-lead)/2.4 var(--font-mono);
  color: #e2d8f8;
  animation: credits-roll 24s var(--ease-linear) forwards;
}
/* was `font: 400 32px var(--font-display)` around the literal text "icybearOS" */
.credits__mark {
  display: block;
  width: 210px;
  margin:  0 auto var(--space-lg);
  aspect-ratio: 1500 / 367;
}
.credits__roll i {
  display: block;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  font-style: normal;
  letter-spacing: var(--track-wide);
  color: #ff9ecb;
  margin-top:  var(--space-xl);
}
@keyframes credits-roll { to { transform: translateY(-170%); } }

/* ---- the icon set: flat brand SVGs, one file each, swappable in place ---- */

/* The sticker outline is white, which is the whole identity -- and on the light
   wallpapers (base day/snow, strawberry day/snow, archangel day) it dissolves
   into a near-white glass tile and takes that identity with it. A tinted
   keyline under the artwork restores the edge without touching the SVGs, and
   doubles as depth on the dark modes. */
.icon__art img,
.dbtn img,
#pstatus button img,
.mitem img {
  filter: drop-shadow(0 1px 1.5px var(--icon-key))
          drop-shadow(0 0 1px var(--icon-key));
}
body { --icon-key: rgba(96, 68, 150, 0.40); }   /* harmless anywhere */
body[data-mode="night"] { --icon-key: rgba(16, 8, 40, 0.44); }
body[data-wall="arcade"] { --icon-key: rgba(10, 6, 32, 0.50); }

.icon__art img {
  display: block;
  width: 62%;
  height: 62%;
  pointer-events: none;
}
.dbtn img,
#pstatus button img,
.mitem img {
  display: block;
  width: 16px;
  height: 16px;
}
.mitem img { width: 15px; height: 15px; }
.chip img { display: block; width: 16px; height: 16px; }
.notif__icon img { display: block; width: 64%; height: 64%; }
.app-hero { display: flex; justify-content: center; }
.app-hero img { width: 46px; height: 46px; }
.stk__art img { display: block; width: 30px; height: 30px; margin: 0 auto; }
#gn .w img { width: 44px; height: 44px; }
.dbtn, .chip, #pstatus button { display: inline-flex; align-items: center; justify-content: center; }
#hdock .dbtn { padding:  var(--space-md) var(--space-lg); }

/* --------------------------------------------------------------------------
   14. THE SECOND PASS — pillars, skill tree, contact, guestbook paging, crash
   -------------------------------------------------------------------------- */

/* ---- in-window app links: a link inside one app that opens another. Beats
        telling someone to count icons, which breaks the moment they drag one. */
.applink {
  padding: 0;
  border: none;
  background: none;
  font: 800 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.applink:hover { color: var(--os-ink); }
.applink--quiet {
  display: inline-block;
  margin-top:  var(--space-md);
  font-weight: 700;
  font-size: var(--text-body);
  color: var(--os-ink-dim);
}
.wbody .experience-card .applink { align-self: flex-start; margin-top:  var(--space-hair); font-size: var(--text-body); }

/* ---- the six pillars, one taxonomy for the whole OS ---- */
.specs { display: flex; flex-direction: column; gap:  var(--space-xs); }
.specs div {
  display: flex;
  align-items: baseline;
  gap:  var(--space-md);
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
}
.specs dt {
  flex-shrink: 0;
  color: var(--os-ink-dim);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}
.specs .dots {
  flex: 1;
  height: 1px;
  margin-bottom:  var(--space-xs);
  border-bottom: 1.5px dotted var(--os-border);
}
.specs dd { color: var(--os-ink); font-weight: 700; }
.specs dd::before {
  content: '';
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-right:  var(--space-sm);
  border-radius: var(--radius-circle);
  background: var(--pillar);
  box-shadow: 0 0 8px var(--pillar);
}
.specs--inline { margin:  var(--space-hair) 0 var(--space-xs); }
.specs--roomy { margin:  var(--space-md) 0 var(--space-xs); gap:  var(--space-sm); }
.specs--roomy div { font-size: var(--text-body); }

#specs-chip {
  padding:  var(--space-sm) var(--space-lg);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--os-ink-dim);
  background: var(--os-pill);
  border: 1.5px solid var(--os-border-strong);
  border-radius: var(--radius-pill);
  z-index: var(--z-icons);
  transition: color var(--motion-state) var(--ease-standard), border-color var(--motion-state) var(--ease-standard);
}
#specs-chip:hover { color: var(--os-accent); border-color: var(--os-accent); }
#specs-panel {
  width: 268px;
  max-width: 100%;
  padding:  var(--space-xl) var(--space-xl);
  border-radius: var(--radius-xl);
  animation: specs-in var(--motion-exit) var(--ease-enter);
}
#specs-panel .os-mono { margin-bottom:  var(--space-md); }
@keyframes specs-in { from { opacity: 0; transform: translateY(-8px) scale(0.97); } }
/* Constrained, never removed. This used to `display: none` the chip AND the
   panel below 980px — but the desktop shell runs all the way down to 701px, and
   that chip is the ONLY route to system_specs on desktop. So between 701 and
   980 (and on any short window) the app was unreachable and `the long way
   round` was unwinnable, with nothing to tell you why.

   The crowding it was guarding against is real; the answer is to bound the
   panel, not to delete the door. */
@media (max-width: 980px), (max-height: 620px) {
  #specs-panel {
    width: min(268px, 38vw);
    max-height: 52vh;
    overflow: auto;
  }
}

/* ---- skill tree: a spine with six branches ---- */
.skill-tree { display: flex; flex-direction: column; gap: 0; margin-top:  var(--space-hair); }
.branch {
  position: relative;
  padding:  0 0 var(--space-lg) var(--space-xl);
  border-left: 2px solid var(--os-border);
}
.branch:last-child { border-left-color: transparent; padding-bottom:  var(--space-hair); }
.branch::before {
  content: '';
  position: absolute;
  left: -6px;
  top: 3px;
  width: 10px;
  height: 10px;
  border-radius: var(--radius-circle);
  background: var(--pillar);
  box-shadow: 0 0 0 3px var(--os-surface), 0 0 10px var(--pillar);
}
.branch::after {
  content: '';
  position: absolute;
  left: 0;
  top: 8px;
  width: 12px;
  height: 2px;
  background: var(--os-border);
}
.branch__name {
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--pillar-ink, var(--os-accent));
  margin-bottom:  var(--space-sm);
}
.branch .tag-row { gap:  var(--space-xs); }

/* ---- contact ---- */
.contact { display: flex; align-items: center; gap:  var(--space-md); flex-wrap: wrap; }

.contact__handle { font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono); color: var(--os-ink-dim); }
/* identity + contact stack beside the avatar, so the two lines together are
   about the height of the pfp rather than a third row under it */
.mood__id { display: flex; flex-direction: column; gap:  var(--space-xs); min-width: 0; }
.mood .contact-pill {
  gap:  var(--space-sm);
  padding:  var(--space-xs) var(--space-md);
  border: none;
  background-color: var(--os-glass);
}
.contact-pill__link { display: inline-flex; color: var(--os-ink-dim); }
.contact-pill__link:hover { color: var(--os-accent); }
.contact-pill__link svg { width: 12px; height: 12px; fill: currentColor; }
.mood .contact-pill__handle { font-size: var(--text-caption); letter-spacing: var(--track-tight); color: var(--os-ink-dim); }

/* ---- guestbook: dates, a count, and real paging ---- */
.wall-count {
  flex-shrink: 0;
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink-dim);
}
.gstamp__d {
  margin-top:  var(--space-hair);
  font: 700 var(--text-micro)/var(--lh-micro) var(--font-mono);
  letter-spacing: var(--track-tight);
  color: var(--os-ink-dim);
  opacity: 0.75;
}
/* the live name hint: it corrects you while you type rather than after you
   have composed a name and pressed the button */
.guest-hint {
  margin:  var(--space-sm) var(--space-hair) 0;
  min-height: 1.1em;
  font: 700 var(--text-caption)/1.25 "Space Mono", monospace;
  color: var(--os-accent);
  text-shadow: 0 1px 0 var(--os-surface);
}

.wall-empty {
  grid-column: 1 / -1;
  padding:  var(--space-2xl) var(--space-lg);
  text-align: center;
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--os-ink-dim);
}
.pager {
  display: flex;
  justify-content: center;
  align-items: center;
  gap:  var(--space-xs);
  margin-top:  var(--space-md);
  flex-wrap: wrap;
}
.pager:empty { display: none; }
.pager button {
  min-width: 24px;
  padding:  var(--space-xs) var(--space-md);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  color: var(--os-ink);
  background: var(--os-pill);
  border: 1.5px solid var(--os-border-strong);
  border-radius: var(--radius-sm);
}
.pager button:hover:not(:disabled) { border-color: var(--os-accent); color: var(--os-accent); }
.pager button[aria-current="true"] {
  background: var(--os-accent);
  border-color: var(--os-accent);
  color: #fff;
}
.pager button:disabled { opacity: 0.35; }
.pager .gap { font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono); color: var(--os-ink-dim); padding:  0 var(--space-hair); }

/* ---- quote ---- */
.quote-send { margin-top:  var(--space-xl); text-align: center; }
.quote-send .button { width: 100%; justify-content: center; }
#q-need .pill::after, #q-budget .pill::after { content: none; }

/* --------------------------------------------------------------------------
   THE CRASH

   Three beats: the desktop itself comes apart, the tube snaps on, and the
   machine tries to recover before giving up. Dismissing collapses it to a line
   and then a dot, the way a CRT actually dies.
   -------------------------------------------------------------------------- */

body[data-glitch] #desktop,
body[data-glitch] #menubar,
body[data-glitch] #dock,
body[data-glitch] .win {
  animation: glitch-slice 0.16s steps(2) infinite;
}
@keyframes glitch-slice {
  0% { transform: none; filter: none; clip-path: none; }
  18% { transform: translateX(-7px); filter: hue-rotate(60deg) saturate(2.4); clip-path: inset(24% 0 48% 0); }
  36% { transform: translateX(8px); filter: hue-rotate(-70deg) contrast(1.4); clip-path: inset(56% 0 14% 0); }
  54% { transform: translateX(-3px); filter: invert(0.14); clip-path: none; }
  72% { transform: translateX(5px); filter: hue-rotate(140deg); clip-path: inset(6% 0 72% 0); }
  100% { transform: none; filter: none; clip-path: none; }
}


#bsod {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: none;
  background: #000;
}
#bsod[data-open] { display: block; }
.bsod__screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap:  var(--space-lg);
  padding:  var(--space-2xl);
  text-align: center;
  color: #fff;
  background: linear-gradient(150deg, #8fc2f4, #b99df0);
  transform-origin: center;
  animation: crt-on var(--motion-enter) var(--ease-standard);
}
#bsod[data-closing] .bsod__screen { animation: crt-off var(--motion-enter) var(--ease-standard) forwards; }
/* the tube snapping on: a bright line that opens out, then settles */
@keyframes crt-on {
  0% { transform: scaleY(0.004); opacity: 0; filter: brightness(6); }
  28% { transform: scaleY(0.004); opacity: 1; filter: brightness(6); }
  62% { transform: scaleY(1); filter: brightness(1.7); }
  100% { transform: none; filter: none; }
}
@keyframes crt-off {
  0% { transform: none; filter: none; }
  42% { transform: scaleY(0.004) scaleX(1); filter: brightness(5); }
  100% { transform: scaleY(0.004) scaleX(0); filter: brightness(9); opacity: 0; }
}
/* scanlines over the whole thing, because it is a tube */
.bsod__screen::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.13) 0 1px, transparent 1px 3px);
}
.bsod__face {
  font: 400 clamp(var(--text-hero), 12vw, 76px) var(--font-display);
  text-shadow: 0 4px 0 #6b3da0;
  animation: bsod-blink 3.4s steps(1) infinite;
}
@keyframes bsod-blink { 0%, 93%, 100% { opacity: 1; } 95% { opacity: 0.25; } }

/* channel split: the headline is printed three times, slightly out of register */
.bsod__t1 {
  position: relative;
  font: 700 clamp(var(--text-lead), 3.4vw, 16px) var(--font-mono);
  letter-spacing: var(--track-wide);
}
.bsod__t1::before,
.bsod__t1::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
}
.bsod__t1::before { color: #ff4ecd; animation: ch-a 1.1s steps(2) infinite; }
.bsod__t1::after { color: #4ee0ff; animation: ch-b 0.9s steps(2) infinite; }
@keyframes ch-a { 0%, 100% { transform: translate(-2px, 0); } 50% { transform: translate(2px, -1px); } }
@keyframes ch-b { 0%, 100% { transform: translate(2px, 0); } 50% { transform: translate(-2px, 1px); } }

.bsod__t2 { font: 400 var(--text-body)/var(--lh-body) var(--font-mono); letter-spacing: var(--track-wide); opacity: 0.9; }
.bsod__trace {
  margin:  var(--space-hair) 0;
  font: 400 var(--text-caption)/1.9 var(--font-mono);
  opacity: 0.72;
  text-align: left;
  white-space: pre;
}
.bsod__press { margin-top:  var(--space-md); animation: boot-blink 1.2s steps(2) infinite; }

@media (prefers-reduced-motion: reduce) {
  body[data-glitch] #desktop, body[data-glitch] #menubar,
  body[data-glitch] #dock, body[data-glitch] .win { animation: none; }
  .bsod__screen, .bsod__t1::before, .bsod__t1::after, .bsod__face { animation: none; }
}

/* ---- the snowman: same pixel discipline, six frames, built in stages ---- */
#snowman {
  --px: 72px;
  position: fixed;
  bottom: 74px;
  left: 56vw;
  z-index: var(--z-decor);
  width: var(--px);
  height: var(--px);
  background-image: url('images/os/snowman.png');
  background-repeat: no-repeat;
  background-size: calc(var(--px) * 6) var(--px);
  background-position: 0 0;
  image-rendering: pixelated;
  filter: drop-shadow(0 4px 5px var(--os-shadow));
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-enter) var(--ease-standard);
}
#snowman[data-stage] { opacity: 1; }
#snowman[data-stage="1"] { background-position-x: 0; }
#snowman[data-stage="2"] { background-position-x: calc(var(--px) * -1); }
#snowman[data-stage="3"] { background-position-x: calc(var(--px) * -2); }
#snowman[data-stage="4"] { background-position-x: calc(var(--px) * -3); }
#snowman[data-stage="5"] { background-position-x: calc(var(--px) * -4); }
/* the poof frame, scaled and lifted away by CSS so it disperses smoothly */
#snowman[data-poof] {
  background-position-x: calc(var(--px) * -5);
  animation: snow-poof var(--motion-enter) var(--ease-standard) forwards;
}
@media (prefers-reduced-motion: reduce) { #snowman { transition: none; } }

/* --------------------------------------------------------------------------
   15. THE BEAR

   One 40px sprite sheet, 8 columns x 8 rows, scaled by whole numbers only and
   rendered with image-rendering:pixelated — so every art-pixel stays a square
   of exactly the same size. Never scale it by a fraction.

   State lives on <body>, so the desktop bear and the phone bear are always
   doing the same thing without either one owning the other.
   -------------------------------------------------------------------------- */

.bear {
  position: absolute;
  z-index: var(--z-bear);
  text-align: center;
  transition: left 0s var(--ease-linear);
  touch-action: none;
}
#bear-desk { bottom: 76px; left: 38vw; }
/* the phone gives the bear its own band above the dock rather than letting it
   walk over the app grid */
#bear-phone { bottom: 74px; left: 14px; z-index: 3; }
#bear-phone .bear__say {
  left: 0;
  transform: none;
  max-width: 62vw;
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-xs);
}
#bear-phone .bear__verbs { left: 0; transform: none; margin-bottom:  var(--space-3xl); }
/* the prompt anchors from the bear's left edge on a phone, like its speech
   bubble does, and is capped so it cannot run off a 320px screen */
#bear-phone .bear__name {
  left: 0;
  transform: none;
  width: min(208px, 78vw);
  margin-bottom:  var(--space-xl);
}
#bear-phone .bear__verbs button { width: 30px; height: 30px; }
#bear-phone .bear__verbs img { width: 15px; height: 15px; }

.bear__sprite {
  /* 40px source frames, scaled by a WHOLE number. The old sheet was 32px shown
     at 72 and 52 — 2.25x and 1.625x — which gives pixel art uneven pixels.
     80 is 2x and 40 is 1x. */
  --px: 80px;
  width: var(--px);
  height: var(--px);
  background-image: url('images/os/bear.png');
  background-repeat: no-repeat;
  background-size: calc(var(--px) * 8) calc(var(--px) * 8);
  background-position: 0 0;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  filter: drop-shadow(0 4px 5px var(--os-shadow));
}
/* No scaleX flip. The sprite is front-facing — it looks at you — so mirroring
   it changes exactly one visible thing: the bow teleports to the other ear.
   A bow is an object tied to an ear, not a reflection. Direction is already
   carried by the bear physically crossing the screen and by the walk cycle;
   it did not need a flip that only ever broke the one asymmetric detail. */

/* Row = state, column = frame, and the rows are ragged: a blink needs two
   frames and a breath needs eight. Explicit steps() per row rather than a
   variable, because a dropped var() in a timing function kills the animation
   outright rather than degrading. */
@keyframes bear2 { to { background-position-x: calc(var(--px) * -2); } }
@keyframes bear4 { to { background-position-x: calc(var(--px) * -4); } }
@keyframes bear6 { to { background-position-x: calc(var(--px) * -6); } }
@keyframes bear8 { to { background-position-x: calc(var(--px) * -8); } }

body[data-bear="idle"]  .bear__sprite { background-position-y: 0; animation: bear8 1.7s steps(8) infinite; }
body[data-bear="blink"] .bear__sprite { background-position-y: calc(var(--px) * -1); animation: bear2 0.44s steps(2) infinite; }
body[data-bear="walk"]  .bear__sprite { background-position-y: calc(var(--px) * -2); animation: bear4 0.62s steps(4) infinite; }
body[data-bear="happy"] .bear__sprite { background-position-y: calc(var(--px) * -3); animation: bear4 0.5s steps(4) infinite; }
body[data-bear="eat"]   .bear__sprite { background-position-y: calc(var(--px) * -4); animation: bear2 0.34s steps(2) infinite; }
body[data-bear="sleep"] .bear__sprite { background-position-y: calc(var(--px) * -5); animation: bear4 3.2s steps(4) infinite; }
body[data-bear="sad"]   .bear__sprite { background-position-y: calc(var(--px) * -6); animation: bear2 2s steps(2) infinite; }
body[data-bear="dance"] .bear__sprite { background-position-y: calc(var(--px) * -7); animation: bear6 1.1s steps(6) infinite; }

/* The phone keeps the same 2x scale rather than dropping to 1x: 40px would put
   the bear barely taller than its own name plate. */

/* the two answers live inside the bubble, so the question and its options are
   one object rather than a bubble plus a floating pair of buttons */
/* .bubble is pointer-events:none — it is a role="status", not a control. When
   it carries a question it has to take clicks back, and the buttons re-enable
   themselves explicitly rather than relying on inheriting it. */
.bubble[data-ask] { pointer-events: auto; }
.bubble__ask { display: flex; gap:  var(--space-sm); margin-top:  var(--space-md); pointer-events: auto; }
.bubble__ask button {
  border: none;
  cursor: pointer;
  pointer-events: auto;
  padding:  var(--space-xs) var(--space-lg);
  border-radius: var(--radius-pill);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-tight);
  color: var(--on-cta);
  background: var(--t-cta);
  box-shadow: var(--elev-raised) var(--os-shadow);
}
.bubble__ask button:last-child { background: var(--os-pill); color: var(--os-ink); }
/* "i'll wait" is only funny if something is visibly waiting */
.bubble[data-waiting]::after {
  content: ' ···';
  animation: waiting 1.4s steps(4) infinite;
}
@keyframes waiting { 0% { opacity: 0.25; } 50% { opacity: 1; } 100% { opacity: 0.25; } }

/* FUS RO DAH. The shake is on <body> so the whole machine moves at once,
   chrome and windows and all — shaking only the desktop would have left the
   menu bar standing still through a thu'um. */
body.shout { animation: shout var(--motion-enter) var(--ease-standard); }
@keyframes shout {
  0%   { transform: translate(0, 0) rotate(0); }
  8%   { transform: translate(-9px, 4px) rotate(-0.5deg); }
  18%  { transform: translate(8px, -5px) rotate(0.45deg); }
  28%  { transform: translate(-7px, -3px) rotate(-0.4deg); }
  40%  { transform: translate(6px, 4px) rotate(0.3deg); }
  54%  { transform: translate(-4px, -2px) rotate(-0.2deg); }
  70%  { transform: translate(3px, 2px) rotate(0.15deg); }
  85%  { transform: translate(-2px, -1px); }
  100% { transform: translate(0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  body.shout { animation: none; }
  .bear__sprite { animation: none !important; }
  .bear { transition: none !important; }
}

.bear__plate {
  display: inline-block;
  margin-top:  var(--space-hair);
  padding:  var(--space-hair) var(--space-md);
  font: 700 var(--text-caption)/var(--lh-caption) var(--font-mono);
  letter-spacing: var(--track-label);
  color: var(--os-ink);
  background: var(--os-pill);
  border-radius: var(--radius-pill);
  /* it is a control now: click it to rename. Free, always -- charging to fix a
     typo would be punitive. */
  cursor: pointer;
  transition: transform var(--motion-state) var(--ease-standard), box-shadow var(--motion-state) var(--ease-standard);
}
.bear__plate:hover { transform: translateY(-1px) scale(1.04); }
.bear__plate:focus-visible {
  outline: 2px solid var(--os-accent);
  outline-offset: 2px;
}

.bear__say {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom:  var(--space-sm);
  padding:  var(--space-sm) var(--space-lg);
  width: max-content;
  max-width: min(230px, 46vw);
  white-space: normal;
  line-height: 1.45;
  text-align: left;
  font: 700 var(--text-body)/var(--lh-body) var(--font-body);
  color: var(--purple-text);
  background: #fff;
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-xs);
  box-shadow: var(--elev-raised) var(--os-shadow);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-exit);
}
.bear__say[data-show] { opacity: 1; }

.bear__verbs {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 34px;
  display: none;
  gap:  var(--space-md);
}
.bear__verbs[data-open] { display: flex; }
.bear__verbs button {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 2px solid #fff;
  border-radius: var(--radius-circle);
  font-size: var(--text-lead);
  color: var(--t-bar-ink);
  background: var(--t-art);
  box-shadow: var(--elev-raised) var(--os-shadow);
  transition: transform var(--motion-state) var(--ease-standard);
}
.bear__verbs button:hover { transform: scale(1.14); }
.bear__verbs img { width: 17px; height: 17px; }

.bear__name {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom:  var(--space-lg);
  width: 208px;
  padding:  var(--space-lg);
  border-radius: var(--radius-lg);
  background: var(--os-surface);
  box-shadow: var(--elev-floating) var(--os-shadow), inset 0 0 0 1.5px var(--os-inset);
  z-index: 20;
}
.bear__name-t { font: 800 var(--text-body)/1.4 var(--font-body); color: var(--os-ink); text-align: center; }
.bear__name input {
  width: 100%;
  margin:  var(--space-md) 0;
  padding:  var(--space-md) var(--space-lg);
  text-align: center;
  font: 700 var(--text-body)/var(--lh-body) var(--font-mono);
  color: var(--os-ink);
  background: var(--os-pill);
  border: 2px solid var(--os-border-strong);
  border-radius: var(--radius-pill);
  outline: none;
}
.bear__name input:focus { border-color: var(--os-accent); }
.bear__name .button { width: 100%; }

/* the floaties a verb throws off */
.floaty {
  position: fixed;
  z-index: var(--z-floaty);
  font-size: var(--text-lead);
  pointer-events: none;
  animation: floaty 1.4s var(--ease-standard) forwards;
}
@keyframes floaty { to { transform: translateY(-70px); opacity: 0; } }
/* --------------------------------------------------------------------------
   16. TABLET  (no new z-index -- this IS the desktop shell, re-proportioned)

   There were two shells and three shapes of screen. Below 700 the phone takes
   over and the site is the phone. Above about 1200 the desktop has room for its
   own composition: a column of icons against the left wall, icy standing on the
   right, and windows cascading through the gap between them. A portrait tablet
   has neither. It is wide enough that it was being handed the desktop, and far
   too narrow to hold the desktop's three columns, so the gap the windows
   cascade through was never there and icy was simply deleted at 880px rather
   than fitted.

   What a portrait slab has instead is HEIGHT. So the composition rotates: the
   apps become a grid across the top the way a tablet home screen is, the mood
   ring keeps the top right, icy stands at the bottom right, and windows open
   under the apps rather than beside them.

   Landscape tablets are not in here on purpose. A landscape iPad is a short,
   wide desktop and the desktop layout already fits it -- it has the horizontal
   gap, and rotating into it is a real change in what the screen can hold rather
   than a cosmetic one. The only thing both orientations share is the dock,
   below, and that is bounded by width because it is a width problem.
   -------------------------------------------------------------------------- */
/* The dock is one row or it is nothing. It wraps at 96vw, and on the narrowest
   slabs that landed the bin on a second line under everything else -- which is
   not cosmetic: the dock grows a button per open window, so the second row is
   the one that moves, and it moves up into icy's caption. Four pixels off the
   gap and eight off the ends buys back the 40 it needed, without touching the
   buttons themselves. A width bound rather than an orientation one, because
   this is a width problem: 912 and up already fit, so they are left alone. */
@media (min-width: 701px) and (max-width: 900px) {
  #dock { gap: var(--space-hair); padding: var(--space-md) var(--space-lg); }
}

/* ---- portrait slab: the composition rotates ---- */
@media (min-width: 701px) and (max-width: 1200px) and (orientation: portrait) {
  /* Windows were sized for a 1440 desk and then allowed onto an 820 one. 92vw
     of an 820 slab is a 754px window, which is the whole screen, and read_me's
     own 780 was wider than the device. Portrait only: a landscape slab is a
     short wide desk with 1180 of room, and 733 of window on it was never the
     problem this is solving. */
  .win { max-width: min(92vw, 660px); }

  /* THE APPS GO ACROSS THE TOP. Same element, same drag-to-reorder, one axis
     turned: a column that wraps into columns becomes a row that wraps into
     rows. `right` is what makes it a grid rather than a strip -- it stops the
     row short of the mood ring instead of running under it, and at these widths
     that lands on three to six per row, which is the shape of a home screen.
     `bottom: auto` because the block is now sized by its contents; the base
     rule pins it to the dock, which is what forced the column in the first
     place. */
  #icons {
    top: 150px;
    left: 28px;
    right: 272px;
    bottom: auto;
    max-width: none;
    flex-direction: row;
  }

  /* She was `display: none` under 880px, which is most of the tablets people
     actually hold. The desktop hid her because there was no room beside a left
     column of icons; with the icons across the top the bottom right is empty
     and it is hers. Sized off the floor rather than the viewport: 620 is the
     worst-case app grid (four rows on the narrowest slab) plus her own 74px of
     ground clearance, so she cannot grow up into the apps on any width in the
     band. */
  #standee {
    display: block;
    right: 24px;
  }
  #standee img {
    /* 46vh was cautious: it was set to clear an icon COLUMN, and the icons went
       to the top in this shell, so the whole width below them is hers. The
       desktop gives her about 70vh and this was giving 46, which is why she read
       as shrunken rather than merely smaller. */
    max-height: min(56vh, calc(100vh - 540px));
    max-width: 76vw;
  }

  /* The window body's cap is furniture arithmetic, and the furniture moved. The
     desktop subtracts 228: menu bar, the drop below it, the dock and a title
     bar. Here the app grid sits between the menu bar and the first window, so
     the grid has to be in the sum too -- and it is the one piece that is not a
     constant. Twelve icons are two rows on a 1024 slab, three on an 820 and
     four on a 744: a 184px swing that a stylesheet cannot see. Worst-casing it
     would cost the wider slabs 184px of window for rows they do not have, so
     os.js measures the grid and publishes its floor as --icons-b.

     What is left is genuinely fixed. Untabbed: 26 of drop, 44 of title bar, 64
     of dock and 20 of air = 154. Tabbed pays 39 more for its tab strip = 193.
     The 52vh term does nothing at these sizes and exists so a 1366-tall slab
     does not produce a wall of window, which is the same job it does on the
     desktop. The fallback covers the frame before os.js has measured; no window
     is open then. */
  .win .wbody:not(.wbody--tabbed) {
    max-height: min(52vh, calc(100vh - var(--icons-b, 480px) - 154px));
  }
  .win .wbody--tabbed {
    height: min(52vh, calc(100vh - var(--icons-b, 480px) - 193px));
  }
  .win[data-resized] .wbody,
  .win[data-zoom] .wbody { max-height: none; }
  .win[data-resized] .wbody--tabbed,
  .win[data-zoom] .wbody--tabbed { height: auto; }
}
