:root {
  /* clean white throughout, not the brown-tinted paper of earlier passes --
     a refined, high-contrast neutral base. Windows lift off the same white
     desktop via their border + shadow (see .window) rather than a tinted
     backdrop; the accent stays a considered, single deep red rather than a
     per-app rainbow of stripes. */
  --bg: #ffffff;
  --panel: #ffffff;
  --panel-2: #f2f2f0;
  --border: #1a1a1a;
  --text: #161616;
  --dim: #6b6b6b;
  --accent: #b7362b;
  --on-accent: #ffffff;
  --danger: #a8321f;
  /* the "grid" desktop background's neon line color -- kept in sync by
     hand with NEON_LIGHT in index.html's inline brightness script, the
     same way every other var above mirrors a DARK/LIGHT stop there */
  --grid-neon: #2da5ff;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, "Courier New", monospace;
  /* a script-ish face reserved for small decorative accents (see
     .work-back's kin) -- falls back gracefully where the specific face
     isn't installed, since it's not a webfont this project ships */
  --hand: "Segoe Print", "Bradley Hand", "Chalkboard SE", "Comic Sans MS", cursive;
  /* the high-contrast fashion-magazine serif for the works editorial feed
     (see .work-detail-title and kin) -- system faces only, no webfont
     shipped, so it falls back gracefully the same way --hand does */
  --editorial-serif: Didot, "Bodoni MT", "Playfair Display", Georgia, "Times New Roman", serif;
  /* a decorative-font pass for titles/labels (work titles, icon names,
     window titles) was tried and rolled back -- these two hooks stay
     defined as plain mono so nothing that referenced them needs to
     change, ready to point at something else later */
  --font-title: var(--mono);
  --font-label: var(--mono);

  --cur-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpolygon points='1,1 1,15 4,12 6,17 8,16 6,11 10,11' fill='%23161616' stroke='%23ffffff' stroke-width='1'/%3E%3C/svg%3E") 1 1, default;
  --cur-wait: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpolygon points='4,2 16,2 10,9' fill='%23b7362b' stroke='%23ffffff' stroke-width='1'/%3E%3Cpolygon points='4,18 16,18 10,11' fill='%23b7362b' stroke='%23ffffff' stroke-width='1'/%3E%3C/svg%3E") 10 10, wait;
  --vignette: rgba(20,20,20,0.78);
  --selection-fill: rgba(217,26,157,0.28);
  /* soft internal seam -- used where two chrome panels (titlebar/sidebar/
     menu-bar/taskbar) meet each other, so the division reads as a subtle
     step rather than a hard rule. var(--border) stays reserved for a
     window's own outer edge, where real contrast against the desktop is
     still wanted. */
  --divider: rgba(26,26,26,0.10);
  --ui-scale: 1;
  --icon-scale: 1;
}

* { box-sizing: border-box; }

/* the browser's own text-selection highlight (dragging over a paragraph,
   not the desktop icon rubber-band box below, though both now share the
   same magenta via --selection-fill) -- instead of whatever blue the
   browser defaults to */
::selection {
  background: var(--selection-fill);
  color: var(--text);
}

.reduce-motion,
.reduce-motion * {
  transition: none !important;
  animation: none !important;
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--mono);
  overflow: hidden;
  cursor: var(--cur-arrow);
}

body.busy, body.busy * {
  cursor: var(--cur-wait) !important;
}

button, .icon, .work-row, .taskbar-item,
.context-menu-item, .window-titlebar, .work-back,
.dropdown-item, .search-result-row {
  cursor: var(--cur-arrow);
}

/* every real outbound hyperlink gets the browser's native hand cursor
   instead of the OS-chrome arrow everything else uses -- the one place a
   familiar "this leaves the site" cue is worth more than aesthetic
   consistency (see .sw-name / .work-link a below for the matching text
   treatment) */
a[href] {
  cursor: pointer;
}

/* ---------- BOOT SCREEN ---------- */

#boot-screen {
  position: fixed;
  inset: 0;
  background: var(--bg);
  color: var(--text);
  font-size: calc(15px * var(--ui-scale));
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
}

.boot-center {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.boot-avatar {
  width: 72px;
  height: 72px;
  border-radius: 0;
  border: 1px solid var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.boot-avatar svg {
  width: 70%;
  height: 70%;
  color: var(--text);
}

.boot-identity {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}

.boot-signing-label {
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.1em;
  color: var(--dim);
  text-align: center;
}

.boot-username {
  font-size: calc(18px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.1em;
  text-align: center;
  /* on a narrow phone (or with Settings > Size set to Large) a long
     persona name can wrap to two lines -- .boot-identity's align-items:
     center only centers the whole text block, not each wrapped line
     inside it, so without this the shorter second line sits ragged-left */
  max-width: min(280px, 82vw);
}

.boot-progress-track {
  width: 260px;
  height: 3px;
  background: var(--border);
  overflow: hidden;
}

.boot-progress-fill {
  height: 100%;
  width: 0%;
  background: var(--text);
  transition: width 0.25s ease;
}

.boot-line {
  min-height: 18px;
  font-size: calc(13px * var(--ui-scale));
  color: var(--text);
  display: flex;
  align-items: center;
}

#boot-cursor {
  display: inline-block;
  margin-left: 2px;
  animation: blink 1s steps(1) infinite;
}

@keyframes blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

.boot-login-btn {
  margin-top: 4px;
  padding: 8px 28px;
  font-family: var(--mono);
  font-size: calc(13px * var(--ui-scale));
  letter-spacing: 0.12em;
  background: var(--text);
  color: var(--bg);
  border: 1px solid var(--text);
  cursor: var(--cur-arrow);
}

.boot-login-btn:hover {
  background: var(--bg);
  color: var(--text);
}

.boot-login-btn.hidden { display: none; }
.boot-progress-track.hidden,
.boot-line.hidden { display: none; }

#boot-screen.power-off {
  animation: boot-fade-out 0.3s ease-in forwards;
}

@keyframes boot-fade-out {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}

/* ---------- DESKTOP ---------- */

#desktop {
  position: fixed;
  inset: 0;
  background-color: var(--bg);
  /* the whole page scrolls if content (mainly the icon column) doesn't fit —
     menu-bar/taskbar/windows-layer are position:fixed so they stay put
     regardless of this scroll, only .icons actually scrolls with it.
     NOTE: perspective must NOT live here — it (like transform/filter) makes
     an element the containing block for its position:fixed descendants,
     which would drag menu-bar/taskbar/windows-layer along with this scroll.
     It's on .icons instead, which is exactly where the 3D tilt is used. */
  overflow-y: auto;
  /* dragging the selection-box shouldn't also highlight text underneath it —
     re-enabled for .window-body specifically, since selecting bio/works text
     is legitimate there */
  user-select: none;
}

.window-body,
.window-body *,
#work-lecture-overlay,
#work-lecture-overlay * {
  user-select: text;
}

/* ---------- DESKTOP BACKGROUND: "TUNNEL" ---------- */
/* an optional replacement for the plain flat desktop -- concentric rings
   racing outward from a single vanishing point at the center of the
   screen, like a wormhole/time-tunnel or a mise en abîme of the same ring
   receding into itself forever. Off by default (see .desktop-bg-tunnel,
   toggled by the desktop context menu's "Change Background" -- see
   cycleDesktopBackground() in script.js); z-index -1 keeps it strictly
   behind the icons and the vignette (#desktop::after) regardless of
   .icons' own stacking, and low-alpha color-mix against var(--text) (not
   a hardcoded tone) so it stays readable at every point of the brightness
   slider, including inverted. */
#desktop::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
}

#desktop.bg-tunnel::before {
  opacity: 1;
  /* repeating-radial-gradient already produces its own infinite rings
     within a single image -- background-repeat must stay no-repeat (the
     default is `repeat`, which was tiling that whole shrunk ring image
     across the screen like wallpaper instead of showing one set of rings
     radiating from a single centered vanishing point) */
  background-repeat: no-repeat;
  background-position: center center;
  background-image: repeating-radial-gradient(circle at center,
    color-mix(in srgb, var(--text) 16%, transparent) 0px,
    color-mix(in srgb, var(--text) 16%, transparent) 1.5px,
    transparent 1.5px,
    transparent 34px
  );
  /* 100% so the rings actually fill the whole screen (not a centered box
     with plain background around it) -- static, not animated: a fixed
     frame of the same warp the old animation used to cycle through, so
     the rings still read as receding into a single vanishing point
     without ever actually moving */
  background-size: 100% 100%;
}

/* ---------- DESKTOP BACKGROUND: "GRID" ---------- */
/* a single one-point-perspective grid -- floor, ceiling, and both walls
   all converging on one shared vanishing point at screen-center, like
   standing inside a room drawn in perspective. The earlier attempt built
   this out of three separately CSS-3D-rotated planes (a floor tipped
   with rotateX, two walls tipped with rotateY); it never actually reads
   as one continuous mesh because each plane compresses its own repeating
   grid pattern at its own rate, so the three seams never line up and the
   walls end up visibly narrower than the floor -- CSS transforms warp a
   flat 2D pattern, they don't do real shared 3D projection. This is a
   hand-computed 1-point-perspective construction instead, the same way
   an artist would rule one out: straight lines from a single center
   point to evenly spaced stops around the whole frame (the "depth"
   lines, i.e. every wall/floor/ceiling grid line receding into the
   distance) crossed with concentric frames at a fixed shrink ratio (the
   "cross" lines at each successive depth, exactly .desktop-bg-abyss's
   own math, since equal spacing in real 3D space maps to geometrically
   shrinking rectangles on screen under perspective). One flat SVG, so
   there's no seam to misalign in the first place.

   Neon, not monochrome: color is --grid-neon (set in index.html's inline
   brightness script) rather than --text, since it snaps blue-by-daylight
   / near-white-by-night instead of the usual dark/light pair -- "white
   when black" was the ask, not just a paler blue. The stacked
   drop-shadows are the bloom -- a fixed blue regardless of theme, since
   the glow is a constant accent, not something that should invert. The
   slow scale animation is the one deliberate exception to every other
   background here being static: this one was asked to feel like it's
   warping past at speed, which a perfectly still grid can't do; it's
   still gated by .reduce-motion like every other animation on the page. */
#desktop.bg-grid::before {
  opacity: 1;
  color: var(--grid-neon);
  background-repeat: no-repeat;
  background-position: center center;
  background-size: 100% 100%;
  filter:
    drop-shadow(0 0 1.5px rgba(64, 176, 255, 0.85))
    drop-shadow(0 0 6px rgba(64, 176, 255, 0.55))
    drop-shadow(0 0 16px rgba(64, 176, 255, 0.35));
  animation: bg-grid-warp 4s ease-in-out infinite alternate;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 360'%3E%3Cg fill='none' stroke='currentColor' stroke-width='0.6'%3E%3Cpath d='M320.0 180.0L0.0 0.0M320.0 180.0L32.0 0.0M320.0 180.0L64.0 0.0M320.0 180.0L96.0 0.0M320.0 180.0L128.0 0.0M320.0 180.0L160.0 0.0M320.0 180.0L192.0 0.0M320.0 180.0L224.0 0.0M320.0 180.0L256.0 0.0M320.0 180.0L288.0 0.0M320.0 180.0L320.0 0.0M320.0 180.0L352.0 0.0M320.0 180.0L384.0 0.0M320.0 180.0L416.0 0.0M320.0 180.0L448.0 0.0M320.0 180.0L480.0 0.0M320.0 180.0L512.0 0.0M320.0 180.0L544.0 0.0M320.0 180.0L576.0 0.0M320.0 180.0L608.0 0.0M320.0 180.0L640.0 0.0M320.0 180.0L640.0 32.7M320.0 180.0L640.0 65.5M320.0 180.0L640.0 98.2M320.0 180.0L640.0 130.9M320.0 180.0L640.0 163.6M320.0 180.0L640.0 196.4M320.0 180.0L640.0 229.1M320.0 180.0L640.0 261.8M320.0 180.0L640.0 294.5M320.0 180.0L640.0 327.3M320.0 180.0L640.0 360.0M320.0 180.0L608.0 360.0M320.0 180.0L576.0 360.0M320.0 180.0L544.0 360.0M320.0 180.0L512.0 360.0M320.0 180.0L480.0 360.0M320.0 180.0L448.0 360.0M320.0 180.0L416.0 360.0M320.0 180.0L384.0 360.0M320.0 180.0L352.0 360.0M320.0 180.0L320.0 360.0M320.0 180.0L288.0 360.0M320.0 180.0L256.0 360.0M320.0 180.0L224.0 360.0M320.0 180.0L192.0 360.0M320.0 180.0L160.0 360.0M320.0 180.0L128.0 360.0M320.0 180.0L96.0 360.0M320.0 180.0L64.0 360.0M320.0 180.0L32.0 360.0M320.0 180.0L0.0 360.0M320.0 180.0L0.0 327.3M320.0 180.0L0.0 294.5M320.0 180.0L0.0 261.8M320.0 180.0L0.0 229.1M320.0 180.0L0.0 196.4M320.0 180.0L0.0 163.6M320.0 180.0L0.0 130.9M320.0 180.0L0.0 98.2M320.0 180.0L0.0 65.5M320.0 180.0L0.0 32.7' stroke-opacity='0.11'/%3E%3Crect x='0.0' y='0.0' width='640.0' height='360.0' stroke-opacity='0.2'/%3E%3Crect x='57.6' y='32.4' width='524.8' height='295.2' stroke-opacity='0.178'/%3E%3Crect x='104.8' y='59.0' width='430.3' height='242.1' stroke-opacity='0.158'/%3E%3Crect x='143.6' y='80.8' width='352.9' height='198.5' stroke-opacity='0.14'/%3E%3Crect x='175.3' y='98.6' width='289.4' height='162.8' stroke-opacity='0.124'/%3E%3Crect x='201.4' y='113.3' width='237.3' height='133.5' stroke-opacity='0.11'/%3E%3Crect x='222.7' y='125.3' width='194.6' height='109.4' stroke-opacity='0.098'/%3E%3Crect x='240.2' y='135.1' width='159.5' height='89.7' stroke-opacity='0.087'/%3E%3Crect x='254.6' y='143.2' width='130.8' height='73.6' stroke-opacity='0.077'/%3E%3Crect x='266.4' y='149.8' width='107.3' height='60.3' stroke-opacity='0.068'/%3E%3Crect x='276.0' y='155.3' width='88.0' height='49.5' stroke-opacity='0.061'/%3E%3Crect x='283.9' y='159.7' width='72.1' height='40.6' stroke-opacity='0.054'/%3E%3Crect x='290.4' y='163.4' width='59.1' height='33.3' stroke-opacity='0.048'/%3E%3C/g%3E%3C/svg%3E");
}

@keyframes bg-grid-warp {
  from { transform: scale(1); }
  to   { transform: scale(1.035); }
}

.reduce-motion #desktop.bg-grid::before {
  animation: none;
}

/* ---------- DESKTOP BACKGROUND: "ABYSS" (mise en abyme) ---------- */
/* nested frames shrinking toward the exact center of the screen, like a
   screen showing a photo of itself showing a photo of itself -- a single
   hand-authored SVG (not a repeating-gradient trick, since these are
   rectangles, not rings/lines) of concentric frames at a fixed shrink
   ratio, each one step fainter than the last, with the outermost frame
   flush against the viewBox edges so background-size: 100% 100% covers
   the full screen edge-to-edge rather than a centered box. currentColor
   picks up `color` set below so it still tracks var(--text) through the
   brightness slider/inversion like every other background here. Static
   by design. */
#desktop.bg-abyss::before {
  opacity: 1;
  color: var(--text);
  background-repeat: no-repeat;
  background-position: center center;
  background-size: 100% 100%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 300'%3E%3Cg fill='none' stroke='currentColor' stroke-width='2'%3E%3Crect x='1' y='1' width='398' height='298' stroke-opacity='0.32'/%3E%3Crect x='33' y='25' width='334' height='250' stroke-opacity='0.28'/%3E%3Crect x='61' y='46' width='278' height='208' stroke-opacity='0.24'/%3E%3Crect x='85' y='64' width='230' height='172' stroke-opacity='0.21'/%3E%3Crect x='105' y='79' width='190' height='142' stroke-opacity='0.18'/%3E%3Crect x='122' y='91.5' width='156' height='117' stroke-opacity='0.15'/%3E%3Crect x='136' y='102' width='128' height='96' stroke-opacity='0.12'/%3E%3Crect x='148' y='111' width='104' height='78' stroke-opacity='0.10'/%3E%3Crect x='158' y='118.5' width='84' height='63' stroke-opacity='0.08'/%3E%3Crect x='166' y='124.5' width='68' height='51' stroke-opacity='0.06'/%3E%3C/g%3E%3C/svg%3E");
}

/* ---------- DESKTOP BACKGROUND: "PHOTO" ---------- */
/* a real photo, not a generated pattern -- cover (not the 100% 100% used
   by the other backgrounds) so it crops to fill the screen instead of
   stretching/distorting a fixed-aspect image. */
#desktop.bg-photo::before {
  opacity: 1;
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
  /* set from JS (see pickDesktopPhoto() in script.js) -- a random pick from
     backgrounds/ each load, not one fixed file. Fallback here only covers
     the unlikely case JS hasn't run yet. */
  background-image: var(--bg-photo-url, none);
  /* the photograph exactly as it is -- never inverted with dark mode */
  filter: none;
}

#desktop::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 160px var(--vignette);
  z-index: 1;
}

/* the screen's own acknowledgment of a window hitting the edge of the
   viewport (see bumpWindowEdge() in script.js) -- a quick pulse of the
   desktop's existing edge vignette, deeper and tighter for an instant
   before it eases back. Deliberately NOT a full-screen flash or a bar
   across the top/bottom -- just the same inset edge shadow the desktop
   already has, briefly intensified. */
body.screen-bump #desktop::after {
  animation: screen-bump-flash 0.26s ease-out;
}

@keyframes screen-bump-flash {
  0%   { box-shadow: inset 0 0 160px var(--vignette); }
  30%  { box-shadow: inset 0 0 90px rgba(0,0,0,0.45); }
  100% { box-shadow: inset 0 0 160px var(--vignette); }
}

#desktop.hidden { display: none; }

/* the phone home screen (see the MOBILE FEED section near the end of this
   file) only ever exists below the mobile breakpoint -- hard off here so
   nothing about it can leak onto the desktop layout regardless of DOM
   order or z-index */
.mobile-feed { display: none; }

#desktop.power-on {
  animation: desktop-fade-in 0.3s ease-out;
}

@keyframes desktop-fade-in {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}

/* ---------- MENU BAR ---------- */

.menu-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 40px;
  /* glass, not solid chrome -- blurs the desktop icons scrolling by
     underneath it rather than fully hiding them. color-mix against
     var(--panel) (not a hardcoded white) so this stays correct at every
     point of the brightness slider -- including inverted/dark, where a
     literal rgba(255,255,255,...) would otherwise paint a pale bar
     straight across an otherwise dark screen */
  background: color-mix(in srgb, var(--panel) 60%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border-bottom: 1px solid var(--divider);
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 16px;
  font-size: calc(14px * var(--ui-scale));
  letter-spacing: 0.05em;
  z-index: 400;
}

/* .menu-bar sits BELOW #windows-layer (400 vs 405) so a dragged window can
   cover it -- but that also means every dropdown nested inside it (lang/
   sound/brightness/display-settings/the hover user-menu) is trapped in its
   parent's stacking context and rendered behind open windows too, even
   though the dropdown's own z-index (502) says it should win. Bumping
   .menu-bar itself above #windows-layer, but ONLY while one of its
   dropdowns is actually open, fixes that without changing the normal
   window-can-cover-the-bar behavior the rest of the time. */
.menu-bar:has(.dropdown-menu:not(.hover-dropdown):not(.hidden)),
.menu-bar:has(.menu-hover-wrap:hover) {
  z-index: 410;
}

/* the only thing docked to the true left edge of the bar -- .menu-left's
   own icon cluster is flex-end'd to the right (see its own comment below),
   so without this the left side of the bar just sits empty. margin-right:
   auto (not a second flex-end/space-between container) is what pushes it
   there while leaving .menu-left's existing layout untouched. Dimmed, not
   full --text, so it reads as ambient chrome rather than competing with
   the icons for attention. */
.menu-welcome {
  margin-right: auto;
  color: var(--text);
  font-weight: bold;
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.03em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* mirrored: row-reverse instead of reordering the DOM, so the icons keep
   their original left-to-right authoring order/dropdown logic, just
   flipped -- Log Out ends up rightmost (first thing hit scanning in from
   the right edge of the cluster), Display Settings/battery leftmost */
.menu-left {
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  gap: 8px;
}

/* ---------- SOCIAL MEDIA APP ----------
   Real accounts (see SOCIAL_ACCOUNTS in script.js), all three link-out
   cards for now -- see that comment for why none of them is a live
   embed yet. Window is sized to hug this content exactly (three cards
   plus the heading, no leftover scroll space) instead of the shared
   980px pane -- see the fixed height below, matched by hand to this
   content; revisit it if the heading or card count ever changes. */

.window[data-app="social"] {
  width: min(400px, 92vw);
  height: min(366px, 86vh);
}

.window[data-app="social"] .window-body {
  padding: 0;
  overflow: hidden;
  background: #ebebeb;
}

.social-app {
  height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.sm-heading {
  padding: 16px 16px 2px;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 15px;
  color: #161616;
  background: #ebebeb;
}

.sm-heading-emoji {
  padding: 0 16px 12px;
  display: flex;
  gap: 10px;
  font-size: 19px;
  line-height: 1;
  background: #ebebeb;
}

.sm-feed {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 0 8px;
}

.sm-post { background: #fff; }

/* ---- link-out cards (Facebook / Instagram / YouTube) ----
   All three real accounts, all link-out for now -- see the comment on
   SOCIAL_ACCOUNTS in script.js for why (Facebook is a personal profile,
   not embeddable; Instagram/YouTube need more than a static page can
   hold on its own). */
.sm-linkout-body {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 14px;
  text-decoration: none;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.sm-linkout-icon {
  width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.sm-linkout-text { min-width: 0; flex: 1; }
.sm-linkout-label { font-size: 14px; font-weight: 600; color: #050505; }
.sm-linkout-sub { font-size: 12.5px; color: #65676b; margin-top: 2px; }
.sm-linkout-open {
  flex-shrink: 0; display: flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 700; padding: 8px 12px; border-radius: 18px;
}
.sm-linkout-fb { background: #1877f2; color: #fff; }
.sm-linkout-ig { background: linear-gradient(135deg, #f9ce34, #ee2a7b, #6228d7); color: #fff; }
.sm-linkout-yt { background: #e02020; color: #fff; }
.sm-linkout-body:hover .sm-linkout-open { filter: brightness(1.08); }

.menu-icon-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(13px * var(--ui-scale));
  color: var(--text);
  background: transparent;
  border: 1.2px solid var(--text);
  border-radius: 0;
  padding: 0;
}

.menu-icon-btn:hover {
  background: var(--text);
  border-color: var(--text);
  color: var(--bg);
}

/* the luminosity toggle stays lit in its accent color for as long as dark
   mode is actually engaged (see setBrightness() in script.js toggling
   this class), not just for the moment it's actually being clicked --
   :active/:hover alone can't express "this is currently on" */
#brightness-toggle.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

#brightness-toggle.active:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.menu-battery {
  display: flex;
  align-items: center;
  color: var(--text);
  flex-shrink: 0;
  transition: color 0.3s ease;
}

.menu-battery.hidden {
  display: none;
}

/* only battery state ever borrows --danger outside an actual warning --
   same "reserve red for things that matter" rule as everywhere else on
   the site */
.menu-battery.low {
  color: var(--danger);
}

.menu-battery-fill {
  transition: width 0.4s ease;
}

.menu-battery-bolt.hidden {
  display: none;
}

.menu-lang-btn {
  width: auto;
  min-width: 24px;
  padding: 0 6px;
  font-family: var(--mono);
  font-size: calc(10.5px * var(--ui-scale));
  letter-spacing: 0.05em;
}

.menu-datetime {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  gap: 10px;
  padding: 6px 12px;
  background: var(--panel-2);
  border: 1px solid var(--border);
}

.menu-date {
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.05em;
  color: var(--text);
  cursor: var(--cur-arrow);
}

.menu-date:hover {
  color: var(--accent);
  text-decoration: underline;
}

.menu-clock {
  font-size: calc(14px * var(--ui-scale));
  font-weight: bold;
  color: var(--text);
  letter-spacing: 0.03em;
}

/* ---------- DROPDOWN MENUS (language / sound) ---------- */

.menu-dropdown-wrap {
  position: relative;
  display: flex;
}

.dropdown-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 150px;
  /* glass, like the window chrome -- a floating popup rather than a solid
     card, higher opacity than the window titlebar/sidebar since it can sit
     over busier content and its own text needs to stay legible. color-mix
     against var(--panel-2), see .menu-bar's comment for why not a
     hardcoded rgba */
  background: color-mix(in srgb, var(--panel-2) 75%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid var(--border);
  z-index: 502;
}

.dropdown-menu.hidden { display: none; }

.dropdown-item {
  padding: 9px 13px;
  font-size: calc(13px * var(--ui-scale));
}

.dropdown-item:hover {
  background: var(--accent);
  color: var(--on-accent);
}

.sound-dropdown {
  /* sizes to whatever its content actually needs at the current text scale
     (Settings > Size), instead of a fixed guess that overflows once labels
     grow at "Large" -- min-width keeps it from looking cramped at "Small" */
  width: max-content;
  min-width: 220px;
  max-width: 90vw;
  padding: 14px 13px 6px;
}

.sound-bank-row {
  border-bottom: none;
  padding: 14px 0 2px;
}

.dropdown-label {
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 8px;
}

#volume-slider {
  width: 100%;
  margin-bottom: 10px;
  accent-color: var(--text);
}

.settings-dropdown {
  /* same reasoning as .sound-dropdown -- grows with the current text scale
     instead of overflowing at "Large" */
  width: max-content;
  min-width: 300px;
  max-width: 90vw;
  padding: 6px 13px 10px;
}

.settings-dropdown .mobile-settings-row {
  padding: 12px 0;
}

.settings-dropdown input[type="range"] {
  accent-color: var(--text);
}

/* "Small"/"Medium"/"Large" are full words (unlike EN/ES chips elsewhere) —
   tighten padding and letting so all three fit on one line without
   spilling past the dropdown's edge */
.size-options {
  gap: 4px;
}
.size-options .mobile-chip {
  padding: 7px 8px;
  letter-spacing: 0.01em;
}

.sound-dropdown .dropdown-item {
  margin: 0 -13px;
}

/* ---- hover-triggered "logged in as" menu ---- */

.menu-hover-wrap {
  position: relative;
}

.hover-dropdown {
  display: none;
  /* flush against the button (no gap), with the visual breathing room moved
     INSIDE the box as padding instead -- a real position gap here is dead
     space the cursor has to cross without touching any element, which drops
     :hover on .menu-hover-wrap and closes the menu before a click can land */
  top: 100%;
  padding-top: 8px;
}

.menu-hover-wrap:hover .hover-dropdown {
  display: block;
}

.user-hover-label {
  padding: 10px 13px 2px;
  font-size: calc(10px * var(--ui-scale));
  letter-spacing: 0.08em;
  color: var(--dim);
}

.user-hover-name {
  padding: 0 13px 12px;
  font-size: calc(13px * var(--ui-scale));
  font-weight: bold;
  color: var(--text);
}

.mobile-settings-row .user-hover-name {
  padding: 0;
}

/* ---------- ICONS ---------- */

.icons {
  /* still normal document flow (so it contributes to #desktop's height and
     scrolls with it if it overflows) -- "relative" with no offset is
     visually identical to "static" for everything already in the grid, but
     it makes .icons the unambiguous containing block for any icon that
     goes position:absolute after being dragged (see .icon.free-positioned
     and makeIconDraggable() in script.js). Grid containers already act as
     that containing block for their own abspos children per spec even
     while static, but pinning it explicitly here means the CSS and the
     positioning math in script.js are never relying on that nuance to agree. */
  position: relative;
  margin: 66px 0 24px 26px;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content; /* without this, implicit "auto" columns
    stretch to fill the container's leftover width instead of hugging the
    icon — this is what made reflowed columns look absurdly far apart */
  justify-content: start;
  /* fixed desktop-folder behavior: fill a column top-to-bottom with 5
     icons, then start a new column — a simple, predictable rule rather
     than a dynamic per-viewport fit */
  grid-template-rows: repeat(5, auto);
  gap: 24px 28px;
  z-index: 5;
  perspective: 1400px; /* for the icon hover-tilt effect — see script.js */
}

.icon-trash {
  /* pinned to the corner on desktop instead of taking part in the
     scrolling column, so it stays reachable regardless of scroll --
     position:fixed lifts it out of .icons' grid flow entirely, so it can
     live in the same markup as every other icon (see the mobile reset
     below, where it rejoins the grid like any other icon) */
  position: fixed;
  top: 88px;
  right: 26px;
  left: auto;
}

/* a dragged icon (see makeIconDraggable() in script.js) -- higher
   specificity than .icon-trash's position:fixed above, so even the
   pinned-corner trash icon can be freely moved like any other */
.icon.free-positioned {
  position: absolute;
  top: 0;
  left: 0;
  right: auto;
  margin: 0;
}

.icon.dragging {
  z-index: 50;
  opacity: 0.85;
  transition: none; /* tracks the cursor 1:1 while held, no easing lag */
}

.icon {
  /* wide enough that every label (checked across en/es/fr/de, the longest
     being "ENSEIGNEMENT ET TECHNIQUE.EXE") wraps to at most 2 lines --
     otherwise one 3-4 line label made its whole grid row taller than the
     rest, throwing off vertical rhythm between columns */
  width: calc(130px * var(--icon-scale));
  background: transparent;
  border: 1px solid transparent;
  color: var(--text);
  font-family: var(--mono);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px 4px;
  border-radius: 0;
  transform-style: preserve-3d;
  transition: transform 0.12s ease, background 0.15s ease;
}

.icon:hover {
  background: transparent;
}

.icon.selected {
  background: transparent;
}

.icon:active {
  transform: scale(0.96) !important;
}

/* no square tile anymore -- just the glyph itself, a bit larger to
   compensate for the box that used to carry some of its visual weight,
   with a drop-shadow (which hugs the actual glyph shape, unlike a
   rectangular box-shadow) standing in for the "slightly 3D" lift the
   border used to imply */
.icon-glyph {
  width: calc(56px * var(--icon-scale));
  height: calc(56px * var(--icon-scale));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(24px * var(--ui-scale));
  color: var(--text);
  filter: drop-shadow(2px 4px 2px rgba(38,25,15,0.35));
  transition: filter 0.15s ease;
}

.icon-glyph svg {
  width: calc(32px * var(--icon-scale));
  height: calc(32px * var(--icon-scale));
}

/* real PNG app icons (ICONS/) carry their own color, so they read a bit
   larger than the hand-drawn line glyphs at the same visual weight */
.icon-glyph img {
  width: calc(40px * var(--icon-scale));
  height: calc(40px * var(--icon-scale));
  object-fit: contain;
}

.icon:hover .icon-glyph {
  filter: drop-shadow(3px 6px 3px rgba(38,25,15,0.45));
}

/* no more square to fill with the accent color -- the glyph itself tints
   instead, with a soft accent glow standing in for the old filled box */
.icon.selected .icon-glyph {
  color: var(--accent);
  filter: drop-shadow(0 0 7px color-mix(in srgb, var(--accent) 55%, transparent));
}

.icon-label {
  font-family: var(--font-label);
  font-size: calc(13px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.03em;
  color: var(--text);
  /* a solid-ish scrim behind the label -- the desktop background is now a
     real photo (see backgrounds/ + pickDesktopPhoto() in script.js), so
     the label needs its own contrast instead of relying on the photo
     being dark/light enough wherever it happens to land */
  background: color-mix(in srgb, var(--panel) 78%, transparent);
  border-radius: 0;
  padding: 2px 5px;
  width: 100%;
  text-align: center;
  overflow-wrap: anywhere;
  text-wrap: balance; /* spreads wrapped lines evenly instead of leaving a lone character on its own line -- some very wide decorative fonts still force one, this just minimizes it */
}

.icon.selected .icon-label {
  color: var(--on-accent);
  background: var(--accent);
}

/* every app icon is now a real PNG from ICONS/, which already carries its
   own color, so there's no more per-app currentColor hover tint -- the
   shared hover/selected drop-shadow lift (see .icon:hover .icon-glyph /
   .icon.selected .icon-glyph above) is the only hover feedback left. */

.icon-glyph svg {
  transition: color 0.15s ease;
}

/* the same per-app identity color echoed as a little folder-tab stripe on
   that app's own window titlebar (see .window-titlebar::before) -- one
   more small, specific decision instead of every window chrome reading
   identically */
.window[data-app="bio"] { --app-accent: #2f5aa8; }
.window[data-app="works"] { --app-accent: #d1382a; }
.window[data-app="software"] { --app-accent: #2f8f4f; }
.window[data-app="contact"] { --app-accent: #c23a7a; }
.window[data-app="calendar"] { --app-accent: #d19a1f; }
.window[data-app="sharing_and_support"] { --app-accent: #6a3fb0; }
.window[data-app="cv"] { --app-accent: #29406b; }
.window[data-app="zoom"] { --app-accent: #1f8fc2; }
.window[data-app="social"] { --app-accent: #e0741f; }
.window[data-app="trash"] { --app-accent: #a3402e; }

/* ---------- WINDOWS ---------- */

#windows-layer {
  position: fixed;
  inset: 0;
  /* above .menu-bar (400) and .taskbar (30) on purpose -- a dragged window
     can be pushed up past the menu bar (see makeDraggable() in script.js,
     which no longer floors it there) and needs to actually cover it, not
     disappear underneath it. Context menus/dropdowns/search (501+) and
     .focus-elevated (410, see below) still stack above this. */
  z-index: 405;
  pointer-events: none;
}

#windows-layer .window {
  pointer-events: auto;
}

/* the live preview of the half-screen a window will snap into (see
   applyWindowSnap() in script.js) -- shown while dragging a titlebar with
   the cursor pinned against the left/right edge of the screen, gone the
   instant the drag ends (snapped or not) */
.snap-preview {
  position: fixed;
  z-index: 1000;
  border: 2px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  pointer-events: none;
}

.snap-preview.hidden {
  display: none;
}

/* ---------- FOCUS MODE (works detail / video) ---------- */
/* a lightbox-style backdrop that lives inside #windows-layer, so its
   z-index is set relative to whichever window is currently "in focus mode"
   (see activateFocusMode() in script.js) -- everything visually behind it
   (desktop icons, other windows) goes dark and blurs. #windows-layer itself
   gets bumped even further above its own already-elevated base (405) for
   the duration via .focus-elevated, so the takeover is total -- this is
   meant to be the most important thing on screen, not a window among
   others. Context menus/dropdowns/search stay above even that (501+) so
   summoning one of those can still escape it if needed. */

#windows-layer.focus-elevated {
  z-index: 410;
}

#focus-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.92);
  backdrop-filter: blur(32px);
  -webkit-backdrop-filter: blur(32px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.4s ease;
}

#focus-backdrop.active {
  opacity: 1;
  pointer-events: auto;
}

.reduce-motion #focus-backdrop {
  transition: none;
}

/* clicking a work photo (gallery or inline) enlarges it here -- a single
   shared overlay reused for every photo, see openPhotoLightbox() */
#photo-lightbox {
  position: fixed;
  inset: 0;
  /* above #work-lecture-overlay (21000) too -- an enlarged photo needs to
     actually be visible when opened from inside lecture mode's own inline
     photos/carousel, not buried behind the reading overlay that triggered it */
  z-index: 22000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,0.88);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

#photo-lightbox.active {
  opacity: 1;
  pointer-events: auto;
}

#photo-lightbox img {
  max-width: 90vw;
  max-height: 90vh;
  object-fit: contain;
  cursor: var(--cur-arrow);
  box-shadow: 0 20px 60px rgba(20,13,7,0.55);
}

.window {
  position: absolute;
  width: min(980px, 90vw);
  height: min(700px, 82vh);
  /* transparent here on purpose -- .window-titlebar and .window-body each
     paint their own solid fill below (see their own rules) */
  background: transparent;
  /* a hairline, not a real border -- the per-app tint (see .window-body's
     comment) is meant to carry the "which window is this" job on its own,
     this is only a backstop for the case two windows land on tints too
     close to tell apart at a glance. Sub-pixel on purpose: 0.5px still
     renders as a crisp hair-thin line on a real (retina-class) display,
     rather than getting rounded up to a full, much more visible 1px. */
  border: 0.5px solid var(--divider);
  /* hard square corners, no rounding -- overflow: hidden below clips the
     titlebar/body children to match, so nothing needs its own radius */
  border-radius: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}


/* thin invisible strips glued to each edge/corner of a window (see
   makeResizable() in script.js) -- overlaid on top of the titlebar/body
   rather than adding any layout space of their own, the same way a real
   desktop OS's resize grab area works */
.window-resize-handle {
  position: absolute;
  z-index: 5;
}

.window.maximized .window-resize-handle { display: none; }

.window-resize-n, .window-resize-s {
  left: 6px;
  right: 6px;
  height: 6px;
  cursor: ns-resize;
}
.window-resize-n { top: -3px; }
.window-resize-s { bottom: -3px; }

.window-resize-e, .window-resize-w {
  top: 6px;
  bottom: 6px;
  width: 6px;
  cursor: ew-resize;
}
.window-resize-e { right: -3px; }
.window-resize-w { left: -3px; }

.window-resize-ne, .window-resize-nw, .window-resize-se, .window-resize-sw {
  width: 12px;
  height: 12px;
}
.window-resize-ne { top: -3px; right: -3px; cursor: nesw-resize; }
.window-resize-sw { bottom: -3px; left: -3px; cursor: nesw-resize; }
.window-resize-nw { top: -3px; left: -3px; cursor: nwse-resize; }
.window-resize-se { bottom: -3px; right: -3px; cursor: nwse-resize; }

.window.maximized {
  left: 0 !important;
  /* almost full screen -- goes OVER the top menu bar (top: 0, not 40px)
     rather than stopping just below it. The menu bar (z-index 400) still
     sits under #windows-layer (405, see its own comment on that), so it
     stays reachable the same way any other maximized window already
     covers it -- click through whatever's still visible of it, or drag/
     restore this window back down. Only the taskbar at the very bottom
     keeps its own reserved strip. */
  top: 0 !important;
  width: 100% !important;
  height: calc(100% - 46px) !important;
  /* flush against the screen edges, so a rounded corner here would just
     look like a bite taken out of the desktop rather than a soft window */
  border-radius: 0;
}

.window.minimized {
  display: none;
}

.window.opening {
  animation: window-open 0.18s ease-out;
}

@keyframes window-open {
  0%   { transform: scale(0.94); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

.window.closing {
  animation: window-close 0.14s ease-in forwards;
}

@keyframes window-close {
  0%   { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.94); opacity: 0; }
}


.window-titlebar {
  position: relative;
  height: 30px;
  /* solid chrome, not glass -- the window's whole frame (titlebar + body)
     is fully opaque now, nothing behind it shows through. Plain soft
     white/panel tone, same as every window -- the hairline border (see
     .window) is what tells overlapping windows apart now, not a per-app
     color wash. */
  background: var(--panel-2);
  /* no border here -- the boundary belongs on the content side instead
     (see .window-body's border-top) */
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 10px;
  user-select: none;
  flex-shrink: 0;
}

/* the focused window's titlebar darkens a step, so which window is
   actually active is obvious at a glance in a stack of same-toned,
   borderless windows -- brightness() darkens correctly in both light and
   dark mode without needing separate math for each direction */
.window.focused .window-titlebar {
  filter: brightness(0.9);
}

.window-title {
  font-family: var(--font-label);
  font-weight: bold;
  font-size: calc(13px * var(--ui-scale));
  letter-spacing: 0.04em;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 7px;
}

.window-title .dot {
  color: var(--app-accent, var(--accent));
}

.window-controls {
  display: flex;
  gap: 4px;
}

/* macOS/iOS puts window controls on the left; Windows (and the default
   for everything else) keeps them on the right, matching each OS's own
   convention — see detectOS() in script.js */
.os-mac .window-titlebar {
  flex-direction: row-reverse;
}

/* the DOM order is minimize, maximize, close (correct for Windows as-is) —
   macOS's own left-to-right order is close, minimize, maximize, so it needs
   its own reorder on top of the side-flip above, not just a mirrored one.
   back/forward (see below) sit outside this close/min/max cluster and
   always stay put at one end of it regardless. */
.os-mac .window-controls .btn-close { order: -1; }
.os-mac .window-controls .btn-min { order: 0; }
.os-mac .window-controls .btn-max { order: 1; }

/* real icon art (icons/close.png, minimize.png, maximize.png) now carries
   the color/shape entirely -- no more CSS-drawn glyph, tint, or bevel on
   top of it. Flat and solid on purpose (see the shared "less 3D" note on
   .work-back/.work-next above): a light brightness bump on hover is
   feedback enough for a button whose own artwork already has all the
   presence it needs. */
.window-controls button {
  width: 20px;
  height: 20px;
  border-radius: 0;
  border: none;
  background: none;
  padding: 0;
}
.window-controls button img {
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  /* a little pale, not full-strength straight off the source PNGs --
     slightly desaturated and lightened so the traffic-light colors sit
     back as quiet chrome instead of popping like a fresh icon-pack render */
  filter: saturate(0.66) brightness(1.1);
}
/* maximize.png ships blue, the same hue as minimize.png -- hue-rotated to
   green here so all three controls stay visually distinct (red/blue/green),
   without needing a separate green source asset */
.window-controls .btn-max img {
  filter: saturate(0.66) brightness(1.1) hue-rotate(-80deg);
}
.window-controls button:hover img {
  filter: saturate(0.66) brightness(1.22);
}
.window-controls .btn-max:hover img {
  filter: saturate(0.66) brightness(1.22) hue-rotate(-80deg);
}

.window-main {
  flex: 1;
  display: flex;
  min-height: 0;
}

.window-body {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 22px;
  font-size: calc(14px * var(--ui-scale));
  line-height: 1.6;
  /* the one fully opaque surface in the window -- text needs a solid
     background to stay readable, unlike the glass chrome around it. Back
     to a plain soft white/panel tone for every app -- the hairline border
     (see .window) is what separates a stack of overlapping windows now,
     not a per-app color wash. */
  background: var(--panel);
  /* see the comment on .window-titlebar for why the border lives here */
  border-top: 1px solid var(--divider);
}


/* the window's own acknowledgment of hitting the edge of the viewport
   while being dragged/resized (see bumpWindowEdge() in script.js) -- a
   quick squash-and-settle, like bumping into something solid, paired
   with the screen's own pulse (see body.screen-bump) and a short thud */
.window.edge-bump {
  animation: window-edge-bump 0.22s ease-out;
}

@keyframes window-edge-bump {
  0%   { transform: scale(1); }
  35%  { transform: scale(0.982); }
  65%  { transform: scale(1.006); }
  100% { transform: scale(1); }
}

.reduce-motion .window.edge-bump,
.reduce-motion body.screen-bump #desktop::after {
  animation: none;
}

/* ---------- TOAST NOTIFICATIONS ---------- */
/* the "little popup" a real app shows after a quiet background action
   (emptied trash, copied, saved) instead of the site just silently doing
   it -- stacks above the taskbar on desktop, above the status bar on
   mobile, and clears itself out after a few seconds */

#toast-container {
  position: fixed;
  left: 50%;
  bottom: 62px;
  transform: translateX(-50%);
  z-index: 900; /* above everything, including focus mode (410) and dropdowns (502) */
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: 8px;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--text);
  color: var(--bg);
  font-family: var(--mono);
  font-size: calc(13px * var(--ui-scale));
  letter-spacing: 0.02em;
  padding: 10px 18px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.4);
  white-space: nowrap;
  animation: toast-in 0.2s ease-out;
}

.toast.leaving {
  animation: toast-out 0.2s ease-in forwards;
}

.reduce-motion .toast,
.reduce-motion .toast.leaving {
  animation: none;
}

@keyframes toast-in {
  0%   { transform: translateY(10px); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

@keyframes toast-out {
  0%   { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(6px); opacity: 0; }
}

@media (max-width: 780px) {
  #toast-container {
    bottom: 24px;
    left: 16px;
    right: 16px;
    transform: none;
  }
  .toast {
    white-space: normal;
    justify-content: center;
    width: 100%;
  }
}

/* ---------- TASKBAR ---------- */

.taskbar {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  height: 46px;
  /* same glass treatment as .menu-bar -- see its comment */
  background: color-mix(in srgb, var(--panel) 60%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border-top: 1px solid var(--divider);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 14px;
  /* above #windows-layer.focus-elevated (410, see its comment) -- that
     backdrop is a near-opaque, full-viewport dimming layer for focus mode
     (WORKS.EXE detail view, video playback), and used to fully paint over
     the taskbar since this used to sit way below it (z-index: 30), making
     the whole bar seem to vanish for as long as focus mode was active.
     Still well below context menus/dropdowns/search/lightbox (501+), which
     should keep covering it when open. */
  z-index: 415;
}

.taskbar-items {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
}

.taskbar-searchbox {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  font-weight: bold;
  color: var(--text);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 0;
  padding: 6px 12px;
  width: 200px;
  flex-shrink: 0;
}

.taskbar-searchbox:hover {
  color: var(--text);
  border-color: var(--text);
}

.taskbar-item {
  font-family: var(--font-label);
  font-size: calc(13px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.02em;
  color: var(--text);
  border: 1px solid transparent;
  border-radius: 0;
  padding: 6px 12px;
  background: var(--panel-2);
  /* same shared "solid, slightly-3D" bevel as the window controls/WORKS
     nav pills -- an open window's own taskbar entry is a button too, so
     it gets the same button language as the rest of the desktop */
  box-shadow:
    inset 1px 1px 0 color-mix(in srgb, white 25%, transparent),
    inset -1px -1px 0 color-mix(in srgb, black 20%, transparent),
    0 2px 5px color-mix(in srgb, black 25%, transparent);
}

.taskbar-item:hover {
  box-shadow:
    inset 1px 1px 0 color-mix(in srgb, black 12%, transparent),
    inset -1px -1px 0 color-mix(in srgb, white 15%, transparent),
    0 1px 2px color-mix(in srgb, black 25%, transparent);
}

.taskbar-item.active {
  color: var(--on-accent);
  background: var(--text);
  border-color: var(--text);
}

/* ---------- APP CONTENT ---------- */

.app-content h2 {
  font-family: var(--font-title);
  font-weight: 400;
  margin: 4px 0 2px;
}

/* ---------- BIO.EXE: "TERMINAL" REDESIGN ----------
   Scoped entirely to .window[data-app="bio"] -- every other app window
   keeps the shared light chrome. BIO.EXE reads as its own imported
   program: a black PowerShell-styled console (own titlebar, own
   monospace font) with one deliberately foreign element superimposed on
   top -- a skeuomorphic Windows-95-style slider (real <input
   type="range">, just re-skinned) for the detail-level control, kept
   straight (not tilted) so it still reads as a real, usable widget
   rather than a decorative sticker. */

.window[data-app="bio"] {
  /* deliberately NOT var(--mono) anywhere in this stack -- BIO.EXE's
     terminal identity is its own thing, independent of whatever font the
     visitor picked in Settings > Font (which can be "sans", a proportional
     stack that would defeat the whole "this is a terminal" premise) */
  --bio-mono: "Cascadia Mono", "Cascadia Code", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --bio-bg: #0c0c0c;
  --bio-titlebar: #161616;
  --bio-text: #e6e6e6;
  --bio-dim: #6e6e6e;
  --bio-quote: #5a5a5a;
  --bio-border: #3a3a3a;
}

/* own dark chrome instead of the shared light titlebar -- the accent dot
   still comes from --app-accent (see the .window[data-app="bio"] rule
   near the top of this file) so it stays visually tied to its taskbar/
   icon color, just recolored for a dark bar. Cascadia Mono at a small,
   regular (not bold) weight instead of the shared sans-serif titlebar
   font -- a real terminal's title text reads small and plain, not like
   a bold OS chrome label */
.window[data-app="bio"] .window-titlebar {
  background: var(--bio-titlebar);
  font-family: var(--bio-mono);
}
.window[data-app="bio"] .window-title {
  color: var(--bio-dim);
  font-weight: normal;
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0;
}

/* a tighter, darker drop shadow -- no border, like every other window */
.window[data-app="bio"] {
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55);
}

/* the shared minimize/maximize/close icons ship in the traffic-light
   colors used everywhere else on this "OS" -- grayscaled here so BIO.EXE's
   own controls read as plain monochrome glyphs, the way a real terminal
   app's do, instead of colorful GUI chrome */
.window[data-app="bio"] .window-controls button img {
  filter: grayscale(1) brightness(1.6) contrast(0.75);
}

/* smaller and tighter than the shared 14px/1.6 body text -- a real
   terminal's default font size reads a good deal smaller and denser than
   a normal reading pane */
.window[data-app="bio"] .window-body {
  background: var(--bio-bg);
  color: var(--bio-text);
  font-family: var(--bio-mono);
  font-size: calc(12.5px * var(--ui-scale));
  line-height: 1.5;
}

/* the "PS C:\Users\agustin> cd .\bio" boot header -- static chrome, not
   translated (a shell prompt reads the same in every language) */
.bio-shell-header {
  padding: 2px 0 0;
  font-size: calc(11px * var(--ui-scale));
  line-height: 1.55;
}
.bio-shell-path { color: var(--bio-text); }
.bio-shell-dim { color: var(--bio-dim); }

/* a command line styled like the ones a real shell prints before its
   output -- dimmed so the real bio content (in full --bio-text
   brightness) reads as the important part, not the scaffolding around it */
.bio-shell-cmd {
  color: var(--bio-dim);
  font-size: calc(12px * var(--ui-scale));
  margin: 10px 0 4px;
}
.bio-shell-glyph { color: #4a6da8; }

.bio-shell-closing { margin-top: 10px; }

@keyframes bio-shell-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
.bio-shell-cursor {
  display: inline-block;
  width: 7px;
  height: 13px;
  background: var(--bio-text);
  margin-left: 4px;
  vertical-align: -2px;
  animation: bio-shell-blink 1.1s step-end infinite;
}

/* ---------- retro (Windows-95/Winamp-style) slider ----------
   Deliberately foreign against the terminal: its own bevels, its own
   drop shadow, its own Tahoma font -- reads as an imported, working
   control rather than something native to this "OS". The <input
   type="range"> underneath is real and fully functional (drag, click,
   arrow keys); only its skin is replaced. */

.bio-detail-row {
  margin: 4px 0 22px;
}

/* stretched to the window's full width (window-body's own padding is
   what keeps it from touching the edges) instead of shrink-wrapping its
   content -- makes the whole control, and everything in it, read as a
   bigger, more visible fixture of the window rather than a small
   incidental widget off to one side */
.bio-retro-slider {
  display: block;
  width: 100%;
  box-sizing: border-box;
  box-shadow: 2px 4px 12px rgba(0, 0, 0, 0.6);
  border: 1px solid #6b6b6b;
  background: #c0c0c0;
  padding: 14px 20px 16px;
  font-family: Tahoma, Geneva, "MS Sans Serif", sans-serif;
}

.bio-retro-slider-label {
  font-size: 13px;
  color: #1a1a1a;
  font-weight: bold;
  margin-bottom: 9px;
}

.bio-slider-track {
  display: flex;
  align-items: center;
  gap: 14px;
}

.bio-slider-endpoint {
  font-size: 12px;
  color: #3a3a3a;
  flex-shrink: 0;
}

/* the real range input, reskinned as a sunken Win95 track + a raised
   beveled handle -- WebKit/Blink and Gecko need separate pseudo-elements
   for both the track and the thumb, so every rule below is duplicated
   with its vendor prefix */
#bio-detail-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  width: auto;
  height: 12px;
  margin: 0;
  background: #8f8f8f;
  box-shadow: inset 1px 1px 0 #4d4d4d, inset -1px -1px 0 #ffffff;
  cursor: pointer;
}
#bio-detail-slider::-webkit-slider-runnable-track { -webkit-appearance: none; height: 12px; background: transparent; }
#bio-detail-slider::-moz-range-track { height: 12px; background: transparent; border: none; }
#bio-detail-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 24px;
  margin-top: -6px;
  background: #c0c0c0;
  box-shadow: inset 1px 1px 0 #ffffff, inset -1px -1px 0 #4d4d4d, 1px 1px 2px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}
#bio-detail-slider::-moz-range-thumb {
  width: 18px;
  height: 24px;
  border: none;
  border-radius: 0;
  background: #c0c0c0;
  box-shadow: inset 1px 1px 0 #ffffff, inset -1px -1px 0 #4d4d4d, 1px 1px 2px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}
#bio-detail-slider:focus-visible { outline: 1px dotted #000000; outline-offset: 2px; }

.bio-retro-flags {
  display: flex;
  align-items: center;
  gap: 18px;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid #a8a8a8;
  font-size: 12px;
  color: #3a3a3a;
}

.bio-word-count::before { content: "word_count: "; }
.bio-word-count:empty::before { content: ""; }

/* shown whenever the currently displayed variant isn't bio.txt verbatim
   (see the level !== data.originalLevel check in updateBioDetailDisplay)
   -- every other level's wording came out of generate_bio_variants.py,
   condensed or embellished, not hand-written */
.bio-ai-notice::before { content: "source: "; }
.bio-ai-notice.hidden { display: none; }

/* clearfix so the floated photo's height is contained inside the body */
.bio-body::after {
  content: "";
  display: table;
  clear: both;
}

.bio-portrait {
  border-radius: 0;
}

/* each detail level's own fixed photo (photos/bio/bio_N.jpg/png -- see
   findBioPhotoForLevel() in script.js), floated beside the text. Capped at
   39% of the body's width -- measured against BIO.EXE's own default open
   size (980x656 at a 1280x800 viewport, see openApp()), that's the
   largest width whose 457:640 aspect ratio still fits the body's visible
   height without the photo's own bottom edge getting cut off/needing a
   scroll. Only ever downsized from its own native size, never upscaled
   past it. */
.bio-portrait.has-photo {
  max-width: 39%;
  float: left;
  margin: 0 20px 12px 0;
  display: block;
}

.bio-portrait.has-photo img {
  display: block;
  width: 100%;
  height: auto;
  /* the 39%-width cap above was calibrated against BIO.EXE's default
     open size, where the resulting photo height already fits the window
     body without scrolling -- but a MAXIMIZED window is far wider, and
     39% of that width (at this portrait 457:640 ratio) computes a much
     taller photo than the screen actually has room for. This caps the
     photo's own height too, so it's whichever is smaller: 39% of the
     window's width, or a photo tall enough to need scrolling to see the
     rest of it. object-fit: contain keeps it undistorted either way. */
  max-height: 70vh;
  object-fit: contain;
  border: 1px solid var(--bio-border);
}

.bio-paragraphs p {
  margin: 0 0 12px;
  color: var(--bio-text);
}

/* a quiet quote-marker instead of a shouted color -- the "less bio.txt
   --warnings" command line right before the warning (see
   .bio-fiction-warning-row below) is what signals this is a warning,
   not a loud red block */
.bio-paragraphs p::before,
.bio-fiction-warning-text::before {
  content: "> ";
  color: var(--bio-quote);
}

.bio-fiction-warning-text {
  font-size: calc(12px * var(--ui-scale));
  color: var(--bio-text);
  margin: 0;
}

.bio-fiction-warning-row.hidden { display: none; }

/* empty rectangle standing in for a photo -- level 1 always gets this, and
   any other level whose own bio_N photo hasn't been added yet falls back
   to it too (see setBioPortraitPhoto() in script.js). Same floated spot,
   same size, as .has-photo's real photos -- level 1 isn't a separate
   centered "empty state" screen, just a normal level with nothing to
   show yet. Sized the same way .has-photo's real photos are (39% share,
   the largest that fits BIO.EXE's default-open height without being cut
   off) with their exact aspect ratio -- every bio_N photo is cropped to
   that same 457x640 -- so the empty box reads as "this photo is missing"
   rather than a different, unrelated shape. */
.bio-portrait.is-blank {
  width: min(364px, 39%);
  aspect-ratio: 457 / 640;
  flex-shrink: 0;
  float: left;
  margin: 0 20px 12px 0;
  /* always a plain white square regardless of OS theme (including
     BIO.EXE's own black one) -- an intentionally blank canvas, same
     reasoning as the CV paper staying white in dark mode */
  background: #fdfdfb;
  border: 1px solid var(--bio-border);
}

/* the shared text-rewrite effect (see .rewrite-word.rewriting below) pins
   its font to var(--mono) so the delete/retype effect stays legible in a
   fixed-width font -- but var(--mono) is also the site-wide Font setting,
   which can be set to a proportional stack ("sans"). Inside BIO.EXE that
   means the word being retyped would flip to a different, non-monospace
   font mid-animation while the rest of the paragraph stays in
   --bio-mono. Pinned to --bio-mono instead here, same reasoning as
   --bio-mono itself: this app's terminal identity shouldn't follow
   whatever font the rest of the site is set to. */
.window[data-app="bio"] .rewrite-word.rewriting {
  font-family: var(--bio-mono);
}

/* the animation's own cursor (a plain "_", see .rewrite-word.rewriting::after
   below) restyled to match BIO.EXE's other blinking cursors (the boot
   header prompt, the closing "drag the slider" line) -- a solid bar
   instead of an underscore character, so every cursor in this window
   reads as the same UI element */
.window[data-app="bio"] .rewrite-word.rewriting::after {
  content: "";
  display: inline-block;
  width: 7px;
  height: 0.85em;
  background: var(--bio-text);
  margin-left: 2px;
  vertical-align: -0.1em;
  animation: bio-shell-blink 1s step-end infinite;
}

/* ==========================================================================
   BIO ON A PHONE

   The phone does not have this desktop. It opens BIO.EXE bare -- one app,
   full bleed, no window and no taskbar (index.html#app=bio/3, see
   bareWorkBoot()) -- inside the touch app's own frame, and that is the only
   route into it a phone has. So everything here hangs off html.bare-work and
   the desktop's terminal is untouched by all of it: on a Windows-ish desktop
   a PowerShell prompt around a biography is the joke and the point; on a
   phone, alone on the screen with no desktop around it, it is a website
   dressed as a terminal for no reason anybody can see from inside.

   WHAT IS KEPT is the whole idea: levels of detail, each with its own
   portrait, chosen by hand with a slider, and the word count and the
   AI/fiction notices still on the page. WHAT GOES is the costume -- the
   shell header, the "> " quote markers, the blinking cursors, the typed-out
   reveal, the bevelled Windows-95 skin on the slider.

   ---- ALL THREE THINGS, ON THE FIRST SCREEN -------------------------------
   The first version of this put the photograph across the full width at its
   own 457:640, which is 546px on a 390px phone: the whole of the first
   screen was the picture, and the two things that explain it -- the CHOOSER
   and the TEXT -- were both below the fold. Opening the bio told you nothing
   about what the bio was.
   What this app is is three things at once (a level of detail, a piece of
   writing, and the portrait that goes with that level), so all three have to
   be on the screen at the same time or the connection between them is
   something you have to remember rather than something you can see.
   So the photograph is SMALL and beside the name, not above it -- a header
   row, the way every profile on a phone is built -- and the chooser and the
   first lines of the biography follow it on the same screen. The picture
   still changes with the level, which is the whole reason it is here; it no
   longer has to be the only thing you can see.

   ---- AREAS, NOT MARKUP ---------------------------------------------------
   The template's own sequence is header, chooser, whoami, [portrait,
   paragraphs], warning, closing -- the terminal's reading order, and not
   this one. Nothing is moved in the DOM (the desktop reads the same
   template): the app becomes a grid, .bio-body becomes display:contents so
   the portrait and the prose are its own children, and each block is placed
   by name.
   ========================================================================== */
/* the window body's terminal type is set on .window[data-app="bio"], so it
   takes a selector at least as specific to undo */
html.bare-work .window[data-app="bio"] .window-body,
html.bare-work .bio-app {
  background: #fff;
  color: #14110f;
  font-family: "Inter", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  padding: 0;
}
html.bare-work .bio-app {
  display: grid;
  /* ---- WHY THE PICTURE'S SIZE IS A LENGTH AND NOT A RATIO ----
     It was width:min(38vw,146px) + aspect-ratio, and the row it sat in came
     out 85px tall with the picture hanging 137px down over the chooser
     below. An aspect-ratio box in an `auto` grid row does not feed its
     derived height back into the row's size: the row wants the item's
     height, the height comes from the ratio, the ratio comes from the width,
     and the width comes from the column the row is being sized for. The
     browser will not close that loop, and resolves it by contributing
     nothing.
     So the width is a length and the height is that length times the
     photographs' own ratio (640/457 = 1.4004), worked out here rather than
     asked of the layout. Nothing circular left. */
  --biophoto: min(46vw, 178px);
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "photo"
    "choice"
    "warn"
    "text";
  /* or the four rows share the height of the app between them and the
     header row is as tall as a third of the screen */
  align-content: start;
  /* clear of the home indicator, and of the touch app's own moving strip
     across the foot of the frame (see .wv-bar in mobile-discover.html) */
  padding-bottom: calc(76px + env(safe-area-inset-bottom));
}
/* the terminal's own furniture, none of which is this app on a phone */
html.bare-work .bio-shell-header,
html.bare-work .bio-shell-cmd,
html.bare-work .bio-shell-closing,
html.bare-work .bio-shell-cursor { display: none !important; }
/* so the portrait and the prose become children of the grid above and can be
   placed in it. The clearfix goes with it: nothing floats here, and as a
   grid item it is an empty box taking a row. */
html.bare-work .bio-body { display: contents; }
html.bare-work .bio-body::after { display: none; }

/* ---- THE PICTURE ----
   Floated beside the text at 39% on the desktop, because there it is an
   illustration in a document. Here it is the profile's own photograph: small,
   top left, with the name beside it.
   AT THE PHOTOGRAPHS' OWN RATIO, 457:640 -- every bio_N is cropped to
   exactly that already (see .bio-portrait.is-blank), so nothing is cropped a
   second time and there is no object-position to guess at. 4:5 was tried for
   a few minutes and that is exactly what went wrong: 59px came off a
   full-length portrait, all of it from the top, and the picture was a man
   from the chin down.
   The same ratio also makes the blank at level 1 exactly as tall as the five
   photographs, so moving along the slider never moves the page under the
   thumb. */
html.bare-work .bio-portrait,
html.bare-work .bio-portrait.has-photo,
html.bare-work .bio-portrait.is-blank {
  grid-area: photo;
  float: none;
  justify-self: start;
  margin: 18px 0 0 20px;
  width: var(--biophoto);
  height: calc(var(--biophoto) * 1.4004);
  max-width: none;
  border: 0;
  border-radius: 3px;
  overflow: hidden;
  background: #ece9e2;
}
html.bare-work .bio-portrait img {
  width: 100%;
  height: 100%;
  max-height: none;
  /* the box is the photographs' own ratio to four decimal places, so cover
     crops nothing -- it is here for the one frame before a new level's
     picture has reported its size */
  object-fit: cover;
  border: 0;
  /* a new <img> is written on every level change (setBioPortraitPhoto), so
     this runs once per change with nothing to schedule. It is kept where the
     text's own animations were taken out, and the difference is what it is
     for: the picture is REPLACED by another picture, and a cut between two
     photographs of the same man reads as a glitch where a sixth of a second
     of cross-over reads as the same man, differently. */
  animation: bio-hero-in .3s ease both;
}
@keyframes bio-hero-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ---- THE CHOOSER, WHICH IS THE POINT OF THIS APP ------------------------
   So it is the loudest thing on the page after the name. The 95 skin comes
   off completely -- no bevel, no drop shadow, no Tahoma, no grey plate --
   and what replaces it is the same control with everything it never said
   written around it: the rung it is on, named, in 20px; the track filling
   as it goes; and six marks under it you can aim at. See buildBioChoice. */
html.bare-work .bio-detail-row {
  grid-area: choice;
  margin: 0;
  padding: 20px 0 0;
}
html.bare-work .bio-retro-slider {
  display: block;
  width: auto;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font-family: inherit;
}
html.bare-work .bio-retro-slider-label {
  margin: 0;
  padding: 0 20px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #8a8479;
}
/* WHERE YOU ARE, IN WORDS, AND HOW LONG IT IS -- the two things the slider
   could never say, on one line, because they are one fact: a level of detail
   IS a length. Baseline-aligned, so two very different sizes sit on the same
   line of the page rather than on two. */
html.bare-work .bio-nowrow {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 9px;
  margin: 2px 0 11px;
  padding: 0 20px;
}
html.bare-work .bio-now {
  margin: 0;
  padding: 0;
  font-size: 21px;
  font-weight: 800;
  letter-spacing: -.025em;
  line-height: 1.1;
}
/* set to be READ, not filed: it was 10.5px tracked capitals in the flags row
   and it is now the second half of the headline. The dot is what makes the
   pair one line instead of two things that happen to be adjacent -- and it
   goes when there is no count, which is level 1, where the whole point is
   that there is nothing to count. */
html.bare-work .bio-nowrow .bio-word-count {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
  text-transform: none;
  color: #8a8479;
  white-space: nowrap;
}
html.bare-work .bio-nowrow .bio-word-count::before { content: "\00b7\00a0"; }
html.bare-work .bio-nowrow .bio-word-count:empty::before { content: ""; }
/* ---- THREE THINGS IN ONE ROW THAT WRAPS ----
   The track takes the full width, the marks go under it at the same width,
   and the two endpoint words go under THOSE, one at each end. All four are
   children of this one box (see buildBioChoice) and it is `order` that puts
   them in that sequence -- the markup's own order is endpoint, input,
   endpoint, which is the desktop's single-line layout and is not this one.
   The track being full width is the whole reason: with CONDENSED and BAROQUE
   beside it the thumb travelled between them, and marks laid out across the
   column lined up with nothing. */
html.bare-work .bio-slider-track {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0;
  padding: 0 20px;
}
html.bare-work #bio-detail-slider { order: 1; }
html.bare-work .bio-scale { order: 2; }
html.bare-work .bio-slider-endpoint { order: 3; }
/* the second of the two words -- the only one that comes after the input in
   the markup -- is pushed to the far end of the line */
html.bare-work #bio-detail-slider ~ .bio-slider-endpoint { margin-left: auto; }
/* Condensed / Baroque: the two ends still named, quietly, because the
   DIRECTION is the thing a slider is for and the ends are what give it one */
html.bare-work .bio-slider-endpoint {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: #8a8479;
}
/* the real input, reskinned twice over -- every rule below has to be written
   once for WebKit and once for Gecko, which is why the 95 skin was this long
   too. 44px of height is the touch target; the visible track is 6 of it. */
html.bare-work #bio-detail-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 0 0 100%;
  width: 100%;
  height: 44px;
  margin: 0;
  background: transparent;
  box-shadow: none;
  cursor: pointer;
}
/* the fill is the chosen share of the axis, so "more detail" has a direction
   you can see and not only a thumb that has moved. --pct is written by
   markBioLevel. */
html.bare-work #bio-detail-slider::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(to right,
    #14110f 0 var(--pct, 0%), #e6e1d9 var(--pct, 0%) 100%);
}
html.bare-work #bio-detail-slider::-moz-range-track {
  height: 6px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(to right,
    #14110f 0 var(--pct, 0%), #e6e1d9 var(--pct, 0%) 100%);
}
/* -12 is (30 - 6) / 2: WebKit centres a thumb on the track by hand */
html.bare-work #bio-detail-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 30px;
  height: 30px;
  margin-top: -12px;
  border-radius: 50%;
  background: #14110f;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
  cursor: pointer;
}
html.bare-work #bio-detail-slider::-moz-range-thumb {
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: #14110f;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
  cursor: pointer;
}
/* ---- THE SIX MARKS ----
   The track is continuous to drag and discrete to aim at, which is what a
   stepped range input already is underneath -- this is only that fact, drawn.
   Inset by half a thumb on each side so the first mark sits under the thumb's
   CENTRE at its leftmost position, not under its left edge; each mark is
   then placed by percentage across what is left (see buildBioChoice) and
   pulled back over its own centre. The endpoint words are 10.5px and the
   track has 10px of gap either side of it, which is what the two margins
   below add up to. */
html.bare-work .bio-scale {
  position: relative;
  flex: 0 0 100%;
  height: 18px;
  margin: -7px 0 2px;
}
html.bare-work .bio-tick {
  position: absolute;
  top: 0;
  /* left is written per mark by buildBioChoice as calc(15px + (100% - 30px)
     * fraction): 15 is half the thumb, so the first mark sits under the
     thumb's CENTRE at its leftmost position and the last under its centre at
     its rightmost, rather than under their outer edges. Padding on the box
     cannot do this -- a percentage `left` resolves against the padding box,
     so padding does not inset an absolutely positioned child at all. */
  transform: translateX(-50%);
  /* 30 wide is a mark you can hit; 5 of it is the mark */
  width: 30px;
  height: 22px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
html.bare-work .bio-tick::before {
  content: "";
  display: block;
  width: 5px;
  height: 5px;
  margin: 4px auto 0;
  border-radius: 50%;
  background: #d6d0c6;
  transition: background .14s ease, transform .14s ease;
}
html.bare-work .bio-tick.on::before {
  background: #14110f;
  transform: scale(1.5);
}

/* ---- AND WHAT THE MACHINE HAD TO DO WITH IT ----
   The word count and the AI notice stay, because they are true and because
   the fiction at the top of the range is only honest if the page says where
   it came from. They lose the terminal's voice: "word_count: 78 words"
   becomes "78 words", "source: AI GENERATED CONTENT" becomes a pill. */
html.bare-work .bio-retro-flags {
  margin: 0;
  padding: 8px 20px 0;
  border-top: 0;
  gap: 8px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #8a8479;
}
html.bare-work .bio-word-count::before,
html.bare-work .bio-ai-notice::before { content: ""; }
html.bare-work .bio-ai-notice {
  padding: 4px 9px;
  border-radius: 999px;
  background: #f1eee8;
  color: #6d6862;
}

/* ---- THE TEXT ----
   Set to be read: a reading face at 16px, no quote markers, no mono -- and
   nothing moving. The typing, the fade that replaced it and the ongoing
   word-rewrite cycle are all gone on the phone; see NOTHING MOVES IN HERE in
   script.js for what each of them was and why none of them belongs to the
   writing. */
html.bare-work .bio-paragraphs { grid-area: text; padding: 16px 20px 0; }
html.bare-work .bio-paragraphs p {
  margin: 0 0 15px;
  color: #14110f;
  font-size: 16px;
  line-height: 1.62;
}
html.bare-work .bio-paragraphs p::before { content: ""; }

/* ---- AND THE WARNING, WHICH IS NOT A COMMAND ----
   It arrived as `less bio.txt --warnings` followed by capitals. The command
   line is gone with the rest of the shell, so the warning has to carry
   itself: its own block, in the one colour on this page that is not ink.
   AND IT SITS ABOVE THE TEXT. In the terminal it follows the biography,
   because that is the order a shell prints things in. Here the top of the
   range is five thousand words of deliberate fiction, so a warning after
   them is a warning nobody reaches: it goes with the CHOOSER, which is the
   thing that just asked for fiction, and is read before the first line of
   it. */
html.bare-work .bio-fiction-warning-row {
  grid-area: warn;
  margin: 12px 20px 0;
  padding: 12px 14px;
  border-radius: 12px;
  background: #fdeceb;
}
html.bare-work .bio-fiction-warning-text {
  margin: 0;
  color: #a3231b;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.45;
}
html.bare-work .bio-fiction-warning-text::before { content: ""; }

/* ---------- WORKS.EXE: "media library" redesign ----------
   A sidebar of curated topics beside the existing sortable list, instead
   of the list alone in the shared window chrome -- the file-glyph icons
   and the hover panel's "Get Info"-style properties/actions were already
   leaning this way; this commits to it so WORKS.EXE reads as its own
   program too, the same way BIO.EXE does. Scoped to
   .window[data-app="works"] so nothing else changes. */

.window[data-app="works"] {
  --works-sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* a softer, warmer-neutral palette than the site's own high-contrast
     --text/--border/--dim/--panel-2 -- a real media-library app reads
     gentler than the OS chrome around it, not identically stark */
  --works-bg: #ffffff;
  --works-titlebar: #fbfbf9;
  --works-text: #2a2a26;
  --works-dim: #8f8f86;
  --works-border: #e4e4de;
  --works-hover: rgba(0, 0, 0, 0.035);
}

/* pinned independently of var(--mono) (the site-wide Font setting), same
   reasoning as --bio-mono: this app's identity shouldn't follow whatever
   font the rest of the site happens to be set to */
.window[data-app="works"] .window-title,
.window[data-app="works"] .window-body {
  font-family: var(--works-sans);
}

.window[data-app="works"] .window-titlebar {
  background: var(--works-titlebar);
}

.works-app {
  display: flex;
  height: 100%;
  min-height: 0;
}

.window[data-app="works"] .window-body {
  padding: 0;
  overflow: hidden;
  background: var(--works-bg);
  color: var(--works-text);
}

.works-body {
  display: flex;
  width: 100%;
  height: 100%;
  min-height: 0;
}

.works-sidebar {
  width: 190px;
  flex-shrink: 0;
  background: color-mix(in srgb, var(--works-text) 3%, var(--works-bg));
  border-right: 1px solid var(--works-border);
  padding: 14px 0;
  overflow-y: auto;
}
.works-sidebar.hidden { display: none; }

.works-sidebar-heading {
  padding: 12px 16px 6px;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--works-dim);
}
.works-sidebar-heading:first-child { padding-top: 0; }

.works-sidebar-item {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 16px;
  border: none;
  background: none;
  font-family: inherit;
  font-size: calc(12px * var(--ui-scale));
  color: var(--works-text);
  text-align: left;
  cursor: pointer;
}
.works-sidebar-item svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--works-dim); }
.works-sidebar-item:hover { background: var(--works-hover); }
.works-sidebar-item.active {
  background: color-mix(in srgb, var(--app-accent) 12%, transparent);
  font-weight: bold;
}
.works-sidebar-item.active svg { color: var(--app-accent); }

/* ---- IN-WINDOW BACK / FORWARD (see navigateWindow() in script.js) ----
   two hand-drawn arrows before the title, only once the window has moved
   to another app; the one with nothing on its side is dimmed */
.window-nav { display: none; align-items: center; gap: 2px; margin-right: 4px; }
.window-nav.shown { display: flex; }
.window-nav button {
  width: 22px;
  height: 20px;
  padding: 0;
  border: none;
  background: none;
  color: #ffd400;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: transform 0.12s ease, color 0.12s ease;
}
.window-nav button:hover:not(:disabled) { color: #ff1d6c; transform: scale(1.15); }
.window-nav button:disabled { opacity: 0.28; cursor: default; }
.window-nav .nav-sketch { width: 16px; height: 16px; overflow: visible; pointer-events: none; }
@media (max-width: 780px) { .window-nav { display: none !important; } }

/* ---- APP NAVIGATION SIDE PANEL (TRASH / SOFTWARE / SHARING & SUPPORT) --
   see mountAppNav() in script.js. The body turns into a row: the rail on
   the left, the app's own content scrolling in .appnav-main beside it.
   Drawn in the desktop's own language rather than as a settings list: a
   dark violet rail that carries on from the black titlebar, each app's
   icon sitting on a small hand-drawn square (the same pink / orange / cream
   family as the desktop icons, each tilted a little differently -- the
   tilt is rolled per window, see mountAppNav), mono capitals in the warm
   cream of the desktop labels, and the app you are in picked out in pink
   with a hand-drawn underline. */
.window-body.has-appnav {
  display: flex;
  padding: 0;
  overflow: hidden;
}
.appnav {
  width: 172px;
  flex-shrink: 0;
  overflow-y: auto;
  scrollbar-width: none;
  padding: 18px 10px 18px 14px;
  background:
    radial-gradient(120% 60% at 0% 100%, rgba(255, 98, 0, 0.2), transparent 60%),
    #1a0a2a;
  color: #f4e8d8;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.appnav::-webkit-scrollbar { display: none; }
.appnav-heading {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px 12px;
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: #ff6200;
}
/* a short hand-drawn stroke after the heading */
.appnav-heading::after {
  content: "";
  flex: 1;
  height: 6px;
  background: no-repeat center / 100% 100% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 6' preserveAspectRatio='none'%3E%3Cpath d='M1 3.6Q12 1.8 24 3.2T46 2.9Q54 2.6 59 3.8' fill='none' stroke='%23ff6200' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  opacity: 0.7;
}
.appnav-item {
  --sq: var(--app-c-bio);
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 6px 4px;
  border: none;
  background: none;
  font-family: var(--mono);
  font-size: calc(11px * var(--ui-scale));
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1.15;
  text-transform: uppercase;
  color: rgba(244, 232, 216, 0.72);
  text-align: left;
  cursor: pointer;
  transition: color 0.15s ease;
}
.appnav-item[data-app="works"] { --sq: var(--app-c-works); }
.appnav-item[data-app="software"] { --sq: var(--app-c-software); }
.appnav-item[data-app="contact"] { --sq: var(--app-c-contact); }
.appnav-item[data-app="calendar"] { --sq: var(--app-c-calendar); }
.appnav-item[data-app="sharing_and_support"] { --sq: var(--app-c-sharing_and_support); }
.appnav-item[data-app="social"] { --sq: var(--app-c-social); }
.appnav-item[data-app="cv"] { --sq: var(--app-c-cv); }
.appnav-item[data-app="zoom"] { --sq: var(--app-c-zoom); }
.appnav-item[data-app="trash"] { --sq: var(--app-c-trash); }
.appnav-icon {
  position: relative;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform: rotate(var(--tilt, -3deg));
  transition: transform 0.18s cubic-bezier(.3, 1.6, .5, 1);
}
/* the hand-drawn square: a filled box with uneven corners, and a second
   outline over it at a slightly different angle */
.appnav-icon::before,
.appnav-icon::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 1.5px solid #0a0a0a;
  border-radius: var(--rad, 3px 6px 2px 5px / 5px 2px 6px 3px);
}
.appnav-icon::before { background: var(--sq); }
.appnav-icon::after {
  background: none;
  border-color: rgba(10, 10, 10, 0.55);
  transform: translate(1px, -1px) rotate(calc(var(--tilt, -3deg) * -1.4));
}
.appnav-icon img, .appnav-icon svg {
  position: relative;
  z-index: 1;
  width: 72%;
  height: 72%;
  object-fit: contain;
  display: block;
}
.appnav-label { position: relative; padding-bottom: 2px; }
.appnav-item:hover { color: #f4e8d8; }
.appnav-item:hover .appnav-icon { transform: rotate(0deg) translateY(-2px) scale(1.08); }
.appnav-item.active { color: #ff1d6c; cursor: default; }
.appnav-item.active .appnav-icon { transform: rotate(0deg) scale(1.08); }
/* the app you are in: underlined by hand */
.appnav-item.active .appnav-label::after {
  content: "";
  position: absolute;
  left: -2px;
  right: -4px;
  bottom: -4px;
  height: 6px;
  background: no-repeat center / 100% 100% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 6' preserveAspectRatio='none'%3E%3Cpath d='M1 4Q14 1.6 28 3.4T52 2.6Q56 2.4 59 3.2' fill='none' stroke='%23ff1d6c' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
}
.appnav-main {
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  padding: 22px;
}
/* no room for it on a narrow screen -- the app gets the whole width */
@media (max-width: 780px) {
  .appnav { display: none; }
}
/* SHARING & SUPPORT's poster fills its body edge to edge, no scroll */
.window[data-app="sharing_and_support"] .appnav-main {
  padding: 0;
  overflow: hidden;
  display: flex;
}
/* and with the panel taking 168px, each half is narrower: tighter margins,
   and a title allowed to come down further so a long word ("Enseignement")
   still fits its half instead of being clipped */
.window[data-app="sharing_and_support"] .appnav-main .sas-half { padding: 28px 24px; }
.window[data-app="sharing_and_support"] .appnav-main .sas-title { font-size: clamp(24px, 15cqw, 92px); }

.works-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  padding: 22px;
}

.window[data-app="works"] .works-toolbar,
.window[data-app="works"] .works-list-header,
.window[data-app="works"] .work-row {
  border-color: var(--works-border);
}
.window[data-app="works"] .work-row:hover { background: var(--works-hover); }
.window[data-app="works"] .work-row-instrumentation,
.window[data-app="works"] .work-row-duration,
.window[data-app="works"] .work-row-year,
.window[data-app="works"] .work-row-genre,
.window[data-app="works"] .work-sort-btn,
.window[data-app="works"] .work-card-year {
  color: var(--works-dim);
}
.window[data-app="works"] .work-sort-btn:hover,
.window[data-app="works"] .work-sort-btn.active {
  color: var(--works-text);
}
.window[data-app="works"] .work-thumb,
.window[data-app="works"] .works-view-btn {
  border-color: var(--works-border);
}
.window[data-app="works"] .works-view-btn.active {
  background: var(--works-text);
  border-color: var(--works-text);
}

/* the track-number column added to the list view (see renderWorksList()
   in script.js) -- a plain index, not a real track order, but it's the
   detail that reads "media library" rather than "file browser" */
.work-row-index {
  width: 18px;
  flex-shrink: 0;
  text-align: right;
  color: var(--works-dim);
  font-size: calc(11px * var(--ui-scale));
}

.work-genre-pill {
  display: inline-block;
  background: color-mix(in srgb, var(--works-text) 5%, var(--works-bg));
  color: var(--works-dim);
  border-radius: 10px;
  padding: 2px 9px;
  font-size: calc(11px * var(--ui-scale));
}


/* ---- text rewrite animation: a fixed-size box (width set inline by
   startWordCycle, sized to the longest alternate) so deleting and
   retyping characters never shifts surrounding text -- only the glyphs
   inside the box change, the box itself never moves or resizes ---- */

/* idle, a .rewrite-word is indistinguishable from plain text -- normal
   inline flow, no reserved space. display/width are set inline by JS only
   for the duration of an actual delete+retype transition (see
   rewriteToNextWord in script.js), so surrounding text never shifts
   without leaving a permanent gap the rest of the time */

/* pinned to the mono stack regardless of whatever font the page itself is
   set to (see setFont() in script.js) -- the delete/retype effect is a
   typewriter conceit, and reads as one clearly and consistently only in a
   fixed-width font; in a proportional one the per-character steps look
   uneven and the effect is easy to miss entirely */
.rewrite-word.rewriting {
  font-family: var(--mono);
}

.rewrite-word.rewriting::after {
  content: "_";
  margin-left: 1px;
  animation: blink 1s steps(1) infinite;
}

.reduce-motion .rewrite-word.rewriting::after {
  animation: none;
}

/* ---------- SOFTWARE.EXE: "apps window" ----------
   A launcher-style grid: a colored glyph tile, the app's name, a black
   platform tag and a one-line tagline -- nothing else. The longer
   description, the GitHub link, the ZIP download and the clone command
   live in a small dark hover card (see initSoftwareApp() in script.js),
   so the grid itself stays quiet. Fixed colors regardless of the desktop
   theme, like every other app.exe redesign. */

.software-repo-header {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--dim);
  font-size: calc(11.5px * var(--ui-scale));
  padding-bottom: 10px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.software-repo-header svg { width: 15px; height: 15px; }

.software-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 10px;
}

.sw-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: 22px 10px 18px;
  border: 1px solid transparent;
  border-radius: 14px;
  outline: none;
}
/* no highlight box round the cell -- the hover card (opened from the icon
   itself, see initSoftwareApp) is the only thing hovering shows. The
   z-index stays so that card sits above the neighbouring cells. */
.sw-cell:hover,
.sw-cell:has(:focus-visible),
.sw-cell.show-card {
  z-index: 30;
}

.sw-tile {
  color: inherit;
  text-decoration: none;
  width: 96px;
  height: 96px;
  border-radius: 22%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(26,26,26,0.15);
}
.sw-tile svg { width: 54px; height: 54px; }

.sw-cell-name {
  margin-top: 8px;
  font-size: calc(13px * var(--ui-scale));
  font-weight: bold;
}

.sw-tag {
  background: #161616;
  color: #fff;
  font-size: calc(10px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.06em;
  padding: 3px 8px;
  border-radius: 4px;
}

.sw-tagline {
  color: var(--dim);
  font-size: calc(11px * var(--ui-scale));
  line-height: 1.4;
  text-align: center;
  max-width: 180px;
}

/* faded, not display:none, so script.js can measure it before it shows */
.sw-hover {
  position: absolute;
  left: 50%;
  bottom: calc(100% - 6px);
  width: 250px;
  transform: translateX(calc(-50% + var(--sw-shift, 0px)));
  background: #161616;
  color: #fff;
  border-radius: 10px;
  padding: 11px 13px 12px;
  box-shadow: 0 6px 0 rgba(26,26,26,0.15);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.12s ease;
  text-align: left;
}
.sw-hover.below { bottom: auto; top: calc(100% - 6px); }
/* shown only from the icon itself, after a short rest on it -- see
   initSoftwareApp() in script.js -- not from anywhere in the cell */
.sw-cell.show-card .sw-hover,
.sw-cell:has(:focus-visible) .sw-hover {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.sw-hover-desc {
  margin: 0;
  font-size: calc(11px * var(--ui-scale));
  line-height: 1.55;
  color: #e6e6e6;
}

.sw-hover-lfs {
  margin-top: 7px;
  font-size: calc(10px * var(--ui-scale));
  line-height: 1.4;
  color: #ff8a7d;
}

.sw-hover-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 9px;
}

.sw-hover-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font: inherit;
  font-size: calc(10.5px * var(--ui-scale));
  font-weight: bold;
  white-space: nowrap;
  line-height: 1.2;
  color: #fff;
  background: transparent;
  border: 1px solid #fff;
  border-radius: 999px;
  padding: 4px 10px;
  text-decoration: none;
  cursor: pointer;
}
.sw-hover-btn.primary { background: var(--accent); border-color: var(--accent); }
.sw-hover-btn.icon { padding: 4px 7px; }
.sw-hover-btn:hover { text-decoration: underline; }

/* ---------- SHARING AND SUPPORT.EXE: "two halves" poster ----------
   Teaching (red) and Support (black), each a full-height half with its
   own big serif word, a small drawing, a short blurb and one button that
   opens CONTACT.EXE with a ready-made message (see
   openContactPrefilled() in script.js). Fixed colors, same reasoning as
   the other redesigned apps. */

.window[data-app="sharing_and_support"] .window-body {
  padding: 0;
  overflow: hidden;
  display: flex;
}

.sharing-app {
  flex: 1;
  min-width: 0;
  min-height: 0;
}

.sas-poster {
  display: flex;
  height: 100%;
  min-height: 0;
}

.sas-half {
  container-type: inline-size;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 32px 36px;
  color: #fff;
  overflow-y: auto;
}
.sas-half.teach { background: #b7362b; }
.sas-half.support { background: #161616; }

.sas-num {
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.14em;
  font-weight: bold;
  opacity: 0.85;
}

.sas-title {
  margin: 0 0 0 -3px;
  font-family: var(--editorial-serif);
  font-size: clamp(44px, 19cqw, 92px);
  font-weight: 700;
  line-height: 0.95;
}

.sas-art svg { width: 150px; height: auto; display: block; }

.sas-blurb {
  margin: 0;
  font-size: calc(12.5px * var(--ui-scale));
  line-height: 1.65;
}

.sas-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12px;
}

.sas-items {
  font-size: calc(11px * var(--ui-scale));
  opacity: 0.85;
}

.sas-cta {
  font: inherit;
  font-size: calc(12px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.06em;
  padding: 11px 22px;
  border-radius: 999px;
  border: 1.5px solid transparent;
  cursor: pointer;
}
.sas-half.teach .sas-cta { background: #fff; color: #b7362b; }
.sas-half.support .sas-cta { background: #b7362b; color: #fff; }
.sas-cta:hover { filter: brightness(0.93); }

/* ---------- CALENDAR.EXE: a wall of dates ----------
   Not a calendar -- nobody needs thirty-one empty squares to find three
   concerts. It is the dates themselves, set like a wall calendar's leaves:
   what is COMING first, as big solid tiles in the month's colour with
   everything on them (the piece, the place, who plays it) and a way to be
   told when it is near; then what HAS BEEN, year by year, as quieter tiles
   in the same colours. Fixed colours regardless of the desktop's own
   dark/light toggle, like every other app.exe: its identity is its own. */

.window[data-app="calendar"] {
  --cal-sans: "Helvetica Neue", Helvetica, -apple-system, "Segoe UI", Arial, sans-serif;
}
.window[data-app="calendar"] .window-body {
  padding: 0;
  overflow: hidden;
  font-family: var(--cal-sans);
}
.calendar-app { height: 100%; min-height: 0; background: #0a0a0a; color: #f6f4ee; }
.calx { height: 100%; overflow-y: auto; padding: 26px 26px 34px; box-sizing: border-box; }
.calx-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.calx-title { margin: 0; font-weight: 800; font-size: 38px; letter-spacing: -0.045em; line-height: 0.9;
  text-transform: uppercase; }
.calx-count { font: 700 10px/1 var(--mono); letter-spacing: 0.2em; text-transform: uppercase; color: #ff1d6c; }
.calx-section { margin: 28px 0 12px; display: flex; align-items: center; gap: 12px;
  font: 700 10px/1 var(--mono); letter-spacing: 0.2em; text-transform: uppercase; color: rgba(246,244,238,.6); }
.calx-section::after { content: ""; flex: 1; height: 1px; background: rgba(246,244,238,.15); }
.calx-year { margin: 26px 0 10px; font-weight: 800; font-size: 54px; letter-spacing: -0.05em; line-height: 0.8;
  color: transparent; -webkit-text-stroke: 1.3px rgba(246,244,238,.45); }

.calx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
.calx-grid.is-up { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }

/* a leaf: the date big, and under it what, where, who */
.calx-tile { position: relative; display: flex; flex-direction: column; min-height: 190px; padding: 14px 14px 16px;
  box-sizing: border-box; background: color-mix(in srgb, var(--mc) 22%, #0a0a0a); color: var(--mc);
  border: 0; text-align: left; font: inherit; }
.calx-tile.is-up { background: var(--mc); color: #0a0a0a; min-height: 280px; }
.calx-tile.is-link { cursor: pointer; }
.calx-tile.is-link:hover { outline: 2px solid var(--mc); outline-offset: 3px; }
.calx-tile.is-up.is-link:hover { outline-color: #f6f4ee; }
.calx-top { display: flex; justify-content: space-between; font: 700 9.5px/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase; }
.calx-soon { background: #0a0a0a; color: var(--mc); padding: 3px 6px; margin: -3px -2px 0 0; }
.calx-day { margin-top: auto; padding-top: 18px; font-weight: 800; font-size: 64px; letter-spacing: -0.06em; line-height: 0.8; }
.calx-tile.is-up .calx-day { font-size: 86px; }
.calx-day.is-long { font-size: 44px; }
.calx-tile.is-up .calx-day.is-long { font-size: 60px; }
.calx-piece { margin-top: 12px; font-weight: 800; font-size: 17px; letter-spacing: -0.03em; line-height: 1.02; text-transform: uppercase; }
.calx-tile.is-up .calx-piece { font-size: 21px; }
.calx-where { margin-top: 6px; font-size: 13px; line-height: 1.3; font-weight: 600; }
.calx-who { margin-top: 4px; font: 600 10px/1.45 var(--mono); letter-spacing: 0.04em; opacity: .75; }
.calx-go { position: absolute; right: 12px; bottom: 12px; font-weight: 800; font-size: 18px; }

/* LET ME KNOW lives on the leaf itself: one line of black at its foot,
   which opens into an email field in the same place */
.calx-lmk { margin-top: 14px; }
.calx-lmk [hidden] { display: none !important; }
.calx-lmk-btn { border: 0; background: #0a0a0a; color: var(--mc); padding: 9px 12px; cursor: pointer;
  font: 800 12px/1 var(--cal-sans); letter-spacing: 0.02em; text-transform: uppercase; }
.calx-lmk-btn:hover { background: #f6f4ee; color: #0a0a0a; }
.calx-lmk-form { display: flex; gap: 6px; flex-wrap: wrap; }
.calx-lmk-form input { flex: 1 1 130px; min-width: 0; border: 0; background: #f6f4ee; color: #0a0a0a;
  font: 600 13px/1 var(--cal-sans); padding: 9px 10px; outline: 2px solid #0a0a0a; outline-offset: -2px; }
.calx-lmk-form button { border: 0; background: #0a0a0a; color: var(--mc); padding: 9px 12px; cursor: pointer;
  font: 800 12px/1 var(--cal-sans); text-transform: uppercase; }
.calx-lmk-note { flex-basis: 100%; margin: 4px 0 0; font: 600 10px/1.4 var(--mono); }
.calx-lmk-note.is-bad { font-weight: 800; text-decoration: underline; }
.calx-lmk-done { font: 700 10.5px/1.4 var(--mono); }
.calx-none { padding: 18px; background: rgba(246,244,238,.06); font-size: 14px; color: rgba(246,244,238,.75); }

.works-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  margin-bottom: 12px;
}
/* a work's own page hides the toolbar (search and view toggle) -- see
   showWorkDetail(); without this the display:flex above won over .hidden
   and the search bar sat on top of every work's page */
.works-toolbar.hidden { display: none; }

.works-view-toggle {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}

/* the search field, left of the view toggle: a pill with a magnifier, in
   the list's own ink, that takes whatever width the toolbar has spare */
.works-search {
  flex: 1;
  max-width: 320px;
  margin-right: auto;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border: 1.5px solid color-mix(in srgb, var(--works-text) 30%, transparent);
  border-radius: 2px 3px 2px 3px;
  color: var(--works-dim);
  transition: border-color 0.15s ease, color 0.15s ease;
  cursor: text;
}
.works-search:focus-within {
  border-color: var(--app-accent, #ff5fa0);
  color: var(--app-accent, #ff5fa0);
}
.works-search svg { width: 15px; height: 15px; flex-shrink: 0; }
.works-search input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  color: var(--works-text);
}
.works-search input::placeholder { color: var(--works-dim); }
.works-search input::-webkit-search-cancel-button { cursor: pointer; }
.works-noresult {
  padding: 40px 8px;
  text-align: center;
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  color: var(--works-dim);
}

.works-view-toggle.hidden {
  display: none;
}

.works-view-btn {
  width: 27px;
  height: 25px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--dim);
}

.works-view-btn svg {
  width: 14px;
  height: 14px;
}

.works-view-btn:hover {
  border-color: var(--text);
  color: var(--text);
}

.works-view-btn.active {
  background: var(--text);
  border-color: var(--text);
  color: var(--on-accent);
}

.works-list {
  display: flex;
  flex-direction: column;
  container-type: inline-size;
}

.work-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 6px;
  border-bottom: 1px solid var(--border);
}

.work-row:hover {
  background: rgba(38,25,15,0.06);
}

/* a fixed-width name column (rather than flex-growing to fit each title)
   is what keeps the instrumentation column's left edge -- and every row's
   text within it -- landing at the same x position instead of drifting
   with each title's length */
/* ---- THE COLUMNS FIT WHAT IS IN THEM ----
   The name column was a fixed 260px, which on the default (620px) window
   left duration, year and genre about 4px each -- the genre pill was cut
   off. Now name, duration, year and genre are each exactly as wide as their
   widest cell, header included (measured after every render -- see
   fitWorksColumns() in script.js), so everything sits right after the
   title, and instrumentation takes whatever is left, cut with an ellipsis.
   Where there is not room for it at all it is dropped rather than squeezed
   to a sliver (the container query below). */
.work-row-name { display: flex; gap: 10px; align-items: center; flex: 0 1 var(--wc-name, 200px); min-width: 110px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.work-row-name .file-glyph { color: var(--accent); }
/* instrumentation/duration/year/genre all share the row's remaining width
   proportionally (4/1/1/1) instead of instrumentation grabbing everything
   up to the others' fixed widths -- that used to bunch duration/year/genre
   into a tight cluster flush against the right edge instead of spreading
   them out like real table columns */
.work-row-instrumentation {
  flex: 1 1 0;
  min-width: 0;
  color: var(--dim);
  font-size: calc(12px * var(--ui-scale));
  font-style: italic;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.work-row-duration { flex: 0 0 var(--wc-duration, 56px); min-width: 0; color: var(--dim); font-size: calc(12px * var(--ui-scale)); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.work-row-year { flex: 0 0 var(--wc-year, 48px); min-width: 0; color: var(--dim); font-size: calc(12px * var(--ui-scale)); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.work-row-genre { flex: 0 0 var(--wc-genre, 96px); min-width: 0; color: var(--dim); font-size: calc(12px * var(--ui-scale)); text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the file-explorer-style column header above the rows (list view only,
   see renderWorksList() in script.js) -- each cell is a button sharing the
   exact same flex-basis classes as the data cells below it, so labels and
   columns land in the same place. Clicking one sorts the list by it;
   clicking the already-active one flips direction (see the arrow glyph). */
@container (max-width: 560px) {
  .works-list .work-row-instrumentation { display: none; }
}
/* and narrower still, duration goes too, before the titles get cut */
@container (max-width: 430px) {
  .works-list .work-row-duration { display: none; }
}
.works-list-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 4px 6px 10px;
  border-bottom: 1px solid var(--border);
}

.work-sort-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font-family: inherit;
  font-size: calc(11px * var(--ui-scale));
  font-weight: bold;
  font-style: normal; /* overrides .work-row-instrumentation's italic, reused here just for column width */
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: left;
  color: var(--dim);
  display: flex;
  align-items: center;
  gap: 4px;
}

.work-sort-btn:hover,
.work-sort-btn.active {
  color: var(--text);
}

.work-sort-arrow {
  font-size: calc(9px * var(--ui-scale));
}

.work-thumb {
  width: 22px;
  height: 22px;
  object-fit: cover;
  border: 1px solid var(--border);
  flex-shrink: 0;
}

/* ---- gallery ("icon") view: a grid of file-icon-style tiles -- thumbnail,
   then the title/year underneath it like a real OS icon's label, never on
   top of the picture. auto-fill/minmax lets the count per row grow with
   the window instead of a fixed column number. */
.works-list.works-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
  gap: 22px 16px;
  align-items: start;
}

/* MUST come after .works-list.works-gallery above -- both have the same
   specificity (two classes each), so whichever is later in the file wins
   when an element carries both at once (gallery view, showing a work's
   detail page). Before this was reordered, .works-gallery's display:grid
   was winning that tie and the whole gallery grid stayed visible,
   overlapping the scrolling detail view underneath it. */
.works-list.hidden {
  display: none;
}

.work-card {
  position: relative;
  cursor: var(--cur-arrow);
}

.work-card-media {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.work-card:hover .work-card-media {
  border-color: var(--text);
}

.work-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.25s ease;
}

.work-card:hover .work-card-media img {
  transform: scale(1.04);
}

.work-card-media .file-glyph {
  font-size: calc(28px * var(--ui-scale));
  color: var(--dim);
}

/* the label lives below the tile, like a desktop icon's -- see .icon-label
   for the same idea at a smaller scale */
.work-card-title {
  display: block;
  margin-top: 8px;
  font-weight: bold;
  font-size: calc(13px * var(--ui-scale));
  line-height: 1.25;
  color: var(--text);
  text-align: center;
}

.work-card-year {
  display: block;
  color: var(--dim);
  font-size: calc(12px * var(--ui-scale));
  text-align: center;
}

/* ---- hover panel: a "Properties"-style flyout for the gallery/icon view --
   hovering a tile shows its properties and a few quick actions without
   leaving the grid (see bindWorkHoverPanels() in script.js). Positioned
   absolutely within the window (its nearest positioned ancestor), not
   fixed to the viewport, so it scrolls and stays anchored with the grid.
   Styled to match .context-menu (same glass alpha, sharp corners, same
   accent-highlight action rows) rather than a separate look of its own. */
.work-hover-panel {
  position: absolute;
  z-index: 20;
  width: 250px;
  background: color-mix(in srgb, var(--panel-2) 75%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid var(--border);
  box-shadow: 0 10px 30px rgba(0,0,0,0.35);
  padding: 10px 12px;
  pointer-events: auto;
}

.work-hover-panel.hidden {
  display: none;
}

.work-hover-title {
  font-weight: bold;
  font-size: calc(14px * var(--ui-scale));
}

.work-hover-label {
  margin: 6px 0 4px;
  font-size: calc(10px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
  border-top: 1px solid var(--divider);
  padding-top: 6px;
}

/* the longest field, given its own full-width line (wrapping naturally)
   instead of being squeezed into a label:value row next to the others,
   which is what made it read as cramped/truncated */
.work-hover-instrumentation {
  font-size: calc(12px * var(--ui-scale));
  font-style: italic;
  color: var(--dim);
  line-height: 1.35;
}

/* duration/year/genre are short, so they share one wrapping line as
   compact "label value" pairs instead of each getting a full row */
.work-hover-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-top: 6px;
  font-size: calc(12px * var(--ui-scale));
  color: var(--dim);
}

.work-hover-meta b {
  color: var(--text);
  font-weight: bold;
  margin-right: 3px;
}

.work-hover-actions {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--divider);
}

.work-hover-action {
  background: none;
  border: none;
  padding: 7px 6px;
  margin: 0;
  font-family: inherit;
  font-size: calc(13px * var(--ui-scale));
  text-align: left;
  color: var(--text);
}

.work-hover-action:hover {
  background: var(--accent);
  color: var(--on-accent);
}

/* ---- inline YouTube embed: a click-to-play facade over the thumbnail,
   swapped for the real <iframe> only once clicked (see loadYouTubeEmbed) ---- */

/* editorial layout: centered, full-width, sitting above the text --
   every theme shares this SAME layout now (copied from what was originally
   just theme-zine's own look), rather than each one floating the video to
   one side at its own size for the text to wrap around. Consistent
   placement across every work's page is worth more than each theme's own
   video-size quirk -- a visitor shouldn't have to re-learn where the video
   sits every time they open a different work. Per-theme styling (fonts,
   kicker treatment, color) is untouched -- only the video's own layout
   was unified. */
.work-video-embed {
  position: relative;
  width: 100%;
  max-width: 820px;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border: 1px solid var(--border);
  margin: 0 auto 20px;
  background: #000;
  cursor: var(--cur-arrow);
}

.work-video-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0.85;
}

.work-video-play {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #2fb35c;
  border: 1px solid rgba(255,255,255,0.85);
  border-radius: 50%;
  color: #fff;
  pointer-events: none;
  box-shadow: 0 2px 12px rgba(0,0,0,0.5);
}

.work-video-embed:hover .work-video-play {
  background: #37cc69;
}

.work-video-embed iframe {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
}

/* ---- THE FILM: our player over YouTube's (see THE FILM in script.js) ----
   A black sixteen-by-nine box. Inside it, the YouTube frame -- taller than
   the box and clipped to it, so its title card and logo fall outside --
   then the poster that veils it whenever it is not running, then a sheet
   that takes every click (the frame itself takes none), then our own
   controls. */
.film {
  position: absolute;
  inset: 0;
  background: #000;
  overflow: hidden;
  outline: none;
  color: #fff;
  font-family: var(--mono);
  user-select: none;
  -webkit-user-select: none;
}
.film-box { position: absolute; inset: 0; overflow: hidden; }
/* fullscreen is OURS too: the film's own box, letterboxed to the screen,
   with our controls on it -- never YouTube's fullscreen player */
.film.is-full { display: flex; align-items: center; justify-content: center; }
.film.is-full .film-box {
  position: relative;
  inset: auto;
  width: min(100vw, 177.78vh);
  height: min(56.25vw, 100vh);
}
.film-clip { position: absolute; inset: 0; overflow: hidden; }
/* the margin YouTube's furniture lives in: a third of the film's height
   above and below it, never less than 64px */
.film .film-yt {
  position: absolute;
  left: 0;
  width: 100%;
  top: calc(-1 * max(64px, 34%));
  height: calc(100% + 2 * max(64px, 34%));
  border: 0;
  display: block;
  pointer-events: none;
}
.film-veil {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  pointer-events: none;
  transition: opacity .28s ease;
}
.film-veil img { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter .25s ease; }
/* up at once, down gently: the veil has to beat the player's own pause
   furniture onto the screen, and it only ever lifts onto a running film */
.film.veiled .film-veil { opacity: 1; transition: none; }
.film.is-paused .film-veil img,
.film.needs-press .film-veil img { filter: brightness(.62); }
.film-hit { position: absolute; inset: 0; cursor: var(--cur-arrow); }
.film.is-playing:not(.show-bar) .film-hit { cursor: none; }

/* our spinner, for a film that is coming: before it starts, and over a
   stall long enough to see */
.film-spin {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  border-radius: 50%;
  border: 2px solid rgba(255,255,255,.22);
  border-top-color: #fff;
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
  animation: filmspin .9s linear infinite;
}
@keyframes filmspin { to { transform: rotate(360deg); } }
.film.is-waiting .film-spin,
.film.is-stalled .film-spin { opacity: 1; transition-delay: .25s; }

/* the big play: the same green disc as the facade the film was opened
   from, back whenever the film is stopped or waiting to be asked */
.film-big {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 62px;
  height: 62px;
  margin: -31px 0 0 -31px;
  padding: 0;
  border: 1px solid rgba(255,255,255,.85);
  border-radius: 50%;
  background: #2fb35c;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 16px rgba(0,0,0,.5);
  cursor: var(--cur-arrow);
  opacity: 0;
  transform: scale(.9);
  pointer-events: none;
  transition: opacity .18s ease, transform .18s ease, background .15s ease;
}
.film-big svg { width: 24px; height: 24px; fill: currentColor; margin-left: 2px; }
.film-big:hover { background: #37cc69; }
.film.is-paused .film-big,
.film.needs-press .film-big { opacity: 1; transform: none; pointer-events: auto; }

/* ---- the controls ---- */
.film-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 56px;
  padding: 0 12px 8px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 10px;
  background: linear-gradient(to bottom, transparent, rgba(0,0,0,.62));
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.film.show-bar .film-bar,
.film.is-paused .film-bar { opacity: 1; pointer-events: auto; }
/* nothing to scrub or pause before the film has started */
.film.is-waiting .film-bar,
.film.needs-press .film-bar { opacity: 0; pointer-events: none; }
.film-bar button {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: var(--cur-arrow);
  transition: background .15s ease, transform .12s ease;
}
.film-bar button:hover { background: rgba(255,255,255,.16); }
.film-bar button:active { transform: scale(.92); }
.film-bar button svg { width: 18px; height: 18px; fill: currentColor; display: block; }
.film-bar .film-pp { width: 38px; height: 38px; background: rgba(255,255,255,.94); color: #111; }
.film-bar .film-pp:hover { background: #fff; }
.film-bar .film-pp svg { width: 17px; height: 17px; }
.film-pp svg + svg, .film-snd svg + svg, .film-fs svg + svg { display: none; }
.film:not(.is-playing) .film-pp svg:first-child { display: none; }
.film:not(.is-playing) .film-pp svg + svg { display: block; }
.film.is-muted .film-snd svg:first-child { display: none; }
.film.is-muted .film-snd svg + svg { display: block; }
/* ---- VOLUME: a slider that slides out of the speaker on hover ---- */
.film-vol { display: flex; align-items: center; }
.film-volr {
  -webkit-appearance: none;
  appearance: none;
  width: 0;
  height: 4px;
  margin: 0;
  opacity: 0;
  border-radius: 2px;
  background: linear-gradient(90deg, #fff 0 var(--v, 100%), rgba(255,255,255,.3) var(--v, 100%) 100%);
  cursor: pointer;
  transition: width .18s ease, opacity .18s ease, margin .18s ease;
}
.film-vol:hover .film-volr,
.film-vol:focus-within .film-volr,
.film.is-full .film-volr { width: 74px; opacity: 1; margin: 0 8px 0 2px; }
.film-volr::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px; height: 12px; border-radius: 50%;
  background: #fff; border: none; box-shadow: 0 1px 3px rgba(0,0,0,.4);
}
.film-volr::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%;
  background: #fff; border: none; box-shadow: 0 1px 3px rgba(0,0,0,.4);
}
.film-volr::-moz-range-track { background: transparent; }
.film-volr:focus-visible { outline: 1px dotted #fff; outline-offset: 3px; }
.film.is-full .film-fs svg:first-child { display: none; }
.film.is-full .film-fs svg + svg { display: block; }
.film-t, .film-d { flex: 0 0 auto; min-width: 34px; text-shadow: 0 1px 3px rgba(0,0,0,.5); }
.film-d { text-align: right; opacity: .75; }
.film-track {
  position: relative;
  flex: 1 1 auto;
  height: 3px;
  border-radius: 2px;
  background: rgba(255,255,255,.22);
  cursor: var(--cur-arrow);
  transition: height .12s ease;
  touch-action: none;
}
/* a finger's height to aim at, not the line's three pixels */
.film-track::before { content: ""; position: absolute; left: -6px; right: -6px; top: -12px; bottom: -12px; }
.film-track:hover, .film.is-dragging .film-track { height: 5px; }
.film-load { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: 2px; background: rgba(255,255,255,.3); }
.film-fill {
  position: absolute;
  inset: 0;
  border-radius: 2px;
  background: #fff;
  transform-origin: 0 50%;
  transform: scaleX(0);
}
.film-knob {
  position: absolute;
  top: 50%;
  left: 0;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,.4);
  opacity: 0;
  transition: opacity .12s ease;
}
.film-track:hover .film-knob, .film.is-dragging .film-knob { opacity: 1; }
.film.is-full .film-bar { height: 72px; padding: 0 24px 16px; gap: 14px; font-size: 13px; }

.works-detail.hidden { display: none; }

/* WORKS.EXE is resizable independently of the browser window, so a plain
   media query (which only ever sees the viewport) can't tell when this
   container itself has gotten too narrow for the layouts that still key
   off it (the critics 2-column grid, field-guide's sidebar) -- the video
   itself no longer needs this now that it's always full-width regardless
   of container size (see .work-video-embed above). */
.works-detail {
  container-type: inline-size;
}

/* the base .work-back button-box -- still used verbatim (icon-less,
   solid pill) by the TRASH.EXE viewer's own "back" (see script.js), so
   this stays untouched. WORKS.EXE's own "back to WORKS" picked up
   icons/previous.png instead (reused -- "back" and "previous" are the
   same gesture) -- see .work-detail-nav .work-back below, which overrides
   this box away specifically for that one usage, without touching the
   shared base class TRASH.EXE still depends on. */
.work-back {
  font-family: var(--mono);
  background: var(--accent);
  color: var(--on-accent);
  cursor: pointer;
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  margin-bottom: 14px;
  display: inline-block;
  border: 1px solid var(--accent);
  padding: 6px 12px;
}

/* "back to WORKS" and "next work" anchor opposite ends of the row (same
   layout trick as .trash-viewer-toolbar's own back/nav pairing);
   "fullscreen lecture" sits between them, pushed there by auto margins on
   its own neighbors rather than a 3-way space-between (which would drift
   it off-center as back/next's own label lengths differ per language) */
.work-detail-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 14px;
}
.work-detail-nav .work-lecture-btn {
  margin: 0 auto;
}

.work-detail-nav .work-back {
  margin-bottom: 0; /* .work-back's own margin assumes it's the only thing on its line */
  background: none;
  border: none;
  padding: 0;
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--accent);
}
.work-detail-nav .work-back img {
  width: 20px;
  height: 20px;
}
.work-detail-nav .work-back:hover {
  color: color-mix(in srgb, var(--accent) 70%, black);
}

/* tinted with this work's own --work-accent (see .work-detail-kicker)
   instead of the site-wide --accent .work-back uses -- "back to WORKS"
   leaves this work entirely, "next work" stays inside its own identity
   for one more page. No button box (unlike .work-back) -- icons/next.png
   already reads as the control, a colored pill wrapped around it on top
   would just be double chrome. */
.work-next {
  font-family: var(--mono);
  background: none;
  color: var(--work-accent);
  cursor: pointer;
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  display: flex;
  align-items: center;
  gap: 7px;
  border: none;
  padding: 0;
}
.work-next img {
  width: 20px;
  height: 20px;
}
.work-next:hover {
  color: color-mix(in srgb, var(--work-accent) 70%, black);
}

/* neutral (not tinted with --work-accent like .work-next, not the site
   --accent like .work-back) -- a utility action, not a "go somewhere"
   nav link, so it reads as a distinct third kind of control rather than
   competing with either navigation color. No button box, same reasoning
   as .work-next -- icons/toggle_off.png (or toggle_on.png, see
   .work-lecture-close) already carries the "this is a control" cue. */
.work-lecture-btn {
  font-family: var(--mono);
  background: none;
  color: var(--dim);
  cursor: pointer;
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.04em;
  display: flex;
  align-items: center;
  gap: 7px;
  border: none;
  padding: 0;
  white-space: nowrap;
}
.work-lecture-btn img {
  width: 17px;
  height: 17px;
}
.work-lecture-btn:hover {
  color: var(--text);
}

/* a dedicated page for exactly the one work clicked (see showWorkDetail()
   in script.js) -- there's only ever one of these in #works-detail-view
   at a time, rendered fresh per work rather than one continuous feed of
   every work stacked together.
   --work-accent-h is set inline per section (see renderWorkSectionHtml())
   -- a hue hashed from the work's own id, not tied to WORKS.EXE's single
   app-wide accent, so every work's page picks up its own identity color
   instead of ten pages all sharing one. Moderate, fixed saturation/
   lightness so it reads reasonably against both the light and dark panel
   tone (this site snaps between the two rather than blending), same
   tradeoff --app-accent already makes elsewhere. */
.work-detail-page {
  position: relative;
  /* establishes a real stacking context, so ::before's z-index: -1 stays
     contained inside this element's own box (behind its content, above
     its own background) instead of being free to sink below unrelated
     content elsewhere in the window */
  z-index: 0;
  --work-accent: hsl(var(--work-accent-h, 0), 58%, 46%);
  /* WORKS.EXE's window-body already pins itself to the app's own fixed
     light palette (--works-bg/--works-text, see .window[data-app="works"]
     .window-body) instead of following the OS-wide brightness toggle --
     but every descendant rule below still reaches for the generic
     --text/--dim/--panel/--border/--accent tokens, which DO follow that
     toggle. In light OS mode the two palettes are close enough that this
     went unnoticed; in dark mode --panel/--text flip to near-black/
     near-white, so this whole page (background above included) briefly
     rendered dark text on a dark background. Re-pinned locally so every
     var(--text) etc. underneath resolves to the same fixed light tone
     regardless of OS brightness. */
  --panel: var(--works-bg);
  --text: var(--works-text);
  --dim: var(--works-dim);
  --border: var(--works-border);
  --accent: var(--app-accent);
  /* real margins, like an actual printed page -- without this, text ran
     edge-to-edge straight into the curl shadow bands below, reading as
     text spilling off the paper rather than a page with the curl just
     being its physical edge. background-image still spans the FULL
     padding box by default (background-origin: padding-box), so the curl
     stays anchored to the page's true edges even though content itself
     now sits well clear of them. */
  padding: 10px 44px 0;
  /* barely off the window's own panel tone -- close enough that the seam
     between the window chrome and the page itself feels like one smooth
     surface, not two visibly different materials meeting.
     WORKS.EXE keeps its own fixed light theme (--works-bg/--works-text)
     regardless of the OS-wide brightness toggle -- this used to pull
     --panel instead, which tracks that OS toggle, so switching to dark
     mode turned this page's background near-black while its text stayed
     the app's fixed dark tone, making every work unreadable. */
  background: color-mix(in srgb, var(--works-bg) 99%, #f2ead8 1%);
  padding-bottom: 0;
  margin-bottom: 0;
}

/* the video (see .work-video-embed) floats beside the description instead
   of stacking full-width above it -- a clearfix so the section's own
   height/border actually contains that float instead of collapsing around
   it (the classic side-effect of a floated child with no in-flow sibling
   tall enough to match it) */
.work-detail-page::after {
  content: "";
  display: table;
  clear: both;
}

/* a small colored section label above the title, the way a magazine
   feature opens under its section name ("PROFILE", "REVIEW"...) before
   the headline itself -- tinted with this work's own --work-accent */
.work-detail-kicker {
  color: var(--work-accent);
  font-family: var(--mono);
  font-weight: bold;
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.16em;
  text-transform: uppercase;
  margin: 4px 0 10px;
}

/* "more images" -- one small, discreet block at the very end of the page,
   not a section competing with the work itself: a quiet label plus a
   single-row, horizontally-scrollable strip (not a wrapping grid that
   could grow to however many rows the photo count needs). Whether a work
   has 4 photos or 12, this stays the same fixed height. Navigated with
   hover-only arrows (see .work-more-images-arrow below), not a visible
   scrollbar sitting there all the time -- cleaner, and the arrows still
   work as a discoverability cue since a bare overflow strip with no
   scrollbar at all would look like the photos just end there. */
.work-more-images {
  clear: both;
  margin: 22px 0;
}
.work-more-images-label {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 8px;
}
.work-more-images-viewport {
  position: relative;
}
/* hides the native scrollbar (Firefox via scrollbar-width, WebKit/Chromium
   via the pseudo-element below) on whichever strip lives inside this
   viewport -- .work-detail-gallery in window mode, .work-lecture-carousel
   in lecture mode, both also carry .work-scroll-strip for this and for
   the arrow buttons' own JS (see script.js's delegated click handler) */
.work-scroll-strip {
  scrollbar-width: none;
}
.work-scroll-strip::-webkit-scrollbar {
  display: none;
}
.work-more-images-arrow {
  position: absolute;
  top: 0;
  bottom: 4px;
  width: 32px;
  /* the photo thumbnails in the strip (.work-detail-thumb-wrap) are ALSO
     position:relative and come later in the DOM -- without an explicit
     z-index here, same-stacking-level positioned elements paint in DOM
     order, so the thumbnails would silently paint over these arrows
     (both had z-index:auto) even though the arrows are visually "on top"
     by intent */
  z-index: 1;
  border: none;
  background: linear-gradient(to right, var(--panel), color-mix(in srgb, var(--panel) 0%, transparent));
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  cursor: var(--cur-arrow);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.work-more-images-arrow-left {
  left: 0;
}
.work-more-images-arrow-right {
  right: 0;
  background: linear-gradient(to left, var(--panel), color-mix(in srgb, var(--panel) 0%, transparent));
}
/* the icon itself only ever points left (see WORKS_SCROLL_ARROW_ICON in
   script.js) -- mirrored on just the svg, not the whole button, so the
   button's own left/right-facing background gradient (above) doesn't
   flip along with it */
.work-more-images-arrow-right svg {
  transform: scaleX(-1);
}
.work-more-images-viewport:hover .work-more-images-arrow {
  opacity: 1;
}
/* set by updateMoreImagesArrows() (script.js) right after each strip is
   rendered -- no point in a "scroll for more" affordance when everything
   already fits without scrolling */
.work-more-images-viewport.no-overflow .work-more-images-arrow {
  display: none;
}
.work-detail-gallery {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  overflow-x: auto;
}

/* wraps each <img> (see galleryHtml in script.js) so themes that number
   their plates (.theme-zine/.theme-field-guide below) have a real element
   to hang a ::after "figure number" off of, positioned over the image */
.work-detail-thumb-wrap {
  position: relative;
  display: block;
  line-height: 0;
  flex-shrink: 0;
}

.work-detail-thumb {
  width: 64px;
  height: 64px;
  object-fit: cover;
  border: 1px solid var(--border);
  cursor: var(--cur-arrow);
  opacity: 0.75;
}

.work-detail-thumb:hover { opacity: 1; border-color: var(--accent); }

/* title/instrumentation sit as a masthead -- a large italic serif
   headline over its own dek, the way a fashion editorial's opening spread
   sets a feature's title -- then duration/year/performers drop to a
   small, wide-tracked "credits line" underneath, closed off with a thin
   rule before the video/text column pair starts */
.work-detail-title {
  font-family: var(--editorial-serif);
  font-style: italic;
  font-weight: 400;
  font-size: calc(40px * var(--ui-scale));
  line-height: 1.1;
  letter-spacing: 0.01em;
  margin: 0 0 4px;
}
.work-detail-instrumentation {
  font-family: var(--editorial-serif);
  font-style: italic;
  color: var(--dim);
  margin: 0 0 10px;
  font-size: calc(16px * var(--ui-scale));
}
.work-detail-duration,
.work-detail-year,
.work-detail-performers {
  display: inline-block;
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: calc(10.5px * var(--ui-scale));
  margin: 0 14px 0 0;
}
.work-detail-performers {
  display: block;
  margin: 6px 0 0;
}

/* closes off the masthead (title/instrumentation/duration/year/performers)
   before the video/text column pair starts -- always rendered (unlike the
   fields above, which only show up when that work actually has the data),
   so every work's page gets this same tinted seam regardless of which
   meta fields it happens to have. Tinted with --work-accent instead of
   the plain --divider every other rule on the site uses, so it doubles
   as this work's own colored identity mark. */
.work-detail-rule {
  height: 2px;
  background: var(--work-accent);
  opacity: 0.55;
  margin: 10px 0 14px;
  position: relative;
  z-index: 1;
}

.work-detail-concept {
  font-family: var(--editorial-serif);
  font-size: calc(16px * var(--ui-scale));
  line-height: 1.7;
  margin-bottom: 16px;
}

/* a proper editorial drop cap on the opening paragraph only -- the touch
   that most reads as "magazine feature" rather than "program note".
   Tinted with this work's own --work-accent instead of plain --text, one
   more echo of its identity color alongside the kicker and the rule above. */
.work-detail-concept-first::first-letter {
  float: left;
  font-size: 3.6em;
  line-height: 0.82;
  padding-right: 8px;
  font-style: normal;
  color: var(--work-accent);
}

/* base pull-quote look (a fallback -- every real work carries a theme
   class below that overrides this with its own identity; this only shows
   up for a future work that hasn't been assigned one yet) */
.work-detail-quote {
  margin: 18px 0;
  padding: 4px 0 4px 20px;
  border-left: 3px solid var(--work-accent);
  font-family: var(--editorial-serif);
  font-style: italic;
  font-size: calc(20px * var(--ui-scale));
  line-height: 1.4;
  color: var(--text);
}

/* a wide container (no floated video crowding it) sets the description in
   two magazine-style columns instead of one full-width block -- narrower
   containers, or a work WITH a video (already occupying its own float
   column), keep the single column: real CSS multi-column and a float
   fight each other, so this only ever applies where there's nothing to
   float against. */
@container (min-width: 640px) {
  .work-detail-page:not(:has(.work-video-embed)) .work-detail-description {
    column-count: 2;
    column-gap: 36px;
  }
  /* the drop cap's float only ever makes sense pinned to the first
     column's own top-left corner -- letting it happen in an arbitrary
     later reading position would look like a layout bug, not a feature */
  .work-detail-page:not(:has(.work-video-embed)) .work-detail-concept-first {
    break-inside: avoid;
  }
}

.work-links {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.work-link {
  display: flex;
  gap: 10px;
  font-size: calc(13px * var(--ui-scale));
}

.work-link-key {
  width: 92px;
  color: var(--accent);
  flex-shrink: 0;
}

.work-link a {
  color: var(--text);
  border-bottom: 1px solid var(--dim);
  text-decoration: none;
}

/* see .sw-name's matching ::after -- same "this goes somewhere" cue */
.work-link a::after {
  content: "↗";
  margin-left: 4px;
  font-size: 0.85em;
  color: var(--dim);
}

.work-link a:hover { color: var(--accent); border-color: var(--accent); }
.work-link a:hover::after { color: var(--accent); }

/* "what the critics say" -- but nobody actually reviewed these pieces, so
   every instance of this block carries this same visible label (see
   getWorkCriticsHtml() in script.js) regardless of theme. Sits right
   after the description, like a program note's critical-reception
   sidebar, before the closing photo gallery. */
/* a tinted "card" (not just a divider + plain text) -- the AI-GENERATED
   label needs to survive a quick skim, not just a close read, so the
   whole block gets its own visual weight (background wash, colored edge,
   a genuinely large decorative quote mark) instead of blending into the
   surrounding body copy the way a thin border-top let it. */
.work-critics {
  clear: both;
  margin: 30px 0;
  padding: 20px 22px;
  background: color-mix(in srgb, var(--work-accent) 7%, var(--panel));
  border-left: 3px solid var(--work-accent);
}
.work-critics-label {
  font-family: var(--mono);
  font-weight: bold;
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--work-accent);
  margin-bottom: 16px;
}
.work-critics-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
}
@container (min-width: 640px) {
  .work-critics-list {
    grid-template-columns: 1fr 1fr;
    column-gap: 28px;
  }
}
.work-critic {
  position: relative;
}
/* a real oversized quote mark, not just quotation punctuation inline --
   the kind of decorative flourish an actual "critics say" pull-quote
   sidebar uses to read as a distinct genre of text at a glance */
.work-critic-mark {
  position: absolute;
  top: -14px;
  left: -6px;
  font-family: Georgia, serif;
  font-size: 52px;
  line-height: 1;
  color: var(--work-accent);
  opacity: 0.35;
  pointer-events: none;
  user-select: none;
}
.work-critic-quote {
  margin: 0 0 10px;
  padding-left: 22px;
  font-family: var(--editorial-serif);
  font-style: italic;
  font-size: calc(15px * var(--ui-scale));
  line-height: 1.55;
  color: var(--text);
}
.work-critic-attr {
  display: block;
  padding-left: 22px;
  font-family: var(--mono);
  font-style: normal;
  font-size: 11px;
  color: var(--dim);
}
/* styled like a hyperlink (a real newspaper's own citation would be one)
   but a plain <span>, not an <a> -- the publication is invented, so
   making it look clickable without anywhere real to go would be its own
   small deception on top of the quote itself */
.work-critic-pub {
  color: var(--work-accent);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--work-accent) 40%, transparent);
}

/* one line: the (deliberately dulled -- see .work-share-icon below)
   platform icon row, left-aligned, immediately followed by the comedic
   message -- rendered twice per work (see getWorkShareHtml() in
   script.js): once right after the lead video, once again at the very
   end. clear: both since the video floats beside the text column on
   every theme but zine -- without it this would sit squeezed into
   whatever sliver of width is left beside the float. No background
   highlight, no send icon -- the icon row itself is the only visual
   marker, kept small/plain (first pass at this part). */
.work-share {
  clear: both;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 12px;
  margin: 18px 0;
}
.work-share-message {
  font-style: italic;
  font-weight: 600;
  font-size: calc(13px * var(--ui-scale));
  color: var(--accent);
  line-height: 1.4;
}
.work-share-icons {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
/* real brand icons, but muted -- grayscale + reduced opacity so they
   read as quiet chrome, not a colorful ad unit competing with the work.
   Full color only on hover, as the "this is actually clickable" cue. */
.work-share-icon {
  display: block;
  background: none;
  border: none;
  padding: 0;
  cursor: var(--cur-arrow);
  filter: grayscale(0.85);
  opacity: 0.55;
}
.work-share-icon:hover {
  filter: none;
  opacity: 1;
}
.work-share-icon img {
  width: 20px;
  height: 20px;
  display: block;
}

/* ---------- WORKS editorial identities (see WORK_THEMES in script.js) ----------
   four hand-designed looks, one per real work, each pulling in its own
   downloaded webfont (editorial-fonts.css) rather than sharing the site's
   single --editorial-serif fallback stack. Past typography/meta/quote/
   gallery treatment, several now genuinely rearrange WHERE elements sit
   too (zine's full-bleed top video, field-guide's sidebar), using flex/
   grid `order`/`grid-column` on .work-detail-page's own direct children
   -- every one of nav/kicker/title/instrumentation/duration/year/
   performers/rule/video/links/description/critics/gallery already is one,
   so this needed no new wrapper elements in script.js, just reassigning
   where each existing one lands. .work-detail-page's own box model
   (padding, background) is untouched either way, so all four still sit
   correctly inside window-body regardless of window size. */

/* == zine (Prompting Games) -- AI/content-generation, tech-poster/terminal ==
   the video's own centered full-width layout is the base .work-video-embed
   rule now (every theme shares it) -- this used to be zine's own
   exception, everything else floated a smaller video beside the text.
   Sits in its own normal document position either way -- after the title/
   meta/rule, same as every other theme, not moved ahead of the title. An
   earlier version of this reordered it to a full-bleed hero above
   everything but the nav; reads as more dramatic, but the title is the
   actual headline and needs to stay the first thing on the page, not
   something you scroll past a photo to reach. */
.theme-zine {
  --work-accent: hsl(var(--work-accent-h, 0), 85%, 40%);
}
.theme-zine .work-detail-kicker {
  display: inline-block;
  background: var(--text);
  color: var(--panel);
  padding: 3px 8px;
}
.theme-zine .work-detail-title {
  font-family: "Archivo Black", var(--editorial-serif);
  font-style: normal;
  font-weight: 400;
  text-transform: uppercase;
  font-size: calc(44px * var(--ui-scale));
  /* Archivo Black's caps-height runs tall relative to its own metrics --
     anything under ~1.05 here clips the tops of capital letters against
     the line box instead of just tightening the visual gap between
     wrapped lines */
  line-height: 1.05;
  letter-spacing: -0.01em;
}
.theme-zine .work-detail-instrumentation {
  font-family: "Space Mono", var(--mono);
  font-style: normal;
  color: var(--dim);
}
.theme-zine .work-detail-instrumentation::before {
  content: "// ";
  color: var(--work-accent);
}
.theme-zine .work-detail-duration,
.theme-zine .work-detail-year,
.theme-zine .work-detail-performers {
  font-family: "Space Mono", var(--mono);
}
.theme-zine .work-detail-rule {
  height: 5px;
  opacity: 1;
  background: var(--text);
}
.theme-zine .work-detail-concept {
  font-family: "Space Mono", var(--mono);
  font-size: calc(13px * var(--ui-scale));
  line-height: 1.65;
}
.theme-zine .work-detail-concept-first::first-letter {
  font-family: "Archivo Black", var(--editorial-serif);
  background: var(--work-accent);
  color: var(--panel);
  padding: 4px 9px;
  /* margin, not more padding -- padding would just grow the colored stamp
     itself, so the text would still butt straight up against its edge.
     This is real whitespace OUTSIDE the stamp before the text starts. */
  margin-right: 10px;
  font-size: 2.2em;
  line-height: 1.3;
  float: left;
}
.theme-zine .work-detail-quote {
  font-family: "Space Mono", var(--mono);
  font-style: normal;
  border: none;
  background: var(--text);
  color: var(--panel);
  padding: 16px 18px;
  margin: 22px 0;
  font-size: calc(15.5px * var(--ui-scale));
  line-height: 1.55;
}
.theme-zine .work-detail-quote::before {
  content: "> ";
  color: var(--work-accent);
}
.theme-zine .work-critics-label,
.theme-zine .work-critic-attr {
  font-family: "Space Mono", var(--mono);
}
.theme-zine .work-critic-quote {
  font-family: "Space Mono", var(--mono);
  font-style: normal;
  font-size: calc(13px * var(--ui-scale));
}
.theme-zine .work-detail-gallery {
  gap: 3px;
}
.theme-zine .work-detail-thumb-wrap::after {
  content: attr(data-i);
  position: absolute;
  bottom: 3px;
  right: 4px;
  font-family: "Space Mono", var(--mono);
  font-size: 10px;
  color: #fff;
  background: rgba(0,0,0,0.65);
  padding: 1px 4px;
  pointer-events: none;
}
.theme-zine .work-detail-thumb {
  width: 80px;
  height: 80px;
  border: none;
  opacity: 1;
}

/* == search (/search?q=love) -- internet vernacular, hyperconnectivity/love ==
   the video used to shrink to a "result card" thumbnail here, floated
   right -- dropped in favor of the same centered full-width layout every
   other theme now uses (see the base .work-video-embed rule); this
   theme's own identity still comes through in its type/kicker/color below. */
.theme-search .work-detail-kicker {
  font-family: "Inter", sans-serif;
  font-weight: 600;
}
.theme-search .work-detail-title {
  font-family: "Inter", sans-serif;
  font-style: normal;
  font-weight: 800;
  font-size: calc(30px * var(--ui-scale));
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 9px 22px;
  display: inline-block;
  background: color-mix(in srgb, var(--panel) 96%, var(--text) 4%);
}
.theme-search .work-detail-instrumentation,
.theme-search .work-detail-duration,
.theme-search .work-detail-year,
.theme-search .work-detail-performers {
  font-family: "Inter", sans-serif;
  font-style: normal;
  font-weight: 400;
}
.theme-search .work-detail-instrumentation {
  margin-top: 12px;
}
.theme-search .work-detail-rule {
  opacity: 0.22;
}
.theme-search .work-detail-concept {
  font-family: "Inter", sans-serif;
  font-size: calc(15px * var(--ui-scale));
  line-height: 1.75;
}
.theme-search .work-detail-concept-first::first-letter {
  font-family: "Inter", sans-serif;
  font-weight: 800;
  font-style: normal;
}
.theme-search .work-detail-quote {
  font-family: "Fraunces", var(--editorial-serif);
  font-style: italic;
  font-weight: 400;
  border: none;
  text-align: center;
  font-size: calc(25px * var(--ui-scale));
  line-height: 1.35;
  margin: 30px auto;
  max-width: 460px;
  color: var(--work-accent);
  padding: 0;
}
.theme-search .work-critic-quote {
  font-family: "Inter", sans-serif;
  font-style: normal;
}
.theme-search .work-critic-attr {
  font-family: "Inter", sans-serif;
}
.theme-search .work-detail-gallery {
  gap: 4px;
}
.theme-search .work-detail-thumb {
  width: 44px;
  height: 44px;
  opacity: 0.55;
}

/* == literary (Transcoding) -- classic magazine feature ==
   the video used to shrink to a small inset photo here, floated so real
   newsprint-style multi-column text could run around it -- dropped in
   favor of the same centered full-width layout every other theme now
   uses (see the base .work-video-embed rule), so there's no floated video
   left for the 2-column text to wrap against either. This theme's own
   identity still comes through in its type/kicker/color below. */
.theme-literary .work-detail-kicker {
  font-family: "Newsreader", var(--editorial-serif);
  font-style: italic;
  font-weight: 500;
}
.theme-literary .work-detail-title {
  font-family: "Newsreader", var(--editorial-serif);
  font-weight: 500;
  font-style: normal;
  font-size: calc(42px * var(--ui-scale));
}
.theme-literary .work-detail-instrumentation {
  font-family: "Newsreader", var(--editorial-serif);
}
.theme-literary .work-detail-concept {
  font-family: "Newsreader", var(--editorial-serif);
  font-size: calc(17px * var(--ui-scale));
  line-height: 1.75;
}
.theme-literary .work-detail-concept-first::first-letter {
  font-family: "Newsreader", var(--editorial-serif);
  font-size: 4em;
}
.theme-literary .work-detail-quote {
  font-family: "Newsreader", var(--editorial-serif);
  font-style: italic;
  font-size: calc(23px * var(--ui-scale));
  border-left: 1px solid var(--work-accent);
  padding-left: 24px;
  margin: 24px 0 24px 8px;
  color: var(--dim);
}
.theme-literary .work-critic-quote {
  font-family: "Newsreader", var(--editorial-serif);
}
.theme-literary .work-critic-attr {
  font-family: "Newsreader", var(--editorial-serif);
  font-style: italic;
}
.theme-literary .work-detail-thumb {
  width: 78px;
  height: 78px;
}

/* == field-guide (Superorganismo) -- natural-history/specimen-plate ==
   used to be a real two-column "specimen sheet" grid (a narrow sidebar
   rail for instrumentation/duration/year/performers/rule, a wider column
   for everything else) -- retired in favor of the same single-column
   stacked layout every other theme uses now, for two concrete reasons
   this kept causing: (1) a CSS grid item's box-generation rules force
   `display: inline-block` to compute as block + stretch-to-fill-column,
   so the kicker's own small bordered "pill" (see .theme-field-guide
   .work-detail-kicker below) rendered as a border spanning the ENTIRE
   row width instead of hugging its own text; (2) the sidebar column
   being much shorter than the main column left a tall stray-looking
   empty band on the left for most of the page. This theme's own
   identity still comes through in its type/kicker-box/thumb-size
   styling below, just in normal document flow now. */
.theme-field-guide .work-detail-kicker {
  font-family: "Zilla Slab", var(--editorial-serif);
  border: 1px solid var(--work-accent);
  padding: 2px 8px;
  display: inline-block;
}
.theme-field-guide .work-detail-title {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-weight: 700;
  font-style: normal;
  font-size: calc(38px * var(--ui-scale));
  /* same tall-caps clipping risk as .theme-zine above -- Zilla Slab Bold's
     ascenders exceed the base .work-detail-title line-height (1.1) at
     this weight/size */
  line-height: 1.15;
}
.theme-field-guide .work-detail-instrumentation,
.theme-field-guide .work-detail-duration,
.theme-field-guide .work-detail-year,
.theme-field-guide .work-detail-performers {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-style: normal;
}
.theme-field-guide .work-detail-instrumentation {
  border: 1px solid var(--border);
  padding: 8px 12px;
  display: block;
  background: color-mix(in srgb, var(--work-accent) 6%, transparent);
}
.theme-field-guide .work-detail-concept {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-size: calc(15px * var(--ui-scale));
}
.theme-field-guide .work-detail-concept-first::first-letter {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-weight: 700;
}
.theme-field-guide .work-detail-quote {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-style: italic;
  border: 1px dashed var(--work-accent);
  padding: 16px;
  position: relative;
  margin: 26px 0 20px;
}
.theme-field-guide .work-detail-quote::before {
  content: "FIELD NOTE";
  position: absolute;
  top: -9px;
  left: 12px;
  background: var(--panel);
  font-family: "Zilla Slab", var(--editorial-serif);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.1em;
  padding: 0 6px;
  color: var(--work-accent);
}
.theme-field-guide .work-critics-label,
.theme-field-guide .work-critic-attr {
  font-family: "Zilla Slab", var(--editorial-serif);
}
.theme-field-guide .work-critic-quote {
  font-family: "Zilla Slab", var(--editorial-serif);
}
.theme-field-guide .work-detail-gallery {
  gap: 12px;
}
.theme-field-guide .work-detail-thumb {
  width: 88px;
  height: 88px;
  border: 2px solid var(--text);
  opacity: 1;
}
.theme-field-guide .work-detail-thumb-wrap::after {
  content: "fig. " attr(data-i);
  display: block;
  text-align: center;
  font-family: "Zilla Slab", var(--editorial-serif);
  font-style: italic;
  font-size: 10px;
  color: var(--dim);
  margin-top: 3px;
}

/* ---------- "Fullscreen Lecture" (see openWorkLecture() in script.js) ----------
   a true viewport-covering reading view -- distraction-free: title/meta/
   quote/body text, video, and (unlike an earlier version of this) its own
   inline/carousel photos are clickable into #photo-lightbox too, same as
   the in-window gallery -- see openWorkLecture()'s own photo-click wiring.
   #photo-lightbox itself sits ABOVE this overlay (z-index 22000 vs 21000)
   so an enlarged photo actually shows up on top of it. Reuses the same
   .theme-* rules as the in-window page (scoped under .work-lecture-content
   instead of .work-detail-page) so the identity carries over intact. */
#work-lecture-overlay {
  position: fixed;
  inset: 0;
  z-index: 21000;
  overflow-y: auto;
  background: var(--panel);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
  /* the viewport itself, unlike .works-detail's own container-type
     (that one exists because a WORKS.EXE window is user-resizable
     independent of the browser viewport) -- but this overlay's own
     @container rule (.work-critics-list's 2-column layout) still needs a
     real containment context to query against, since a plain @media
     query would be measuring the wrong box if this ever nests inside
     something narrower than the full viewport later */
  container-type: inline-size;
  /* set from JS (openWorkLecture()) -- lives here, not just on
     .work-lecture-content, so the reused .work-back/.work-next prev/next
     buttons (siblings of that content div, not descendants of it) still
     pick up this work's own identity color */
  --work-accent: hsl(var(--work-accent-h, 0), 58%, 46%);
}
#work-lecture-overlay.active {
  opacity: 1;
  pointer-events: auto;
}
.reduce-motion #work-lecture-overlay {
  transition: none;
}

/* icon (icons/toggle_on.png -- "on" since this button only shows up
   while lecture mode IS on) plus its own text label, same "clarify it
   textually" convention as the prev/next buttons below -- no button box
   (the icon already reads as the control), just a color shift on hover */
.work-lecture-close {
  position: fixed;
  top: 20px;
  right: 24px;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  line-height: 1;
  background: none;
  border: none;
  color: var(--text);
  padding: 0;
  cursor: var(--cur-arrow);
}
.work-lecture-close img {
  width: 23px;
  height: 23px;
}
.work-lecture-close:hover {
  color: var(--dim);
}

/* prev/next work -- both anchored to the SAME bottom row, like a book's
   page-turn arrows, rather than two unrelated buttons in different
   corners. Close stays alone up top. Reuses .work-back/.work-next for
   their layout only (see the HTML) -- their own button-box AND their
   per-work accent color are both overridden away here: icons/previous.png
   and next.png already carry the "this is a control" presence, and the
   text stays plain --text black regardless of which work's theme is
   showing, same as .work-lecture-close -- lecture-mode chrome doesn't
   borrow the page's own editorial identity color the way window-mode's
   nav does. */
.work-lecture-prev,
.work-lecture-next {
  position: fixed;
  z-index: 1;
  bottom: 24px;
  margin: 0 !important; /* .work-back's own margin-bottom assumes it's inline with body content */
  background: none;
  border: none;
  padding: 0;
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
}
.work-lecture-prev img,
.work-lecture-next img {
  width: 20px;
  height: 20px;
}
.work-lecture-prev:hover,
.work-lecture-next:hover {
  color: var(--dim);
}
.work-lecture-prev {
  left: 24px;
}
.work-lecture-next {
  right: 24px;
}

.work-lecture-content {
  max-width: 680px;
  margin: 0 auto;
  padding: 90px 32px 120px;
}

/* the in-window .work-video-embed floats beside the text column (see its
   own rule) -- there's no text column to float beside here, just one full
   reading width, so the lecture's video sits as a plain full-width lead
   image instead, the way a feature article opens on its own photo before
   any body text */
.work-lecture-video {
  float: none;
  width: 100%;
  max-width: none;
  margin: 0 0 32px;
}

/* photos distributed through the body text (see distributeLecturePhotos()
   in script.js) instead of a detached gallery strip -- reuses the same
   .work-detail-thumb-wrap/.work-detail-thumb classes (and their per-theme
   numbering/caption styling) the in-window gallery uses, just sized up
   from a small thumbnail to a genuine full-width plate since this is
   standing in for real inline body images, not a filmstrip */
/* a real inset image now -- floated at a modest size so the surrounding
   paragraph text actually wraps around it (the way a magazine tucks a
   photo into a column of text), not a full-width block that breaks the
   read every time one shows up. Alternates which side it floats on
   (odd/even) so a piece with more than one doesn't always interrupt the
   same margin. */
.work-lecture-inline-photo {
  display: block;
  width: 42%;
  max-width: 300px;
  margin: 6px 0 20px 22px;
}
.work-lecture-inline-photo:nth-of-type(even) {
  float: left;
  margin: 6px 22px 20px 0;
}
.work-lecture-inline-photo:nth-of-type(odd) {
  float: right;
}
.work-lecture-inline-photo .work-detail-thumb {
  width: 100%;
  height: auto;
  max-height: 320px;
  object-fit: cover;
  border: none;
  opacity: 1;
}
.work-lecture-inline-photo .work-detail-thumb:hover {
  opacity: 0.85;
  border-color: transparent;
}
/* the in-window gallery's per-theme numbered-plate badge (.theme-zine's
   corner tag, .theme-field-guide's "fig. N" caption -- both keyed off
   .work-detail-thumb-wrap, which this inline photo reuses) doesn't belong
   here: those numbers exist to key the gallery grid, and there's no grid
   in a reading view, just one picture reused wherever it falls in the
   text -- it read as an unexplained stray icon instead. */
.work-lecture-inline-photo::after {
  content: none !important;
}

/* everything selectLecturePhotos() (script.js) didn't feature inline
   still shows up here -- same "More images" treatment as the in-window
   gallery (see .work-more-images/.work-detail-gallery): a small label
   plus a single-row, horizontally-scrollable strip, not a filmstrip that
   grows to however many rows the leftover photo count needs. Clickable
   into #photo-lightbox, same as the inline photos above (see
   openWorkLecture()'s own photo-click wiring). Every theme gets the same
   plain strip, after the critics section (see openWorkLecture()'s own
   HTML order). */
.work-lecture-carousel {
  display: flex;
  flex-wrap: nowrap;
  gap: 10px;
  overflow-x: auto;
}
.work-lecture-carousel-img {
  flex: 0 0 auto;
  height: 140px;
  width: auto;
  object-fit: cover;
  cursor: var(--cur-arrow);
  opacity: 0.85;
}
.work-lecture-carousel-img:hover {
  opacity: 1;
}

.work-lecture-kicker {
  color: var(--work-accent);
  font-family: var(--mono);
  font-weight: bold;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  margin-bottom: 14px;
}
.work-lecture-content.theme-zine .work-lecture-kicker {
  display: inline-block;
  background: var(--text);
  color: var(--panel);
  padding: 3px 8px;
}
.work-lecture-content.theme-search .work-lecture-kicker {
  font-family: "Inter", sans-serif;
  font-weight: 600;
}
.work-lecture-content.theme-literary .work-lecture-kicker {
  font-family: "Newsreader", var(--editorial-serif);
  font-style: italic;
}
.work-lecture-content.theme-field-guide .work-lecture-kicker {
  font-family: "Zilla Slab", var(--editorial-serif);
  border: 1px solid var(--work-accent);
  padding: 2px 8px;
  display: inline-block;
}

.work-lecture-title {
  font-family: var(--editorial-serif);
  font-style: italic;
  font-size: 52px;
  line-height: 1.12;
  margin: 0 0 18px;
}
.work-lecture-content.theme-zine .work-lecture-title {
  font-family: "Archivo Black", var(--editorial-serif);
  font-style: normal;
  text-transform: uppercase;
  font-size: 56px;
  line-height: 1.05;
}
.work-lecture-content.theme-search .work-lecture-title {
  font-family: "Inter", sans-serif;
  font-style: normal;
  font-weight: 800;
  font-size: 42px;
  line-height: 1.25;
}
.work-lecture-content.theme-literary .work-lecture-title {
  font-family: "Newsreader", var(--editorial-serif);
  font-weight: 500;
  font-style: normal;
  font-size: 54px;
}
.work-lecture-content.theme-field-guide .work-lecture-title {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-weight: 700;
  font-style: normal;
  font-size: 48px;
  line-height: 1.15;
}

.work-lecture-meta,
.work-lecture-performers {
  font-family: var(--mono);
  color: var(--dim);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 12px;
  margin: 0 0 6px;
}
.work-lecture-content.theme-zine .work-lecture-meta,
.work-lecture-content.theme-zine .work-lecture-performers {
  font-family: "Space Mono", var(--mono);
}
.work-lecture-content.theme-search .work-lecture-meta,
.work-lecture-content.theme-search .work-lecture-performers {
  font-family: "Inter", sans-serif;
  text-transform: none;
  letter-spacing: 0;
}
.work-lecture-content.theme-literary .work-lecture-meta,
.work-lecture-content.theme-literary .work-lecture-performers {
  font-family: "Newsreader", var(--editorial-serif);
  font-style: italic;
  text-transform: none;
  letter-spacing: 0;
}
.work-lecture-content.theme-field-guide .work-lecture-meta,
.work-lecture-content.theme-field-guide .work-lecture-performers {
  font-family: "Zilla Slab", var(--editorial-serif);
}

.work-lecture-p {
  font-family: var(--editorial-serif);
  font-size: 19px;
  line-height: 1.8;
  margin: 0 0 22px;
}
.work-lecture-p-first::first-letter {
  float: left;
  font-size: 3.4em;
  line-height: 0.8;
  padding-right: 10px;
  color: var(--work-accent);
}
.work-lecture-content.theme-zine .work-lecture-p {
  font-family: "Space Mono", var(--mono);
  font-size: 15px;
  line-height: 1.7;
}
.work-lecture-content.theme-zine .work-lecture-p-first::first-letter {
  font-family: "Archivo Black", var(--editorial-serif);
  background: var(--work-accent);
  color: var(--panel);
  padding: 4px 9px;
  margin-right: 10px;
  font-size: 2.2em;
  line-height: 1.3;
  float: left;
}
.work-lecture-content.theme-search .work-lecture-p {
  font-family: "Inter", sans-serif;
  font-size: 18px;
}
.work-lecture-content.theme-literary .work-lecture-p {
  font-family: "Newsreader", var(--editorial-serif);
  font-size: 20px;
}
.work-lecture-content.theme-field-guide .work-lecture-p {
  font-family: "Zilla Slab", var(--editorial-serif);
  font-size: 18px;
}

.work-lecture-quote {
  font-family: var(--editorial-serif);
  font-style: italic;
  font-size: 28px;
  line-height: 1.4;
  border-left: 3px solid var(--work-accent);
  padding-left: 24px;
  margin: 30px 0;
  color: var(--text);
}
.work-lecture-content.theme-zine .work-lecture-quote {
  font-family: "Space Mono", var(--mono);
  font-style: normal;
  border: none;
  background: var(--text);
  color: var(--panel);
  padding: 18px 20px;
  font-size: 18px;
}
.work-lecture-content.theme-zine .work-lecture-quote::before {
  content: "> ";
  color: var(--work-accent);
}
.work-lecture-content.theme-search .work-lecture-quote {
  font-family: "Fraunces", var(--editorial-serif);
  border: none;
  text-align: center;
  font-size: 32px;
  color: var(--work-accent);
  padding: 0;
  margin: 34px auto;
  max-width: 480px;
}
.work-lecture-content.theme-literary .work-lecture-quote {
  font-family: "Newsreader", var(--editorial-serif);
  font-size: 30px;
  color: var(--dim);
}
.work-lecture-content.theme-field-guide .work-lecture-quote {
  font-family: "Zilla Slab", var(--editorial-serif);
  border: 1px dashed var(--work-accent);
  padding: 18px;
}

/* ---------- MAIL COMPOSE (contact app) ---------- */

/* the message field grows to fill whatever room is left in the window
   after the header/email/subject fields and the send button -- previously
   it was a fixed 220px regardless of how much taller the window was,
   leaving a lot of dead white space below it on anything bigger than the
   smallest window size */
/* ---------- CONTACT.EXE: "desktop mail client" redesign ----------
   A Mail.app/Outlook-style compose pane (To/From/Subject header rows, a
   plain body, a rounded blue Send button) plus a sidebar of invented
   Inbox/Sent threads -- pure set dressing, not real correspondence, so
   the compose window reads as one pane of a fuller mail client rather
   than a bare form. Fixed light colors regardless of the desktop's own
   dark/light toggle, same reasoning as BIO.EXE's fixed dark terminal:
   this app's identity is its own, not the OS's. */

.window[data-app="contact"] {
  --mail-bg: #ffffff;
  --mail-sidebar-bg: #f5f5f3;
  --mail-border: #e2e2dc;
  --mail-text: #1a1a1a;
  --mail-dim: #8a8a80;
  --mail-accent: #1155cc;
}

.window[data-app="contact"] .window-body {
  padding: 0;
  overflow: hidden;
  background: var(--mail-bg);
  color: var(--mail-text);
  font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

.contact-app {
  height: 100%;
}

.mail-body {
  display: flex;
  height: 100%;
  min-height: 0;
}

/* ---- invented Inbox/Sent sidebar -- see the CONTACT.EXE template
   comment in index.html: purely decorative, not real mail ---- */
.mail-sidebar {
  width: 190px;
  flex-shrink: 0;
  background: var(--mail-sidebar-bg);
  border-right: 1px solid var(--mail-border);
  padding: 10px 0;
  overflow-y: auto;
}

.mail-sidebar-heading {
  padding: 12px 14px 6px;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mail-dim);
}
.mail-sidebar-heading:first-child { padding-top: 2px; }

.mail-thread {
  padding: 6px 14px;
  border-bottom: 1px solid var(--mail-border);
  cursor: pointer;
}
.mail-thread:hover { background: var(--mail-border); }
.mail-thread.active { background: var(--mail-accent); }
.mail-thread.active .mail-thread-name,
.mail-thread.active .mail-thread-subject,
.mail-thread.active .mail-thread-time {
  color: #fff;
}

.mail-thread-top {
  display: flex;
  justify-content: space-between;
  gap: 6px;
}

.mail-thread-name {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--mail-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mail-thread-time {
  font-size: 10px;
  color: var(--mail-dim);
  flex-shrink: 0;
}

.mail-thread-subject {
  font-size: 10.5px;
  color: var(--mail-dim);
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- compose pane ---- */
.mail-compose {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.mail-compose-toolbar {
  height: 38px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 16px;
  border-bottom: 1px solid var(--mail-border);
}

.mail-compose-title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--mail-text);
}

.mail-attach-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  font-size: 11px;
  color: var(--mail-dim);
  cursor: pointer;
}
.mail-attach-btn:hover { color: var(--mail-text); }
.mail-attach-btn.hidden { display: none; }

#contact-attachment { display: none; }

.mail-compose-title.hidden { display: none; }

.mail-back-btn {
  border: none;
  background: none;
  color: var(--mail-accent);
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  padding: 0;
}
.mail-back-btn.hidden { display: none; }

#contact-form.hidden { display: none; }

/* ---- reading pane: shown instead of the compose form when an invented
   thread in the sidebar is clicked (see initContactApp() in script.js) ---- */
.mail-reading-pane {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 16px 20px;
}
.mail-reading-pane.hidden { display: none; }

.mail-reading-from {
  font-size: 13px;
  font-weight: 600;
  color: var(--mail-text);
}

.mail-reading-subject {
  font-size: 15px;
  font-weight: 700;
  color: var(--mail-text);
  margin-top: 4px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--mail-border);
}

.mail-reading-body {
  font-size: 13px;
  color: var(--mail-text);
  line-height: 1.6;
  margin-top: 14px;
  white-space: pre-wrap;
}

#contact-form {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  padding: 4px 20px 16px;
}

.mail-field-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--mail-border);
  font-size: 12.5px;
}

.mail-field-row label {
  width: 90px;
  flex-shrink: 0;
  color: var(--mail-dim);
}

.mail-to-value { color: var(--mail-text); }

.mail-field-row input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: none;
  font: inherit;
  color: var(--mail-text);
}

/* ---- format bar: font/size/bold/italic/underline + an emoji row, all
   above the message body (see initContactApp() in script.js for the
   execCommand wiring, and its comment on the HTML-vs-plain-text caveat) ---- */
.mail-format-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding: 8px 0;
  margin-top: 6px;
  border-top: 1px solid var(--mail-border);
  border-bottom: 1px solid var(--mail-border);
}

.mail-format-select {
  font-family: inherit;
  font-size: 11px;
  color: var(--mail-text);
  background: var(--mail-sidebar-bg);
  border: none;
  border-radius: 4px;
  padding: 4px 6px;
}

.mail-format-divider {
  width: 1px;
  height: 16px;
  background: var(--mail-border);
  margin: 0 2px;
}

.mail-format-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--mail-text);
  font-size: 12px;
  border-radius: 4px;
  cursor: pointer;
}
.mail-format-btn:hover { background: var(--mail-sidebar-bg); }

.mail-emoji-btn {
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  font-size: 14px;
  line-height: 1;
  border-radius: 4px;
  cursor: pointer;
}
.mail-emoji-btn:hover { background: var(--mail-sidebar-bg); }

.mail-field-message {
  flex: 1;
  min-height: 0;
  margin-top: 10px;
}

.mail-body-editor {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  overflow-y: auto;
  outline: none;
  font-size: 13px;
  color: var(--mail-text);
  line-height: 1.6;
  /* the default body's blank line before the "(I know this website is a
     joke...)" aside (see contact.defaultBody in script.js) is a literal
     \n\n in that string -- textContent doesn't turn that into a visible
     line break on its own, this is what does */
  white-space: pre-wrap;
}

.mail-attachment-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  background: var(--mail-sidebar-bg);
  color: var(--mail-dim);
  font-size: 11px;
  padding: 5px 8px;
  border-radius: 5px;
  margin-top: 10px;
}
.mail-attachment-chip.hidden { display: none; }

#contact-attachment-remove {
  border: none;
  background: none;
  color: var(--mail-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  padding: 0 0 0 4px;
}
#contact-attachment-remove:hover { color: var(--mail-text); }

.mail-send-btn {
  align-self: flex-end;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: #fff;
  background: var(--mail-accent);
  border: none;
  padding: 9px 22px;
  border-radius: 20px;
  margin-top: 14px;
}

.mail-send-btn:hover {
  background: #0d3d99;
}

.mail-send-btn:disabled {
  opacity: 0.6;
}

.window-body::-webkit-scrollbar { width: 8px; }
.window-body::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 0;
}

.trash-empty {
  font-size: calc(13px * var(--ui-scale));
  color: var(--dim);
  padding: 20px 0;
  text-align: center;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}

.trash-empty.hidden { display: none; }

.trash-empty-btn {
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  color: var(--danger);
  background: transparent;
  border: 1px solid var(--danger);
  padding: 6px 12px;
  flex-shrink: 0;
}

.trash-empty-btn:hover:not(:disabled) {
  background: var(--danger);
  color: var(--bg);
}

.trash-empty-btn:disabled {
  opacity: 0.4;
  cursor: var(--cur-arrow);
}

.trash-empty-btn.hidden { display: none; }

.trash-viewer-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}

.trash-viewer-toolbar .work-back {
  margin-bottom: 0; /* .work-back's own margin assumes it's the only thing on its line */
}

.trash-viewer-nav {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.trash-nav-btn {
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.02em;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  padding: 5px 10px;
}

.trash-nav-btn:hover {
  border-color: var(--text);
  background: var(--text);
  color: var(--bg);
}

.trash-viewer-photo {
  display: block;
  max-width: 100%;
  max-height: 420px;
  margin: 0 auto 14px;
  border: 1px solid var(--border);
}

.trash-viewer-video {
  display: block;
  width: 100%;
  max-height: 420px;
  background: #000;
  margin-bottom: 14px;
}

.trash-viewer-audio {
  display: block;
  width: 100%;
  margin-bottom: 14px;
}

.trash-viewer-text {
  white-space: pre-wrap;
  font-family: var(--mono);
  font-size: calc(13px * var(--ui-scale));
  line-height: 1.6;
  border: 1px solid var(--border);
  padding: 14px;
  margin: 0 0 14px;
  max-height: 420px;
  overflow-y: auto;
}

/* ---------- DESKTOP SELECTION BOX ---------- */

.selection-box {
  position: fixed;
  border: 1px dashed var(--text);
  background: var(--selection-fill);
  z-index: 15;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: none;
}

.selection-box.hidden { display: none; }

.selection-text {
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  font-weight: bold;
  color: var(--text);
  white-space: nowrap;
  padding: 4px 10px;
}

/* ---------- CONTEXT MENU ---------- */

.context-menu {
  position: absolute;
  width: 185px;
  /* same glass treatment as .dropdown-menu -- see its comment */
  background: color-mix(in srgb, var(--panel-2) 75%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  border: 1px solid var(--border);
  z-index: 501;
}

.context-menu.hidden { display: none; }

.context-menu-item {
  padding: 9px 13px;
  font-size: calc(13px * var(--ui-scale));
}

.context-menu-item:hover {
  background: var(--accent);
  color: var(--on-accent);
}

.context-menu-item.disabled {
  color: var(--dim);
  opacity: 0.5;
}

.context-menu-item.disabled:hover {
  background: transparent;
  color: var(--dim);
}

.context-menu-sep {
  height: 1px;
  background: var(--border);
  margin: 4px 0;
}

/* ---------- MENU PERSPECTIVE / RELIEF ----------
   shared depth treatment for every popup menu (right-click menu, top-bar
   dropdowns): a soft top highlight + bottom seam etched into the panel
   itself (like light catching a raised edge), a two-layer shadow (a tight
   contact shadow plus a wider ambient one, instead of one flat blob) so
   the panel actually reads as lifted off the desktop, and a quick
   perspective pop-in instead of just appearing. Items get the same idea
   in miniature: hovering lifts a row a hair with its own small shadow,
   clicking presses it back down. */

.context-menu,
.dropdown-menu {
  border-radius: 0;
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.55),
    inset 0 -1px 0 rgba(0,0,0,0.12),
    0 2px 6px rgba(0,0,0,0.22),
    0 18px 40px rgba(0,0,0,0.5);
  transform-origin: top center;
  animation: menu-pop-in 130ms cubic-bezier(0.2, 0.8, 0.3, 1);
}

@keyframes menu-pop-in {
  from {
    opacity: 0;
    transform: perspective(600px) rotateX(-8deg) scale(0.97) translateY(-4px);
  }
  to {
    opacity: 1;
    transform: perspective(600px) rotateX(0deg) scale(1) translateY(0);
  }
}

.context-menu-item,
.dropdown-item {
  position: relative;
  transition: transform 90ms ease, box-shadow 90ms ease;
}

.context-menu-item:hover,
.dropdown-item:hover {
  box-shadow: 0 2px 5px rgba(0,0,0,0.25);
  transform: translateY(-1px);
}

.context-menu-item:active,
.dropdown-item:active {
  transform: translateY(0);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
}

.context-menu-item.disabled:hover {
  box-shadow: none;
  transform: none;
}

/* ---------- SEARCH (spotlight) ---------- */

.search-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: 600;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 14vh;
}

.search-overlay.hidden { display: none; }

.search-box {
  width: min(560px, 86vw);
  max-height: 60vh;
  background: var(--panel-2);
  border: 1px solid var(--border);
  box-shadow: 0 30px 70px rgba(0,0,0,0.6);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#search-input {
  width: 100%;
  padding: 16px 18px;
  font-family: var(--mono);
  font-size: calc(18px * var(--ui-scale));
  color: var(--text);
  background: transparent;
  border: none;
  border-bottom: 1px solid var(--border);
}

#search-input:focus {
  outline: none;
}

.search-results {
  overflow-y: auto;
}

.search-result-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 11px 18px;
  font-size: calc(13px * var(--ui-scale));
  border-bottom: 1px solid var(--border);
}

.search-result-row:last-child { border-bottom: none; }

.search-result-row .file-glyph {
  color: var(--accent);
  margin-right: 10px;
}

.search-result-tag {
  color: var(--dim);
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.search-result-row:hover,
.search-result-row.active {
  background: var(--accent);
  color: var(--on-accent);
}

.search-result-row:hover .search-result-tag,
.search-result-row.active .search-result-tag {
  color: var(--on-accent);
  opacity: 0.7;
}

.search-empty {
  padding: 16px 18px;
  font-size: calc(13px * var(--ui-scale));
  color: var(--dim);
}

/* ============================================================
   MOBILE — phone-OS layout, active only below the breakpoint.
   Desktop DOM/behavior above this point is untouched; everything
   here is either hidden by default and revealed by the media query,
   or overridden inside the media query.
   ============================================================ */

.mobile-statusbar,
.mobile-home-heading {
  display: none;
}

.mobile-settings {
  display: none;
}

@media (max-width: 780px) {

  /* ---- hide desktop chrome, show phone chrome ---- */

  .menu-bar,
  .taskbar {
    display: none;
  }

  /* no top bar at all now -- search and settings were the only two
     things in it, and both are gone (see index.html: this stays in the
     DOM but is never shown on mobile). The feed runs fully edge to edge
     with nothing floating over its top corner. */
  .mobile-statusbar {
    display: none;
  }

  .mobile-statusbar > * {
    pointer-events: auto;
  }

  .mobile-clock {
    display: none;
  }

  .mobile-statusbar-btn {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text);
    background: color-mix(in srgb, var(--panel) 55%, transparent);
    backdrop-filter: blur(14px) saturate(160%);
    -webkit-backdrop-filter: blur(14px) saturate(160%);
    border: 1px solid var(--divider);
    box-shadow: 0 2px 10px rgba(0,0,0,0.16);
    padding: 0;
    transition: transform 0.12s ease, background 0.15s ease;
  }

  /* :hover doesn't give touch screens any feedback -- :active does */
  .mobile-statusbar-btn:active {
    background: color-mix(in srgb, var(--panel) 85%, transparent);
    transform: scale(0.9);
  }

  .mobile-chip {
    padding: 9px 14px;
  }

  .mobile-chip:active {
    transform: scale(0.95);
  }

  /* ---- home screen ---- */
  /* #desktop's overflow-y: auto is now the base rule (not mobile-only) */

  .mobile-home-heading {
    display: block;
    position: relative;
    z-index: 5; /* keep above #desktop::after's vignette (z-index: 1) */
    margin: 56px 0 8px;
    padding: 0 20px;
    font-size: calc(26px * var(--ui-scale));
    font-weight: bold;
    color: var(--text);
    text-align: center;
  }

  .icons {
    /* reset the desktop-only column-grid properties (margin, column auto-flow,
       explicit row count) — otherwise mobile inherits a broken hybrid of
       both layouts */
    position: relative;
    margin: 0;
    z-index: 5; /* z-index: auto here would sink icons below the vignette again */
    display: grid;
    grid-auto-flow: row;
    grid-template-rows: none;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 30px 8px;
    width: 100%;
    padding: 10px 14px 28px;
    justify-items: center;
  }

  .icon-trash {
    /* rejoins the same grid as every other icon here instead of staying
       pinned to a corner -- see the desktop rule this overrides */
    position: static;
  }

  .icon {
    width: auto;
  }

  /* filled app tiles (Metro/tile-UI territory) instead of a bare glyph
     floating on the background, so the home screen reads more like an
     actual app grid at a glance -- unlike the desktop rule above (which
     dropped its box entirely), this one keeps its own border/background/
     radius, so it needs to redeclare all three rather than inherit them */
  .icon-glyph {
    width: calc(54px * var(--icon-scale));
    height: calc(54px * var(--icon-scale));
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 0;
    filter: none;
    box-shadow: 0 3px 10px rgba(0,0,0,0.18);
    transition: transform 0.1s ease, box-shadow 0.1s ease;
  }

  .icon:active .icon-glyph {
    transform: scale(0.92);
    box-shadow: 0 1px 4px rgba(0,0,0,0.18);
  }

  .icon-label {
    font-size: calc(11.5px * var(--ui-scale));
    /* the 3-column mobile grid is much narrower than desktop's fixed-width
       icon column, so the longest labels (translated "TEACHING AND
       TECHNICAL.EXE") wrap to 3 lines while everything else wraps to 1-2 --
       CSS Grid sizes each ROW to its tallest cell, so that one label alone
       was making its whole row taller and throwing every row below it out
       of alignment. Reserving space for the worst case up front keeps
       every row the same height regardless of what any one label needs. */
    line-height: 1.25;
    min-height: 3.75em; /* 3 lines at the line-height above */
  }

  /* ---- full-screen apps ---- */

  .window {
    position: fixed !important;
    left: 0 !important;
    top: 40px !important;
    width: 100% !important;
    height: calc(100% - 40px) !important;
    border: none;
    outline: none;
    /* flush edge-to-edge on a phone screen -- same reasoning as
       .window.maximized above */
    border-radius: 0;
  }

  .window-resize-handle {
    display: none;
  }

  .window.opening {
    animation: mobile-slide-up 0.28s ease-out;
  }

  .window.closing {
    animation: mobile-slide-down 0.22s ease-in forwards;
  }

  .window-titlebar {
    height: 52px;
    padding: 0 8px 0 4px;
  }

  .window-controls .btn-min,
  .window-controls .btn-max {
    display: none;
  }

  .window-controls .btn-close {
    width: 40px;
    height: 34px;
    font-size: calc(15px * var(--ui-scale));
  }

  /* ---- search overlay: full screen on phones ---- */

  .search-overlay {
    padding-top: 40px;
    align-items: stretch;
    justify-content: stretch;
  }

  .search-box {
    width: 100%;
    max-height: none;
    height: calc(100% - 40px);
  }
}

@keyframes mobile-slide-up {
  0%   { transform: translateY(100%); }
  100% { transform: translateY(0); }
}

@keyframes mobile-slide-down {
  0%   { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(100%); opacity: 1; }
}

/* ---- mobile settings sheet (structure defined outside the query,
   visibility gated inside it — see .mobile-settings rule above) ---- */

@media (max-width: 780px) {
  .mobile-settings:not(.hidden) {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset: 0;
    background: var(--panel);
    z-index: 700;
    /* same slide-up an app window uses when it opens -- makes this read as
       a modal sheet the OS is presenting, not a page that just appears */
    animation: mobile-slide-up 0.28s ease-out;
  }
}

.mobile-settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 52px;
  flex-shrink: 0;
  padding: 0 16px;
  font-size: calc(15px * var(--ui-scale));
  font-weight: bold;
  color: var(--text);
  border-bottom: 1px solid var(--border);
}

.mobile-settings-header button {
  width: 30px;
  height: 30px;
  font-size: calc(13px * var(--ui-scale));
  color: var(--text);
  background: transparent;
  border: 1.5px solid var(--text);
}

.mobile-settings-header button:hover {
  background: var(--text);
  color: var(--bg);
}

.mobile-settings-body {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

/* the profile card an actual phone settings app opens on -- avatar + name
   before any toggle, so the sheet reads as "your account" rather than a
   flat list of switches */
.mobile-profile-card {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 6px 2px 22px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--border);
}

.mobile-profile-avatar {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  border: 1px solid var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--panel-2);
}

.mobile-profile-avatar svg {
  width: 70%;
  height: 70%;
  color: var(--text);
}

.mobile-profile-info {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.mobile-profile-label {
  font-size: calc(10.5px * var(--ui-scale));
  letter-spacing: 0.1em;
  color: var(--dim);
}

.mobile-profile-name {
  font-size: calc(16px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.04em;
  color: var(--text);
  overflow-wrap: anywhere;
}

.mobile-settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
  font-size: calc(14px * var(--ui-scale));
  color: var(--text);
}

.mobile-settings-row-stack {
  flex-wrap: wrap;
}

.mobile-settings-row-stack input[type="range"] {
  flex: 1;
  min-width: 120px;
  accent-color: var(--text);
}

.mobile-lang-options {
  display: flex;
  gap: 6px;
}

.mobile-chip {
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  color: var(--text);
  background: transparent;
  border: 1.5px solid var(--border);
  padding: 7px 12px;
}

.mobile-chip:hover {
  border-color: var(--text);
}

.font-select {
  font-family: var(--mono);
  font-size: calc(12px * var(--ui-scale));
  letter-spacing: 0.04em;
  color: var(--text);
  background: var(--panel-2);
  border: 1.5px solid var(--border);
  padding: 7px 10px;
  max-width: 160px;
}

.font-select:hover,
.font-select:focus {
  border-color: var(--text);
  outline: none;
}

.font-select option {
  background: var(--panel-2);
  color: var(--text);
}

.mobile-chip.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.mobile-shutdown-row {
  width: 100%;
  margin-top: 20px;
  padding: 12px 0;
  text-align: center;
  border-color: var(--danger);
  color: var(--danger);
}

/* ---------- CV APP ---------- */
/* two panes: a control rail in the OS's own mono/terminal voice, and a
   preview pane holding a "real paper" that intentionally breaks from that
   voice -- serif/sans, fixed light colors regardless of theme -- because
   it's a preview of an actual printed document, not desktop chrome */

.window[data-app="cv"] {
  width: min(1120px, 94vw);
  height: min(780px, 88vh);
}

.cv-app {
  display: flex;
  height: 100%;
  min-height: 0;
}

.window[data-app="cv"] .window-body {
  padding: 0;
  overflow: hidden;
}

.cv-controls {
  width: 250px;
  flex-shrink: 0;
  overflow-y: auto;
  padding: 20px 18px;
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.cv-controls-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.cv-controls-group:last-of-type {
  border-bottom: none;
  padding-bottom: 0;
}

.cv-check-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 2px;
  font-size: calc(12.5px * var(--ui-scale));
  color: var(--text);
  cursor: var(--cur-arrow);
}

.cv-check-row input[type="checkbox"] {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  accent-color: var(--text);
}

.cv-check-row-main {
  font-weight: bold;
  letter-spacing: 0.03em;
}

.cv-check-row-sub {
  padding-left: 18px;
  color: var(--dim);
}

.cv-section-controls + .cv-section-controls {
  margin-top: 12px;
}

.cv-check-row-section {
  font-weight: bold;
  letter-spacing: 0.03em;
}

.cv-check-row-solo {
  margin-bottom: 4px;
}

.cv-entries-controls {
  display: flex;
  flex-direction: column;
  padding-left: 18px;
}

.cv-check-row-entry {
  color: var(--dim);
  font-size: calc(11.5px * var(--ui-scale));
}

.cv-check-row-entry:hover,
.cv-check-row-sub:hover {
  color: var(--text);
}

.cv-fit-group {
  gap: 8px;
}

.cv-fit-label {
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--dim);
}

.cv-fit-select {
  max-width: none;
  width: 100%;
}

.cv-page-estimate {
  font-size: calc(11px * var(--ui-scale));
  color: var(--dim);
  letter-spacing: 0.02em;
}

.cv-print-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 11px 10px;
  font-family: var(--mono);
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.05em;
  color: var(--on-accent);
  background: var(--accent);
  border: 1px solid var(--accent);
}

.cv-print-btn:hover {
  opacity: 0.85;
}

/* ---- preview pane: a scrollable "desk" holding a paper-proportioned card ---- */

.cv-preview {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 28px;
  background: var(--panel-2);
  display: flex;
  justify-content: center;
}

.cv-paper {
  width: 100%;
  max-width: 700px;
  height: fit-content;
  background: #fdfdfb;
  box-shadow: 0 14px 38px rgba(0,0,0,0.45);
  flex-shrink: 0;
}

.cv-paper-inner {
  padding: 50px 46px;
  position: relative;
  /* the paper always reads the same regardless of the OS theme or
     accessibility settings -- it's a preview of a printed page, not UI */
  color: #1a1a18;
  font-family: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif;
  font-size: 13.5px;
  line-height: 1.5;
  zoom: 1;
}

.cv-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
}

.cv-name {
  font-size: 25px;
  font-weight: bold;
  letter-spacing: 0.01em;
}

.cv-role {
  margin-top: 4px;
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #6b6b63;
}

.cv-contact-line {
  margin-top: 9px;
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 11.5px;
  color: #45453f;
}

.cv-dot {
  margin: 0 7px;
  color: #b8b6a9;
}

.cv-photo {
  width: 76px;
  height: 76px;
  flex-shrink: 0;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid #d8d6c9;
}

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

.cv-rule {
  height: 1px;
  background: #cfcdbf;
  margin: 18px 0 20px;
}

.cv-section + .cv-section {
  margin-top: 18px;
}

.cv-section-title {
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #8a887c;
  padding-bottom: 5px;
  margin-bottom: 10px;
  border-bottom: 1px solid #dedcd0;
  break-after: avoid;
  page-break-after: avoid;
}

.cv-profile-text {
  margin: 0;
}

.cv-profile-text + .cv-profile-text {
  margin-top: 10px;
}

.cv-empty-state {
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 12px;
  color: #8a887c;
}

.cv-entry-list {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.cv-entry {
  display: flex;
  gap: 16px;
  break-inside: avoid;
  page-break-inside: avoid;
}

.cv-entry-dates {
  flex-shrink: 0;
  width: 92px;
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 10.5px;
  letter-spacing: 0.02em;
  color: #8a887c;
  padding-top: 1px;
}

.cv-entry-main {
  flex: 1;
  min-width: 0;
}

.cv-entry-title {
  font-weight: bold;
}

.cv-entry-title-italic {
  font-weight: normal;
  font-style: italic;
}

.cv-entry-sub {
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 11.5px;
  color: #55534a;
}

.cv-tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
}

.cv-tag {
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 11px;
  padding: 3px 9px;
  border: 1px solid #d8d6c9;
  color: #3d3d37;
}

.cv-lang-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 22px;
}

.cv-lang {
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 12px;
}

.cv-lang-level {
  color: #8a887c;
}

.cv-print-stamp {
  margin-top: 26px;
  text-align: right;
  font-family: -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size: 9px;
  letter-spacing: 0.02em;
  color: #b6b4a7;
}

/* ---- offscreen measuring clone: never visible, used purely to compute
   the shrink-to-fit zoom factor at true A4 content width before it's
   applied to whichever element (preview or print) actually renders ---- */

.cv-measure-offscreen {
  position: fixed;
  left: -99999px;
  top: 0;
  visibility: hidden;
  width: 178mm; /* A4 210mm minus 2×16mm margins, matching @page below */
  pointer-events: none;
}

/* ---- print / export ---- */

.cv-print-root {
  display: none;
}

.cv-print-paper {
  box-shadow: none;
  max-width: none;
  width: auto;
}

.cv-print-paper .cv-paper-inner {
  padding: 0;
}

@media print {
  html, body {
    background: #fff;
  }
  #desktop, #boot-screen {
    display: none !important;
  }
  .cv-print-root {
    display: block !important;
  }
  @page {
    size: A4;
    margin: 16mm;
  }
}

@media (max-width: 780px) {
  .cv-app {
    flex-direction: column;
  }

  .cv-controls {
    width: 100%;
    max-height: 42vh;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }

  .cv-preview {
    padding: 16px;
  }

  .cv-paper-inner {
    padding: 30px 24px;
  }
}

/* ---------- ZOOM APP (prerecorded call + local self-view) ---------- */
/* a dark call-UI theatre regardless of OS theme -- same reasoning as the
   CV paper staying white in dark mode: this represents a video call, not
   desktop chrome, so it doesn't follow the brightness slider */

.window[data-app="zoom"] {
  width: min(760px, 92vw);
  height: min(600px, 84vh);
}

.window[data-app="zoom"] .window-body {
  padding: 0;
  overflow: hidden;
}

.zoom-app {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #0b0b0d;
}

/* two equal panels, side by side: your camera on the left, the
   prerecorded call on the right -- stacked vertically on narrow/mobile
   screens instead (see the @media block at the bottom of this section) */
.zoom-stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: row;
  gap: 2px;
  background: #0b0b0d;
}

.zoom-tile {
  position: relative;
  overflow: hidden;
  flex: 1;
  min-width: 0;
  background: #151517;
}

.zoom-tile video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.zoom-tile-self video {
  transform: scaleX(-1); /* mirrored, like every real self-view */
}

/* the main tile's two stacked <video> elements (see crossfadeZoomVideo()
   in script.js) -- one is always "front" (opacity 1), the other loads
   the next clip underneath it and fades in once it's ready, so every
   waiting->ok / waiting->rejected switch (and every clip-to-clip loop
   step) crossfades instead of cutting through a blank frame the way a
   single <video> swapping .src always does. */
.zoom-owner-video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.4s ease;
}
.zoom-owner-video.is-front {
  opacity: 1;
}
.reduce-motion .zoom-owner-video {
  transition: none;
}

.zoom-connecting {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0b0b0d;
  color: #e8e8e8;
  font-size: calc(13px * var(--ui-scale));
  letter-spacing: 0.12em;
  text-align: center;
  padding: 12px;
  animation: blink 1.1s steps(1) infinite;
}

.zoom-connecting.hidden {
  display: none;
}

.zoom-connecting.is-failed {
  animation: none;
}

.zoom-status {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 20px 20px 44px;
  background: #0b0b0d;
  color: #e8e8e8;
  text-align: center;
  z-index: 1;
}

.zoom-status.hidden,
.zoom-status .hidden {
  display: none;
}

.zoom-status-title {
  font-size: calc(13px * var(--ui-scale));
  letter-spacing: 0.12em;
}

.zoom-status-detail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  max-width: 34em;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.zoom-status.is-detailed .zoom-status-detail {
  opacity: 1;
}

.zoom-status:not(.is-detailed) .zoom-status-detail {
  pointer-events: none;
}

.zoom-status-msg {
  font-size: calc(14px * var(--ui-scale));
  color: #f2f2f2;
  line-height: 1.4;
}

.zoom-status-hint {
  font-size: calc(12px * var(--ui-scale));
  color: #8a8a8f;
  line-height: 1.5;
}

.zoom-status-btn {
  margin-top: 4px;
  padding: 8px 20px;
  background: #1f8fc2;
  border: 1px solid #1f8fc2;
  color: #fff;
  font-family: var(--mono);
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.04em;
  cursor: pointer;
}

.zoom-status-btn:hover {
  opacity: 0.85;
}

.zoom-sound {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 4px 10px;
  background: rgba(0,0,0,0.65);
  color: #f2f2f2;
  font-size: calc(11px * var(--ui-scale));
  z-index: 2;
  pointer-events: none;
}

.zoom-sound.hidden {
  display: none;
}

.reduce-motion .zoom-status-detail {
  transition: none;
}

.zoom-notice {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 40px;
  padding: 8px 12px;
  background: rgba(0,0,0,0.65);
  color: #f2f2f2;
  font-size: calc(13px * var(--ui-scale));
  line-height: 1.4;
  text-align: center;
  z-index: 2;
}

.zoom-notice.hidden {
  display: none;
}

.reduce-motion .zoom-connecting {
  animation: none;
}

.zoom-self-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: #1c1c1f;
  color: #8a8a8f;
  text-align: center;
  padding: 12px;
}

.zoom-self-placeholder.hidden {
  display: none;
}

.zoom-self-placeholder span {
  font-size: calc(11px * var(--ui-scale));
  line-height: 1.3;
}

.zoom-tile-label {
  position: absolute;
  left: 8px;
  bottom: 8px;
  z-index: 3;
  padding: 3px 8px;
  background: rgba(0,0,0,0.55);
  color: #f2f2f2;
  font-size: calc(11px * var(--ui-scale));
  letter-spacing: 0.03em;
}

.zoom-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  text-align: center;
  background: #0b0b0d;
  color: #8a8a8f;
  font-size: calc(12px * var(--ui-scale));
  line-height: 1.5;
}

.zoom-empty.hidden {
  display: none;
}

@media (max-width: 780px) {
  .zoom-stage {
    flex-direction: column;
  }
}

.zoom-controls {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 14px;
  background: #17171a;
  border-top: 1px solid rgba(255,255,255,0.1);
}

.zoom-ctrl-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #2b2b2f;
  border: 1px solid rgba(255,255,255,0.12);
  color: #f2f2f2;
}

.zoom-ctrl-btn:hover {
  background: #38383d;
}

.zoom-ctrl-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.zoom-ctrl-btn.off {
  background: var(--danger);
  border-color: var(--danger);
  color: #1a0a0a;
}

.zoom-leave-btn {
  width: auto;
  border-radius: 0;
  padding: 0 22px;
  background: var(--danger);
  border-color: var(--danger);
  color: #1a0a0a;
  font-family: var(--mono);
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: 0.04em;
}

.zoom-leave-btn:hover {
  opacity: 0.85;
  background: var(--danger);
}

/* ================================================================
   MOBILE FEED (phone home screen)
   ================================================================
   The phone doesn't get a shrunk-down version of the desktop -- it gets
   its own native grammar: every app is a post in an endless vertical
   feed (see renderMobileFeed() in script.js), swipe sideways within a
   post to move through that app's own media (a work's video vs. its
   production stills), tap Open to drop into the exact same full-screen
   window every icon on this site already opens (see .window's mobile
   override above) -- this is a new front door onto that system, not a
   parallel one.

   Each card is its own photo, at that post's own aspect ratio (see
   --mfeed-ratio, set inline per post in script.js from the real photo's
   dimensions; falls back to a plain 4:5 box for posts with no photo of
   their own), always at its own natural size -- nothing here ever
   scales a post up or down. EXPERIMENTAL focus effect (see
   mfeedApplyWheelStyles() in script.js) instead: the feed scroll-snaps a
   post centered in the viewport, where it reads at full brightness;
   every other post just dims the further it drifts from that center, so
   the one in focus pops against a continuous, endless-feeling scroll of
   dimmed neighbors above and below rather than a series of isolated
   cards. Its poster (avatar + name, see .mfeed-userline) lives as its
   own sibling element in the plain white space BEFORE the photo, and its
   caption (title/body/actions, see .mfeed-caption-below) as another
   sibling in the plain white space AFTER it -- neither is ever overlaid
   on the image itself, which stays a clean, unobstructed photo
   throughout. Both siblings only fade into view within a narrow window
   right at focus, so an out-of-focus post reads as just its dimmed
   photo, nothing riding on top of or below it yet. */

/* everything below belongs to the phone home screen ONLY -- this whole
   section must stay inside the mobile breakpoint. It briefly wasn't
   (a wrapping @media (max-width: 780px) got dropped somewhere along the
   way while this section was being rewritten across several passes),
   which meant #mobile-feed sat as a full-viewport position:absolute,
   z-index:5 layer on TOP OF THE DESKTOP at any window width -- not
   visibly (it was empty there), but very much still there for hit-
   testing, silently eating every click meant for a desktop icon
   underneath it. jsdom-based tests never caught this because jsdom
   doesn't evaluate @media queries for layout at all; it only surfaced
   under a real browser. */
@media (max-width: 780px) {

/* ============================================================
   CLEAN FEED
   ============================================================
   A plain, legible feed in the site's own tokens -- no separate skin
   layered on top, so the mobile feed picks up the same clean white/black/
   deep-red system (and the brightness slider) as the rest of the site
   instead of opting out into its own palette. Nothing decorative rides
   along with a post's identity: no per-post tint, no glow, no scanline --
   just the photo (at its own aspect ratio, see .mfeed-media) and a plain
   caption strip underneath it. */
.mobile-feed {
  /* the rest of the site is deliberately monospace/terminal -- the feed's
     own caption text breaks from that on purpose, the same plain system
     UI face every real feed app sets its own type in, since a caption is
     read as a caption, not as "the OS talking" */
  --mfeed-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  /* EXPERIMENTAL wheel effect -- a post always settles centered in the
     viewport, never left resting halfway between two posts. That
     halfway-resting state is what "proximity" (tried first) allowed --
     nothing is ever fully in focus there, so the smallest wobble of
     momentum scroll flips which neighboring post reads as "nearest"
     back and forth, each flip repainting scale/opacity on both -- the
     actual jitter, not the per-frame math itself. "mandatory" removes
     that state outright: scroll is only ever mid-transit or settled
     exactly on a post, nothing in between to oscillate over. */
  scroll-snap-type: y mandatory;
  /* Chrome's scroll anchoring "helpfully" renudges scrollTop on its own
     the instant a post above the viewport changes height (exactly what
     the per-frame margin compensation below does) -- fighting the JS
     that's already driving this scroll-linked effect and reading as
     visible jitter. This opts the whole feed out of that so only the
     JS's own math ever moves anything. */
  overflow-anchor: none;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 14px 0 calc(env(safe-area-inset-bottom, 0px) + 14px);
  z-index: 5;
  background: var(--bg);
}

/* pull-to-refresh -- sits BEHIND #mobile-feed at the very top, height
   0 by default (nothing visible, nothing in the layout). As the visitor
   drags down from a scrollTop of 0, initMfeedPullToRefresh() (script.js)
   grows this element's height and translates #mobile-feed down by that
   same amount, which is what actually reveals it -- a plain height/
   translateY pairing rather than anything z-index-dependent, so it works
   out to the same reveal regardless of stacking order. */
.mfeed-pull-indicator {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  overflow: hidden;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--dim);
  font-family: var(--mfeed-font);
  font-size: calc(12px * var(--ui-scale));
  font-weight: 600;
  opacity: 0;
}
.mfeed-pull-indicator.is-armed {
  color: var(--accent);
}

.mfeed-post {
  position: relative;
  /* a floating card with a thin side margin, not full device width --
     hard square corners throughout, so .mfeed-media itself never needs to
     know anything about rounding */
  width: calc(100% - 24px);
  margin: 0 12px;
  border-radius: 0;
  border: 1px solid var(--divider);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 28px rgba(0, 0, 0, 0.07);
  /* a flex column shrinks its children to fit its own box by default --
     even with overflow-y: auto on .mobile-feed, flex-shrink defaults to
     1, so without this every post was getting proportionally squashed
     until all of them fit within one screen instead of the feed
     actually scrolling. flex-shrink: 0 makes each post keep its natural
     (aspect-ratio-driven) height and forces the feed to overflow/scroll
     the way it was always meant to. */
  flex-shrink: 0;
  overflow: hidden;
  background: var(--panel);
  /* EXPERIMENTAL focus effect -- every post stays its own natural size
     (a previous pass also scaled the focused one up and others down;
     dropped so the feed reads as one continuous, endless scroll rather
     than a series of isolated cards popping in and out of size). Nothing
     is set directly on .mfeed-post itself anymore -- its photo dims via
     .mfeed-media's own opacity, and its userline/caption text fades via
     their own opacity (see those rules below and
     mfeedApplyWheelStyles() in script.js), which is why THEIR
     transitions carry the easing instead of this element's. */
  /* settles centered in the viewport as scroll comes to rest (see
     scroll-snap-type on .mobile-feed above) -- that's what "focus" is
     now: whichever post scroll actually parks on */
  scroll-snap-align: center;
  /* forces even a fast flick to actually stop AT a post -- without this,
     "mandatory" above only guarantees scroll ENDS on a snap point, not
     that it can't blow past several in one motion first */
  scroll-snap-stop: always;
}

/* EXPERIMENTAL wheel effect: past a post's own "in focus" arc, its
   .mfeed-caption-below sits fully invisible and stops being tappable --
   toggled directly on that element (see mfeedApplyWheelStyles() in
   script.js) since it's a SIBLING of .mfeed-post, not a descendant, so a
   selector scoped through .mfeed-post can't reach it. There's no
   display:none step (comments/composer only ever render once opened by
   tapping the comment button, see mfeedCommentsPanelHTML() in script.js)
   -- just this, so an invisible button can't still eat a stray tap. */
.mfeed-caption-below.is-defocused {
  pointer-events: none;
}

/* CONTACT, SHARING AND SUPPORT, and SOFTWARE are framed as sponsored
   placements (see mfeedAdPostHTML() in script.js) -- a warm, slightly
   different card tint is the other half of that framing, so an ad reads
   as visually distinct from the personas' own posts even at a glance,
   not just via its badge. No .mfeed-media at all here (see
   mfeedAdPostHTML()'s own comment) -- extra top padding on the caption
   stands in for the space a photo would otherwise have opened up above it. */
.mfeed-post.is-ad {
  background: color-mix(in srgb, var(--accent) 5%, var(--panel));
  border-color: color-mix(in srgb, var(--accent) 20%, var(--divider));
}
.mfeed-post.is-ad .mfeed-caption {
  padding-top: 16px;
}

/* a plain "Sponsored" notice, small and quiet -- an ad's own equivalent
   of the poster info a real post's .mfeed-userline would show, just not
   applicable here since an ad has no persona/photo of its own */
.mfeed-ad-badge {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 12px;
  color: var(--dim);
  font-family: var(--mfeed-font);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.mfeed-ad-badge svg {
  width: 13px;
  height: 13px;
}

/* the app's own glyph standing in for a logo, next to the headline --
   what actually makes an ad card read as "a thing", not a fixed photo */
.mfeed-ad-heading {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 8px;
}
.mfeed-ad-icon {
  width: 42px;
  height: 42px;
  flex-shrink: 0;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--accent) 16%, var(--panel-2));
  color: var(--accent);
}
.mfeed-ad-icon svg {
  width: 56%;
  height: 56%;
}
.mfeed-ad-icon img {
  width: 56%;
  height: 56%;
  object-fit: contain;
}
.mfeed-ad-heading .mfeed-caption-title {
  margin: 0;
}

/* the media box's own aspect ratio varies per post (see --mfeed-ratio,
   set inline in script.js from each photo's real dimensions) instead of
   a single fixed crop for everything -- a square photo, a vertical
   portrait, and a landscape shot each keep their own real shape, the
   same varied look a real feed has. Posts with no photo of their own
   (a text slide, an icon tile) fall back to a plain 4:5 box. */
.mfeed-media {
  position: relative;
  width: 100%;
  /* always the real photo's own ratio, never cropped by an artificial
     height cap -- every real photo classifies into one of 4 buckets (see
     MOBILE_RATIO_BUCKETS in build_content.py: 1/1, 3/4, 4/3, 16/9), and
     the tallest of those (3/4) is nowhere near tall enough to threaten
     the "there's more above and below" peek of neighboring posts on a
     normal phone viewport, so there's nothing here left to guard against */
  aspect-ratio: var(--mfeed-ratio, 4 / 5);
  flex-shrink: 0;
  background: var(--panel-2);
  /* EXPERIMENTAL wheel effect -- opacity set per-frame from JS (see
     mfeedUpdateWheelTransforms()): the photo dims the further out of
     focus its post drifts, so the one post actually in focus visibly
     pops against its dimmed neighbors above and below */
  transition: opacity 0.12s linear;
}

/* who posted this -- a small rounded avatar plus name, living in the
   plain white space BEFORE the photo (see mfeedUserlineHTML() in
   script.js) as its own sibling flex item in #mobile-feed, not floated
   on top of the image anymore. Sits on the page's own background, so
   unlike the caption overlay still on the photo, it tracks the site's
   normal --text/--divider tokens instead of needing fixed light-on-dark
   colors. Fixed height regardless of focus (see .mfeed-userline below)
   -- only its opacity (driven per-frame from JS, see
   mfeedUpdateWheelTransforms()) changes, so the white space it lives in
   never itself resizes/jumps as a post scrolls toward or away from
   focus. See mfeedAuthorFor() in script.js for who actually gets
   credited. */
.mfeed-userline {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  margin: 0 12px;
  transition: opacity 0.12s linear;
}

.mfeed-avatar {
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px solid var(--divider);
}
.mfeed-avatar svg {
  width: 62%;
  height: 62%;
  color: #fff;
}
.mfeed-avatar-self img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.mfeed-userline-name {
  font-family: var(--mfeed-font);
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mfeed-track {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.mfeed-track::-webkit-scrollbar { display: none; }

/* the caption (title/body/tags/actions/comments) as its own sibling
   element AFTER .mfeed-post -- in the plain white space below the photo,
   not overlaid on it, so it tracks the site's normal --text/--dim tokens
   the same way .mfeed-userline above the photo does (see
   .mfeed-caption-title/-body/-tags/.mfeed-actions further below for
   those actual colors -- shared with the ad-post .mfeed-caption box,
   unchanged since ads never had a photo to overlay in the first place).
   Opacity is driven per-frame from JS (see mfeedUpdateWheelTransforms())
   within a narrow window right at focus, so it only ever appears gently,
   right as a post settles into place -- never on an out-of-focus, still-
   compressed post. */
.mfeed-caption-below {
  padding: 10px 16px 4px;
  color: var(--text);
  font-family: var(--mfeed-font);
  transition: opacity 0.12s linear;
}

.mfeed-slide {
  position: relative;
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  scroll-snap-align: start;
  overflow: hidden;
  background: color-mix(in srgb, var(--panel-2) 80%, black 20%);
}

.mfeed-slide img,
.mfeed-slide iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: none;
}

/* the video is sized like THE FILM's (see .film .film-yt): taller than
   the slide by a margin above and below, which is where YouTube puts its
   title card and logo, so they are cut off -- and it never takes a touch,
   so it never draws them on hover either */
.mfeed-slide iframe {
  inset: auto;
  left: 0;
  top: calc(-1 * max(64px, 34%));
  height: calc(100% + 2 * max(64px, 34%));
  pointer-events: none;
}

/* the poster is what's actually visible the whole time a video slide is
   loading -- explicit z-index because the iframe mounts AFTER it in the
   DOM (later sibling paints on top by default) and needs to stay hidden
   underneath until mfeedPlayVideo() (script.js) confirms it's ready, not
   the other way around. Only .is-playing hides the poster and reveals
   the iframe; .is-loading is the brief window between tap and that. */
.mfeed-video-poster {
  z-index: 2;
}
.mfeed-slide.is-playing .mfeed-video-poster {
  display: none;
}

/* a quiet pulse on the poster itself while a tapped video connects --
   there's still a brief real load happening, this just gives it
   something calmer to look like than a hard cut to black */
.mfeed-slide.is-loading .mfeed-video-poster {
  animation: mfeed-video-loading-pulse 1.1s ease-in-out infinite;
}
@keyframes mfeed-video-loading-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}
.reduce-motion .mfeed-slide.is-loading .mfeed-video-poster {
  animation: none;
}

/* posts with no photograph of their own (CALENDAR, CONTACT, SOFTWARE)
   get a plain icon tile instead -- same glyph-on-tint language as the
   desktop icons, not a fake stock image standing in for content that
   doesn't exist. Every post's title/description/meta live on their own
   matching slide right after it in the same track -- text is something
   you swipe to see here too, not a caption sitting under a photo. Flat
   and neutral on purpose -- a plain tinted panel, not a scene. */
.mfeed-slide.mfeed-tile,
.mfeed-slide.mfeed-text-slide {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-2);
}
.mfeed-slide.mfeed-tile svg {
  position: relative;
  z-index: 1;
  width: 30%;
  height: 30%;
  max-width: 84px;
  max-height: 84px;
  color: var(--dim);
  opacity: 0.9;
}
.mfeed-slide.mfeed-tile img {
  position: relative;
  z-index: 1;
  width: 30%;
  height: 30%;
  max-width: 84px;
  max-height: 84px;
  object-fit: contain;
  opacity: 0.9;
}

.mfeed-text-inner {
  position: relative;
  z-index: 1; /* above the grid-horizon ::before/::after on the slide itself */
  width: 100%;
  height: 100%;
  /* no scroll inside a post, ever -- only the feed itself scrolls. A
     paragraph long enough to risk overflowing here has already been
     split into several of these slides instead (see mfeedChunkText() /
     MFEED_TEXT_CHUNK_MAX in script.js), so this is a hard backstop, not
     the thing actually doing the fitting. */
  overflow: hidden;
  padding: 36px 26px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* a real social-app "text post" treatment -- big, bold, and (on the
   first slide of a paragraph only, see .mfeed-text-lead) a large
   stylized opening quote mark standing in for a graphic flourish real
   apps would use a template/sticker for. Left-aligned rather than
   centered: these are full paragraphs, not one-line quotes, and
   centering multi-line prose of uneven width reads as broken, not
   stylish. Three size tiers (set in script.js by chunk length) instead
   of one fixed size, so a short leftover sentence still reads as a bold
   text-post while a chunk closer to the chunking cap steps down to
   whatever actually fits its box without scrolling. */
.mfeed-text-body {
  margin: 0;
  font-family: var(--mono);
  color: var(--text);
}
.mfeed-text-body.mfeed-text-lg {
  font-size: calc(20px * var(--ui-scale));
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: -0.01em;
}
.mfeed-text-body.mfeed-text-md {
  font-size: calc(16.5px * var(--ui-scale));
  font-weight: 700;
  line-height: 1.42;
  letter-spacing: -0.008em;
}
.mfeed-text-body.mfeed-text-sm {
  font-size: calc(13.5px * var(--ui-scale));
  font-weight: 600;
  line-height: 1.46;
}
.mfeed-text-body.mfeed-text-lead::before {
  content: "“";
  display: block;
  font-size: 2.4em;
  line-height: 0.6;
  margin-bottom: 0.2em;
  color: var(--accent);
}

/* the tap-to-load facade -- a plain centered circle, nothing about it
   should read as a third-party embed. Disappears the instant it's
   tapped (see .is-loading/.is-playing below), not lingering through
   the load. */
.mfeed-play-btn {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  z-index: 4;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  cursor: pointer;
  padding: 0 0 0 3px; /* optically centers the triangle glyph */
  transition: transform 0.12s ease, background 0.15s ease;
}
.mfeed-play-btn:active {
  transform: translate(-50%, -50%) scale(0.92);
  background: color-mix(in srgb, var(--panel-2) 80%, transparent);
}
.mfeed-slide.is-loading .mfeed-play-btn,
.mfeed-slide.is-playing .mfeed-play-btn {
  display: none;
}

/* per-post carousel position, tracking which slide is showing within
   THAT post's own horizontal media track -- small dots, one per slide,
   marking position only. Deliberately NOT a progress/story-style bar:
   nothing here is being "watched through" the way a story is, it's a
   swipeable set of photos, so filling a track toward completion would
   be the wrong metaphor -- just mark which dot is current. */
.mfeed-segbar {
  position: absolute;
  bottom: 12px;
  left: 0;
  right: 0;
  z-index: 6;
  display: flex;
  justify-content: center;
  gap: 5px;
  pointer-events: none;
}
.mfeed-seg {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
  transition: background 0.2s ease, transform 0.2s ease;
}
.mfeed-seg.is-active {
  background: #fff;
  transform: scale(1.3);
}

/* the real Instagram-caption shape, no app-name label anymore -- just a
   bold title ("the name of the work itself", not the app name), a body
   line built from that work's own real info, and a hashtag line. Only
   the parts a given post actually has render at all (bio, for instance,
   has no title/body of its own here -- its content is the swipeable
   text slides above, so it's just a small hashtag line). Which program
   a post belongs to is still available to assistive tech via the
   role="group" aria-label on .mfeed-post itself (see mfeedProgramName()
   in script.js), just not shown visually. */
/* only ad posts (see mfeedAdPostHTML() in script.js) still use this --
   a real post's own caption lives in .mfeed-caption-below instead (see
   above); an ad has no persona/photo of its own, so it keeps this plain
   strip, always fully visible (no wheel-effect fade). */
.mfeed-caption {
  padding: 12px 16px 16px;
  color: var(--text);
  /* the whole caption strip (title/body/tags/CTA) reads in the feed's own
     plain system-UI face, not the rest of the site's monospace -- set
     once here and inherited, rather than repeated on every child rule
     below */
  font-family: var(--mfeed-font);
}
.mfeed-caption-title {
  margin: 0 0 4px;
  font-size: calc(14.5px * var(--ui-scale));
  font-weight: bold;
  letter-spacing: -0.005em;
  color: var(--text);
}
.mfeed-caption-body {
  margin: 0 0 8px;
  font-size: calc(13px * var(--ui-scale));
  line-height: 1.5;
  color: var(--text);
  opacity: 0.85;
}
/* clamped to 2 lines until mfeedCaptionBodyHTML()'s "more" button (see
   wireMobileFeedPost() in script.js) lifts it on tap -- a caption long
   enough to need this stays out of the photo's way until asked for */
.mfeed-caption-body.is-clamped {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mfeed-caption-more {
  display: block;
  margin: -4px 0 8px;
  border: none;
  background: none;
  padding: 0;
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: 700;
  color: var(--dim);
  cursor: pointer;
}
.mfeed-caption-tags {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--dim);
}
.mfeed-caption-time {
  margin: 0;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--dim);
  opacity: 0.7;
}

/* like/comment/share now live in the caption strip, not floating over
   the photo -- a plain icon row, the real feed-app shape, sized for an
   ergonomic thumb target without needing a circular glass button to get
   there since it's not sitting on top of arbitrary photo content anymore */
.mfeed-actions {
  display: flex;
  align-items: center;
  gap: 18px;
  margin: 0 0 10px;
}
.mfeed-actions button {
  border: none;
  background: none;
  padding: 4px;
  margin: -4px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  cursor: pointer;
  transition: transform 0.12s ease, opacity 0.15s ease;
}
.mfeed-actions button:active {
  transform: scale(0.88);
  opacity: 0.7;
}

/* the ad's own call to action (see mfeedAdCtaHTML() in script.js) -- a
   real filled button, not another plain text link, so it doesn't get
   lost among the caption/tags/comments around it */
.mfeed-cta-btn {
  display: block;
  width: 100%;
  margin: 2px 0 12px;
  padding: 10px 14px;
  border: none;
  border-radius: 0;
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--mfeed-font);
  font-size: calc(13px * var(--ui-scale));
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.12s ease, opacity 0.15s ease;
}
.mfeed-cta-btn:active {
  transform: scale(0.98);
  opacity: 0.9;
}

/* comments/composer/time now live collapsed here by default (see
   mfeedCommentsPanelHTML() in script.js) -- hidden entirely, not just
   scrolled past, until the visitor taps the comment action button (see
   wireMobileFeedPost()), which removes .is-collapsed. For a real post
   this nests inside .mfeed-caption-below (which already carries its own
   horizontal padding, hence none here); for an ad post it nests directly
   in .mfeed-caption instead, same reasoning. */
.mfeed-comments-panel {
  padding: 12px 0 8px;
  color: var(--text);
  font-family: var(--mfeed-font);
  border-top: 1px solid var(--divider);
}
.mfeed-comments-panel.is-collapsed {
  display: none;
}

.mfeed-comments {
  margin: 0 0 8px;
}
/* "View all N comments" -- shown only when there's more than one item
   (a Q&A thread or a plain comment, each counted whole) to preview.
   Tapping removes .is-collapsed from .mfeed-comments-list below and
   removes this button entirely (see wireMobileFeedPost() in script.js),
   the same one-way reveal .mfeed-caption-more already uses. */
.mfeed-comments-toggle {
  display: block;
  margin: 0 0 6px;
  border: none;
  background: none;
  padding: 0;
  font-family: var(--mfeed-font);
  font-size: calc(12px * var(--ui-scale));
  font-weight: 600;
  color: var(--dim);
  cursor: pointer;
}
.mfeed-comments-list {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* preview: only the first item (thread or plain comment) stays visible */
.mfeed-comments-list.is-collapsed > :not(:first-child) {
  display: none;
}
.mfeed-comment-thread {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.mfeed-comment {
  margin: 0;
  font-size: calc(12.5px * var(--ui-scale));
  line-height: 1.45;
  color: var(--text);
}
.mfeed-comment-name {
  font-weight: 700;
  margin-right: 4px;
}
/* the answer half of a Q&A thread -- indented under its own question, a
   real reply rather than just the next line down, with a short vertical
   connector so the pairing reads at a glance */
.mfeed-comment-reply {
  position: relative;
  margin-left: 18px;
  padding-left: 10px;
  border-left: 2px solid var(--divider);
}

/* a visitor's own comment (see mfeedAddComment() in script.js) posts
   under whichever persona this browser session was logged in as -- an
   always-present composer, not a toggle, the same "just type" affordance
   a real feed's own comment field gives */
.mfeed-composer {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 2px 0 10px;
  padding-top: 10px;
  border-top: 1px solid var(--divider);
}
.mfeed-composer-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  font-family: var(--mfeed-font);
  font-size: calc(12.5px * var(--ui-scale));
  color: var(--text);
}
.mfeed-composer-input::placeholder {
  color: var(--dim);
}
.mfeed-composer-input:focus {
  outline: none;
}
.mfeed-composer-send {
  flex-shrink: 0;
  border: none;
  background: none;
  padding: 0;
  font-family: var(--mfeed-font);
  font-size: calc(12.5px * var(--ui-scale));
  font-weight: 700;
  color: var(--accent);
  cursor: pointer;
}

/* liked state -- the icon fills solid (see MFEED_LIKE_ICONS in
   script.js) in the same red every real feed app's heart turns */
.mfeed-like-btn.is-liked {
  color: #ff3040;
}
.mfeed-like-btn.mfeed-pop {
  animation: mfeed-like-pop 0.32s ease-out;
}
@keyframes mfeed-like-pop {
  0% { transform: scale(1); }
  35% { transform: scale(1.28); }
  100% { transform: scale(1); }
}
.reduce-motion .mfeed-like-btn.mfeed-pop {
  animation: none;
}

/* the video sound toggle lives on the media itself (bottom-left of the
   video slide, clear of the rail on the right) -- same circular glass
   button language as the rail, so it reads as one family of controls */
.mfeed-mute-btn {
  position: absolute;
  left: 12px;
  bottom: 12px;
  z-index: 8;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  cursor: pointer;
  padding: 0;
  transition: transform 0.12s ease, background 0.15s ease;
}
.mfeed-mute-btn:active {
  transform: scale(0.88);
  background: color-mix(in srgb, var(--panel-2) 80%, transparent);
}

.icons,
.mobile-home-heading {
  display: none;
}

} /* close @media (max-width: 780px) opened at the top of the MOBILE FEED section */

@media (prefers-reduced-motion: reduce) {
  .mobile-feed,
  .mfeed-track {
    scroll-behavior: auto;
  }
}

/* =========================================================================
   EXPERIMENTAL: "Amiga Workbench -- Sunset" OS chrome restyle -- desktop
   test pass for the design-exploration round (dithered violet desktop,
   hollow drag-bar titlebars, boot/login screen restyled to match). Additive/
   override-only and self-contained, so it can be deleted wholesale (or the
   whole file reverted with `git checkout`) to go back cleanly.
   ========================================================================= */

/* ---- day/night tokens ----
   The luminosity toggle used to drive --bg/--text/--accent etc. (see
   applyBrightness() in index.html), but this theme's surfaces are all
   hand-picked hex colors, so the toggle had nothing left to move -- it
   looked broken. setBrightness() in script.js now also flips `.os-dark`
   on <html>, and these tokens give it something real to switch: a violet
   "dusk" (dark) and a lavender "day" (light) pair. Borders/titlebars stay
   fixed black in both -- only the desktop/menu-bar/taskbar/window-fill
   surfaces and their warm text actually invert. */
:root {
  --sunset-bar: #f4e8d8;
  --sunset-field: #d9c6ea;
  --sunset-warm: #3d0a7a;
}
.os-dark {
  --sunset-bar: #1a0a2a;
  --sunset-field: #7a4a9a;
  --sunset-warm: #ffd400;
}

/* ---- boot/login screen ----
   the same photograph the desktop is about to show (see pickDesktopPhoto),
   as it is, with the login box over it */
#boot-screen {
  background-color: #000;
  background-image: var(--bg-photo-url, none);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}
.boot-center {
  position: relative;
  background: var(--sunset-field);
  border: 3px solid #1a0a2a;
  padding: 42px 44px 30px;
  box-shadow: 7px 7px 0 rgba(0,0,0,0.45);
}
.boot-center::before {
  content: "LOGIN";
  position: absolute;
  top: -3px;
  left: -3px;
  right: -3px;
  height: 24px;
  background: #0a0a0a;
  color: #ffd400;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  display: flex;
  align-items: center;
  justify-content: center;
  border-bottom: 2px solid #000;
}
.boot-avatar {
  border: 2px solid #1a0a2a;
  background: #ff1d6c;
  border-radius: 0;
}
.boot-avatar svg { color: #1a0a2a; }
.boot-signing-label { color: var(--sunset-warm); }
.boot-username { color: var(--sunset-warm); }
.boot-progress-track { background: #1a0a2a; border: 1px solid #1a0a2a; }
.boot-progress-fill { background: #ff6200; }
.boot-line { color: var(--sunset-warm); }
.boot-login-btn {
  background: #ff6200;
  color: #1a0a2a;
  border: 2px solid #1a0a2a;
  font-weight: 700;
  box-shadow: 3px 3px 0 rgba(0,0,0,0.4);
}
.boot-login-btn:hover {
  background: #ff1d6c;
  color: #1a0a2a;
}

/* ---- desktop: dithered violet Workbench field ---- */
#desktop.bg-amiga::before {
  opacity: 1;
  background-color: var(--sunset-field);
  background-image:
    repeating-linear-gradient(45deg, transparent 0 2px, rgba(0,0,0,0.14) 2px 4px),
    linear-gradient(180deg, rgba(255,140,60,0.16), transparent 55%);
}

/* ---- menu bar / taskbar: dark navy-violet with warm text ---- */
.menu-bar {
  background: var(--sunset-bar);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 2px solid #000;
}
.menu-welcome,
.menu-datetime,
.menu-clock,
.menu-date {
  color: var(--sunset-warm) !important;
  font-weight: 700;
}
/* "ghost" buttons -- no box at all, just a colored glyph; the active/
   selected state is a thin underline instead of a filled chip */
.menu-icon-btn {
  background: transparent;
  border: none;
  color: var(--sunset-warm);
}
.menu-icon-btn:hover {
  background: transparent;
  border-color: transparent;
  color: #ff6200;
}
#brightness-toggle.active,
#brightness-toggle.active:hover {
  background: transparent;
  border-color: transparent;
  color: #ff6200;
  box-shadow: inset 0 -2px 0 #ff6200;
}
#lang-toggle {
  background: transparent;
  color: #ff6200;
  border: none;
  font-weight: 700;
  box-shadow: inset 0 -2px 0 #ff6200;
}

.taskbar {
  background: var(--sunset-bar);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-top: 2px solid #000;
}
.taskbar-searchbox {
  background: transparent;
  border: none;
  color: var(--sunset-warm);
  font-weight: 700;
}
.taskbar-searchbox:hover {
  color: #ff6200;
}
.taskbar-item {
  background: transparent;
  border: none;
  color: var(--sunset-warm);
  box-shadow: none;
}
.taskbar-item:hover {
  color: #ff6200;
}
.taskbar-item.active {
  background: transparent;
  color: #ff6200;
  border: none;
  box-shadow: inset 0 -2px 0 #ff6200;
}

/* ---- desktop icons: no label background at all -- just colored text --
   plus a hand-drawn square behind the glyph instead of a hard tile.
   Three variants cycle through the icon grid so it doesn't read as one
   stamped-out shape repeated everywhere. */
.icon-label {
  background: transparent;
  color: var(--sunset-warm);
  font-family: var(--mono);
}
.icon.selected .icon-label {
  background: transparent;
  color: #ff6200;
}
.icon-glyph {
  position: relative;
}
/* hand-drawn SQUARES behind the glyphs, not blobs -- the site is angular.
   Full-strength colours (hot pink, vivid orange, electric yellow; teal for
   the trash), not the pastel versions -- bold enough to hold their own
   against the violet desktop.
   Each is a filled, slightly skewed box (uneven corners, a small tilt) with
   a second, unfilled outline over it at a different tilt, the way a square
   drawn by hand gets gone over twice and the two passes never line up. */
.icon-glyph::before,
.icon-glyph::after {
  content: "";
  position: absolute;
  inset: -6px;
  z-index: -1;
  border: 2px solid #1a0a2a;
}
.icon-glyph::after {
  background: none;
  border-width: 1.5px;
}
.icons .icon:nth-of-type(3n+1) .icon-glyph::before {
  border-radius: 3px 6px 2px 5px / 5px 2px 6px 3px;
  transform: rotate(-2.5deg) skewX(1deg);
}
.icons .icon:nth-of-type(3n+1) .icon-glyph::after {
  border-radius: 5px 2px 4px 3px / 2px 5px 3px 6px;
  transform: translate(1px, -1px) rotate(1.8deg);
}
.icons .icon:nth-of-type(3n+2) .icon-glyph::before {
  border-radius: 6px 2px 5px 3px / 3px 6px 2px 5px;
  transform: rotate(2deg) skewY(-1deg);
}
.icons .icon:nth-of-type(3n+2) .icon-glyph::after {
  border-radius: 2px 5px 3px 6px / 6px 3px 5px 2px;
  transform: translate(-1px, 1px) rotate(-1.5deg);
}
.icons .icon:nth-of-type(3n) .icon-glyph::before {
  border-radius: 4px 3px 6px 2px / 2px 5px 3px 6px;
  transform: rotate(-1.2deg) skewX(-1.5deg);
}
.icons .icon:nth-of-type(3n) .icon-glyph::after {
  border-radius: 3px 6px 2px 4px / 5px 2px 6px 3px;
  transform: translate(1px, 1px) rotate(2.4deg);
}
/* trash reads as its own thing, not just the next color in the 3-cycle --
   a cooler teal keeps it from being mistaken for a regular app icon */
.icon-trash .icon-glyph::before {
  border-radius: 5px 3px 6px 2px / 3px 6px 2px 5px !important;
  transform: rotate(1.6deg) skewX(1deg) !important;
}
/* ---- ONE COLOUR PER APP, NONE REPEATED ----
   ten apps, ten hues spread round the wheel, each chosen against its own
   icon art so the picture pops (the yellow folder on violet, the blue
   calendar and zoom icons on orange and red). The same colour follows the
   app into the side panel (see .appnav-item below). */
:root {
  --app-c-bio: #ff1d6c;
  --app-c-works: #8a2eff;
  --app-c-software: #ffd400;
  --app-c-contact: #00c853;
  --app-c-calendar: #ff7a00;
  --app-c-sharing_and_support: #9be000;
  --app-c-social: #00c8d7;
  --app-c-cv: #1f6bff;
  --app-c-zoom: #ff2b2b;
  --app-c-trash: #e81cff;
}
.icon[data-app="bio"] .icon-glyph::before { background: var(--app-c-bio); }
.icon[data-app="works"] .icon-glyph::before { background: var(--app-c-works); }
.icon[data-app="software"] .icon-glyph::before { background: var(--app-c-software); }
.icon[data-app="contact"] .icon-glyph::before { background: var(--app-c-contact); }
.icon[data-app="calendar"] .icon-glyph::before { background: var(--app-c-calendar); }
.icon[data-app="sharing_and_support"] .icon-glyph::before { background: var(--app-c-sharing_and_support); }
.icon[data-app="social"] .icon-glyph::before { background: var(--app-c-social); }
.icon[data-app="cv"] .icon-glyph::before { background: var(--app-c-cv); }
.icon[data-app="zoom"] .icon-glyph::before { background: var(--app-c-zoom); }
.icon[data-app="trash"] .icon-glyph::before { background: var(--app-c-trash); }
.icon-glyph img {
  position: relative;
  filter: none;
}
.icon:hover .icon-glyph,
.icon.selected .icon-glyph {
  filter: none;
}

/* ---- window chrome: violet fill, dark border, solid black titlebar ----
   the hollow drag-bar stripe pattern read inconsistently once per-app
   titlebars (BIO's own dark terminal bar, etc.) sat next to the generic
   striped ones -- solid black everywhere reads as one consistent chrome
   language instead of "some windows got the treatment, some didn't". */
.window {
  background: transparent;
  border: none;
  box-shadow: 6px 6px 0 rgba(0,0,0,0.4);
}
.window.maximized {
  border-width: 0 0 0 0;
  box-shadow: none;
}
.window-titlebar {
  background-color: #0a0a0a;
  background-image: none;
  border-bottom: 2px solid #000;
  filter: none !important;
}
.window-title {
  background: transparent;
  color: #ffd400;
  padding: 2px 8px;
}
.window-title .dot {
  color: #ff1d6c;
}
.window-controls {
  background: transparent;
  padding: 2px 4px;
}
.window-controls button {
  background: #ff1d6c;
  border: 2px solid #1a0a2a;
  width: 16px;
  height: 16px;
  color: #1a0a2a;
  display: grid;
  place-items: center;
}
/* black buttons, with the drawings themselves in the macOS traffic-light
   colors: red close, yellow minimize, green maximize */
@media (min-width: 781px) {
  .window-controls button { background: #0a0a0a; }
  .window-controls .btn-close { color: #ff5f57; }
  .window-controls .btn-min { color: #febc2e; }
  .window-controls .btn-max { color: #28c840; }
  .window-controls button:hover { filter: brightness(1.2); }
}
.window-controls button img { display: none; }
/* hand-drawn -, square, X (see sketchControlGlyph() in script.js) --
   allowed to spill a hair past the button's edge like a real pen stroke */
.window-controls .ctl-sketch {
  width: 14px;
  height: 14px;
  overflow: visible;
  pointer-events: none;
}
@media (max-width: 780px) {
  .window-controls .ctl-sketch { display: none; }
}
.window-main,
.window-body {
  background: var(--sunset-field);
}
/* no frame: the body runs edge to edge under the titlebar, with no violet
   margin or outline around it */
.window-body {
  background: #f4e8d8;
  color: #2a1c10;
  margin: 0;
  border: none;
  width: 100%;
}

/* ---- popup menus (language/sound/display dropdowns, right-click menu,
   the "logged in as" hover menu): translucent violet-glass panel with a
   small colored blob bullet per row -- the same pink/orange/cream family
   as the icon backdrops -- standing in for a background hover fill. ---- */
.dropdown-menu,
.context-menu {
  background: rgba(26, 10, 42, 0.9);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 2px solid #1a0a2a;
  box-shadow: 0 10px 24px rgba(0,0,0,0.45) !important;
}

.dropdown-item,
.context-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #f4e8d8;
  font-weight: 700;
}
/* the marker before each row: a small hand-drawn square, not a blob (the
   desktop icons are squares now), cycling through five of the icons' own
   saturated colours so a menu reads as part of the same desktop */
.dropdown-item::before,
.context-menu-item::before {
  content: "";
  flex-shrink: 0;
  width: 9px;
  height: 9px;
  background: #ff1d6c;
  border-radius: 1px 3px 1px 2px / 2px 1px 3px 1px;
  transform: rotate(-4deg);
}
.dropdown-item:nth-of-type(5n+2)::before,
.context-menu-item:nth-of-type(5n+2)::before {
  background: #ff6200;
  border-radius: 3px 1px 2px 1px / 1px 3px 1px 2px;
  transform: rotate(3deg);
}
.dropdown-item:nth-of-type(5n+3)::before,
.context-menu-item:nth-of-type(5n+3)::before {
  background: #ffd400;
  transform: rotate(-2deg);
}
.dropdown-item:nth-of-type(5n+4)::before,
.context-menu-item:nth-of-type(5n+4)::before {
  background: #00c8d7;
  border-radius: 2px 1px 3px 1px / 1px 2px 1px 3px;
  transform: rotate(5deg);
}
.dropdown-item:nth-of-type(5n)::before,
.context-menu-item:nth-of-type(5n)::before {
  background: #9be000;
  transform: rotate(-5deg);
}

.dropdown-item:hover,
.context-menu-item:hover {
  background: transparent;
  color: #fff;
}
.dropdown-item:hover::before,
.context-menu-item:hover::before {
  filter: brightness(1.25);
}

.context-menu-item.disabled {
  color: #a8a0b0;
}
.context-menu-item.disabled::before {
  background: #6a5a78 !important;
}
.context-menu-item.disabled:hover {
  background: transparent;
  color: #a8a0b0;
}

.context-menu-sep {
  background: rgba(244, 232, 216, 0.2);
}

.dropdown-label {
  color: var(--sunset-warm, #ffd400);
}

.hover-dropdown .user-hover-label {
  color: #c9b8d8;
}
.hover-dropdown .user-hover-name {
  color: #f4e8d8;
}

/* ---- controls INSIDE the redesigned dropdowns (sound/settings) --
   first pass gave .mobile-chip/.font-select a flat black-bordered
   rectangle + drop shadow, which is exactly the "straight lines" chrome
   the rest of this theme deliberately avoids -- redone in the same
   hand-drawn language as the icons: an irregular, slightly tilted SQUARE
   ::before behind the label (they were organic blobs until the icons went
   square), ghost/outline when idle, filled once picked. Three variants
   cycle by position so a row of chips doesn't read as one shape stamped
   three times. */
.dropdown-menu .mobile-settings-row {
  color: #f4e8d8;
  border-bottom-color: rgba(244, 232, 216, 0.2);
}

.dropdown-menu .mobile-chip {
  position: relative;
  background: transparent;
  color: #f4e8d8;
  border: none;
  z-index: 0;
}
.dropdown-menu .mobile-chip::before {
  content: "";
  position: absolute;
  inset: -3px -5px;
  z-index: -1;
  background: transparent;
  border: 2px solid transparent;
  /* a hand-drawn square, not a blob -- same shape language as the icons */
  border-radius: 2px 4px 1px 3px / 3px 1px 4px 2px;
  transform: rotate(-1.6deg);
  transition: border-color 0.15s ease;
}
.dropdown-menu .mobile-chip:nth-of-type(3n+2)::before {
  border-radius: 4px 1px 3px 2px / 1px 4px 2px 3px;
  transform: rotate(1.4deg);
}
.dropdown-menu .mobile-chip:nth-of-type(3n)::before {
  border-radius: 1px 3px 2px 4px / 3px 2px 4px 1px;
  transform: rotate(-0.8deg);
}
.dropdown-menu .mobile-chip:hover {
  color: #ff6200;
}
.dropdown-menu .mobile-chip:hover::before {
  border-color: #ff6200;
}
.dropdown-menu .mobile-chip.active {
  color: #1a0a2a;
  font-weight: 700;
}
.dropdown-menu .mobile-chip.active::before {
  background: #ff6200;
  border-color: #1a0a2a;
}

.dropdown-menu .font-select {
  background: #1a0a2a;
  color: #f4e8d8;
  border: 2px solid #1a0a2a;
  border-radius: 2px 4px 2px 3px / 3px 2px 4px 2px;
  padding: 7px 10px;
}
.dropdown-menu .font-select:hover,
.dropdown-menu .font-select:focus {
  border-color: #ff6200;
}
.dropdown-menu .font-select option {
  background: #1a0a2a;
  color: #f4e8d8;
}

.dropdown-menu input[type="range"] {
  accent-color: #ff6200;
}

/* ---- taskbar date/time chip -- same fix as the dropdown chips above:
   dropped the plain grey OS-default box, but instead of leaving it as
   bare ghost text, it gets its own hand-drawn square backdrop (matching
   the icons) so it still reads as "a control" rather than
   vanishing into the taskbar entirely. ---- */
.menu-datetime {
  position: relative;
  background: transparent;
  border: none;
  padding: 6px 10px;
  z-index: 0;
}
.menu-datetime::before {
  content: "";
  position: absolute;
  inset: 1px 0;
  z-index: -1;
  background: rgba(244, 232, 216, 0.14);
  border: 2px solid #1a0a2a;
  /* a hand-drawn square, not a blob */
  border-radius: 2px 4px 1px 3px / 3px 1px 4px 2px;
  transform: rotate(-1deg);
}
.menu-date:hover {
  color: #ff6200;
}


/* ==========================================================================
   BARE WORK PAGE  —  index.html#work=<id>
   One work's page, full bleed, no desktop around it. See bareWorkBoot() in
   script.js for why this exists: the phone frames this page rather than
   owning a second copy of it.

   Everything here is scoped under .bare-work on <html>, so none of it can
   reach the desktop. The approach is subtractive on purpose -- hide every
   child of #desktop EXCEPT the window layer, rather than listing the parts
   to keep -- so a new piece of desktop chrome added later is hidden here by
   default instead of appearing over the work page unannounced.
   ========================================================================== */
html.bare-work, html.bare-work body {
  margin: 0; padding: 0; overflow: hidden; background: #fff;
}
/* the login screen must be gone in the FIRST painted frame, which is why
   the class is set by an inline script in index.html's <head> rather than
   by script.js at the end. display:none here is what that class buys. */
html.bare-work #boot-screen,
html.bare-work #taskbar,
html.bare-work #focus-backdrop,
html.bare-work #desktop > *:not(#windows-layer) { display: none !important; }

html.bare-work #desktop {
  position: fixed; inset: 0; background: #fff;
}
html.bare-work #windows-layer { position: fixed; inset: 0; }

/* the window becomes the page: no frame, no title bar, no shadow, no drag */
html.bare-work .window {
  position: fixed !important; inset: 0 !important;
  width: 100% !important; height: 100% !important;
  transform: none !important; border: 0 !important; border-radius: 0 !important;
  box-shadow: none !important; background: #fff;
}
html.bare-work .window .title-bar,
html.bare-work .window .window-titlebar,
html.bare-work .window .window-controls,
html.bare-work .window-resize-handle { display: none !important; }
html.bare-work .app-content { height: 100%; }

/* Every app's own scrollable content fills the frame. Each of these apps
   was written to sit inside a window with a fixed height, so without this
   the content runs off the bottom with nothing to scroll. */
html.bare-work .app-content { overflow-y: auto; -webkit-overflow-scrolling: touch; }
/* the mail client's decorative inbox is three panes of dressing for a
   window that is not here -- on a phone it would be most of the screen */
html.bare-work .mail-sidebar { display: none !important; }
html.bare-work .mail-body { display: block !important; height: 100%; }

/* WORKS.EXE's own chrome -- the library sidebar, the list/gallery toggle --
   is navigation for a desktop that is not here */
html.bare-work .works-sidebar,
html.bare-work .works-toolbar,
html.bare-work #works-list-view,
html.bare-work .work-hover-panel { display: none !important; }
html.bare-work .works-app, html.bare-work .works-body { height: 100%; }
/* the window's own padding and frame colours -- .window-main is the purple
   plate the desktop window sits on, and .works-main insets its content by
   22px so the app does not touch the frame. Neither has a frame to sit in
   here, and both were showing as a coloured border around the page. */
html.bare-work .window-main { background: #fff !important; padding: 0 !important; }
/* THE WHITE RING. .window-body carries a 6px margin so that, on the
   desktop, the app floats a little inside its own frame -- and the white
   .window-main showing through that margin is exactly the border seen
   around the bio programme on the phone. There is no frame here for it to
   float inside, so the margin has nothing to do but leak. The inner padding
   stays: that is the app's own breathing room, not a border, and it takes
   the app's colour rather than the window's. */
html.bare-work .window-body { border: 0 !important; margin: 0 !important; }
html.bare-work .window, html.bare-work .window-main { background: transparent !important; }
html.bare-work .works-main { width: 100%; height: 100%; padding: 0 !important; }
html.bare-work .works-detail {
  height: 100%; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 0 8vh;
}
/* back / lecture / next belong to the desktop's own way around the works.
   Whatever is framing this page owns getting out of it. */
html.bare-work .work-detail-nav { display: none !important; }

/* THE SHARE ROW ARRIVES WITH THE PAGE, NOT AFTER IT. Six <img> at their
   natural size, fetched only once the markup that references them exists --
   so the row laid itself out empty, then jumped as each icon landed. A
   fixed box for each reserves the space before any of them arrive, and the
   <link rel=preload> hints in index.html's head start the fetches at the
   same moment the document does. */
html.bare-work .work-share-icon { width: 30px; height: 30px; display: inline-flex;
  align-items: center; justify-content: center; }
html.bare-work .work-share-icon img { width: 26px; height: 26px; object-fit: contain; }

/* it is being read on a phone, held in one hand */
@media (max-width: 700px) {
  html.bare-work .work-detail-page { padding-left: 18px; padding-right: 18px; }
  html.bare-work .work-detail-title { font-size: clamp(30px, 9vw, 56px); }
  html.bare-work .work-detail-description { font-size: 16px; line-height: 1.62; }
  html.bare-work .work-detail-quote { font-size: clamp(19px, 5.4vw, 30px); }
}

/* =========================================================================
   BIO.EXE, A PROGRAM (desktop only -- the phone's own BIO layout is the
   html.bare-work rules above and is not touched)
   The terminal is gone: no shell prompt, no blinking cursor, no typing.
   BIO.EXE is a whole retro program window in the language its slider was
   already speaking -- grey Win95 chrome, a navy title bar, a menu bar, the
   level-of-detail slider as its toolbar, the text in a sunken white
   document, a status bar along the foot.
   ========================================================================= */
html:not(.bare-work) .window[data-app="bio"] {
  --w95-face: #c0c0c0;
  --w95-light: #ffffff;
  --w95-shadow: #808080;
  --w95-dark: #404040;
  --w95-font: Tahoma, Geneva, "MS Sans Serif", "Segoe UI", sans-serif;
  box-shadow: 1px 1px 0 var(--w95-dark), 6px 6px 0 rgba(0, 0, 0, 0.4);
}
html:not(.bare-work) .window[data-app="bio"] .window-titlebar {
  background: linear-gradient(90deg, #000080, #1084d0);
  font-family: var(--w95-font);
}
html:not(.bare-work) .window[data-app="bio"] .window-title {
  color: #fff;
  font-family: var(--w95-font);
  font-weight: bold;
  font-size: 12px;
  letter-spacing: 0;
}
html:not(.bare-work) .window[data-app="bio"] .window-title .dot { color: #ffd400; }
html:not(.bare-work) .window[data-app="bio"] .window-main,
html:not(.bare-work) .window[data-app="bio"] .window-body {
  background: var(--w95-face);
}
html:not(.bare-work) .window[data-app="bio"] .window-body {
  color: #000;
  font-family: var(--w95-font);
  font-size: 12px;
  line-height: 1.4;
  padding: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--w95-light);
}
html:not(.bare-work) .bio-app {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
html:not(.bare-work) .bio-shell-header,
html:not(.bare-work) .bio-shell-cmd,
html:not(.bare-work) .bio-shell-closing { display: none !important; }

/* the slider, as the program's toolbar: the same control, now flush with
   the chrome it belongs to instead of a foreign object floating on black */
html:not(.bare-work) .bio-detail-row { margin: 6px 6px 6px; }
html:not(.bare-work) .bio-retro-slider {
  box-shadow: inset 1px 1px 0 var(--w95-shadow), inset -1px -1px 0 var(--w95-light),
              inset 2px 2px 0 var(--w95-light), inset -2px -2px 0 var(--w95-shadow);
  border: none;
  padding: 10px 16px 11px;
}

/* the document: sunken, white, and set like a page rather than a console */
html:not(.bare-work) .bio-doc {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin: 0 6px 4px;
  /* the same grey as the rest of the program, so the text and the picture
     sit on the window itself rather than in a white page inside it */
  background: var(--w95-face);
  padding: 14px 20px 22px;
  color: #111;
  /* the system's own plain sans -- the face a program's text is set in */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Tahoma, Helvetica, Arial, sans-serif;
  font-size: calc(14px * var(--ui-scale));
  line-height: 1.6;
}
html:not(.bare-work) .bio-doc .bio-paragraphs p { margin: 0 0 0.9em; color: #111; }
/* no shell prompt in front of every paragraph and the warning any more */
html:not(.bare-work) .bio-paragraphs p::before,
html:not(.bare-work) .bio-fiction-warning-text::before { content: none; }
html:not(.bare-work) .bio-doc .rewrite-word { color: inherit; }
/* a word being rewritten stays in the paragraph's own face and size. It
   used to switch to the terminal's mono while it retyped -- bigger and
   different -- and then snap back when it finished; its box is measured in
   the paragraph's font (see rewriteToNextWord), so the mono also overflowed
   it. The cursor is a thin black bar, not the terminal's block. */
html:not(.bare-work) .window[data-app="bio"] .bio-doc .rewrite-word.rewriting {
  font-family: inherit;
  font-size: inherit;
}
html:not(.bare-work) .window[data-app="bio"] .bio-doc .rewrite-word.rewriting::after {
  width: 1.5px;
  background: #111;
  margin-left: 1px;
}
html:not(.bare-work) .bio-doc .bio-portrait.has-photo {
  border: 1px solid #000;
  box-shadow: 3px 3px 0 var(--w95-shadow);
}
/* a Win95 scrollbar for the document */
html:not(.bare-work) .bio-doc::-webkit-scrollbar { width: 16px; }
html:not(.bare-work) .bio-doc::-webkit-scrollbar-track {
  background: repeating-conic-gradient(#dfdfdf 0 25%, #fff 0 50%) 0 0 / 2px 2px;
}
html:not(.bare-work) .bio-doc::-webkit-scrollbar-thumb {
  background: var(--w95-face);
  box-shadow: inset 1px 1px 0 var(--w95-light), inset -1px -1px 0 var(--w95-dark),
              inset 2px 2px 0 #dfdfdf, inset -2px -2px 0 var(--w95-shadow);
}

/* the fiction warning, as the program's own message box -- at the TOP of
   the text, so it is read before the text it is about. (The document is a
   column so the warning, which follows the text in the markup the phone
   also uses, can be moved up here without touching that order.) */
html:not(.bare-work) .bio-doc { display: flex; flex-direction: column; }
html:not(.bare-work) .bio-doc .bio-body { flex: 0 0 auto; }
html:not(.bare-work) .bio-fiction-warning-row {
  order: -1;
  flex: 0 0 auto;
  clear: both;
  margin: 0 0 16px;
  padding: 12px 14px 12px 54px;
  position: relative;
  background: var(--w95-face);
  box-shadow: inset 1px 1px 0 var(--w95-light), inset -1px -1px 0 var(--w95-dark),
              inset 2px 2px 0 #dfdfdf, inset -2px -2px 0 var(--w95-shadow);
  font-family: var(--w95-font);
}
html:not(.bare-work) .bio-fiction-warning-row::before {
  content: "!";
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 0;
  height: 0;
  border-left: 15px solid transparent;
  border-right: 15px solid transparent;
  border-bottom: 26px solid #ffd400;
  filter: drop-shadow(1px 1px 0 #000);
  color: transparent;
}
html:not(.bare-work) .bio-fiction-warning-row::after {
  content: "!";
  position: absolute;
  left: 25px;
  top: 50%;
  transform: translateY(-38%);
  font: bold 15px/1 var(--w95-font);
  color: #000;
}
html:not(.bare-work) .bio-fiction-warning-text {
  margin: 0;
  color: #000;
  font-size: 12px;
  font-weight: bold;
  letter-spacing: 0;
  line-height: 1.45;
}
html:not(.bare-work) .bio-warn-note { font-weight: normal; }

/* the status bar */
.bio-statusbar { display: none; }
html:not(.bare-work) .bio-statusbar {
  display: flex;
  gap: 3px;
  padding: 2px 3px 3px;
  font-size: 11px;
}
html:not(.bare-work) .bio-status-cell {
  padding: 2px 8px;
  box-shadow: inset 1px 1px 0 var(--w95-shadow), inset -1px -1px 0 var(--w95-light);
}
html:not(.bare-work) .bio-status-main { flex: 1; }

/* and the phone does not have the wrapper at all -- its layout was built
   on .bio-body and the warning being direct children of .bio-app */
html.bare-work .bio-doc { display: contents; }

/* =========================================================================
   CV.EXE, A TERMINAL (see the same heading in script.js)
   Black screen, a mono face, the desktop's own saturated colours for what
   the program is saying: the command and the prompt in yellow, the ticks
   in green, section headings in pink, field names in cyan. A line that is
   unticked goes dim and is struck through -- it will not be printed.
   ========================================================================= */
.window[data-app="cv"] .window-body {
  padding: 0;
  overflow: hidden;
  background: #0c0c0c;
  display: flex;
}
.window[data-app="cv"] .window-main { background: #0c0c0c; }
.cv-app.cvt { flex: 1; min-width: 0; min-height: 0; display: flex; }
.cvt-screen {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding: 16px 20px 26px;
  background: #0c0c0c;
  color: #d8d8d8;
  font-family: var(--bio-mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
  font-size: calc(12.5px * var(--ui-scale));
  line-height: 1.6;
  outline: none;
  cursor: default;
}
.cvt-screen::-webkit-scrollbar { width: 8px; }
.cvt-screen::-webkit-scrollbar-thumb { background: #333; }
.cvt-line { white-space: pre-wrap; overflow-wrap: anywhere; }
.cvt-line.cvt-pending { display: none; }
.cvt-cmd { color: #ffd400; }
.cvt-out.dim { color: #7a7a7a; }
.cvt-ok { color: #00c853; }
.cvt-box { color: #00c853; margin-right: 10px; white-space: pre; }
.cvt-box-some { color: #ffd400; }
.cvt-box-off { color: #5a5a5a; }
.cvt-box-fixed { color: #5a5a5a; }
.cvt-tog { cursor: pointer; border-radius: 2px; }
.cvt-tog:hover { background: rgba(255, 255, 255, 0.06); }
.cvt-tog:hover .cvt-box { color: #fff; }
.cvt-field { display: flex; align-items: baseline; gap: 0; }
.cvt-label { color: #00c8d7; flex: 0 0 auto; }
/* every field name the same width and every leader the same length, so the
   values start in one column -- NAME ....... / CONTACT .... */
.cvt-field .cvt-label { flex: 0 0 9ch; }
.cvt-dots {
  flex: 0 0 12ch;
  margin: 0 1ch 0 0;
  border-bottom: 1px dotted #555;
  transform: translateY(-4px);
}
.cvt-field .cvt-val { flex: 0 1 auto; }
.cvt-head { color: #ff1d6c; font-weight: bold; letter-spacing: 0.04em; text-transform: uppercase; }
.cvt-entry { padding-left: 2ch; }
.cvt-off .cvt-text, .cvt-off .cvt-val, .cvt-off .cvt-head, .cvt-off .cvt-label {
  color: #5a5a5a;
  text-decoration: line-through;
  text-decoration-color: #444;
}
.cvt-disabled { opacity: 0.45; cursor: default; }
.cvt-photo { padding: 6px 0 4px 5ch; }
.cvt-photo canvas {
  width: 138px;
  height: auto;
  image-rendering: pixelated;
  display: block;
  border: 1px solid #333;
}
.cvt-photo.cvt-off canvas { opacity: 0.2; filter: grayscale(1); }
.cvt-fitline .cvt-label { margin-right: 8px; }
.cvt-opt { cursor: pointer; padding: 0 4px; color: #9a9a9a; }
.cvt-opt:hover { color: #fff; }
.cvt-opt.on { color: #0c0c0c; background: #ffd400; }
.cvt-est { color: #7a7a7a; margin-left: 10px; }
.cvt-prompt .cvt-q { color: #ffd400; font-weight: bold; }
.cvt-yn { cursor: pointer; padding: 0 3px; color: #fff; text-decoration: underline; text-underline-offset: 3px; }
.cvt-prompt:not(.cvt-live) .cvt-yn { cursor: default; text-decoration: none; color: #7a7a7a; }
.cvt-live .cvt-yn:hover { background: #fff; color: #0c0c0c; }
.cvt-ans { color: #fff; margin-left: 8px; }
@keyframes cvt-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
.cvt-caret {
  display: inline-block;
  width: 8px;
  height: 14px;
  background: #d8d8d8;
  vertical-align: -2px;
  margin-left: 4px;
  animation: cvt-blink 1.05s step-end infinite;
}
