  :root{
    --wall:#8f9296; --wall-dark:#75787c; --wall-line:#7c7f83;
    --floor:#5c6570; --floor-dark:#4a525c;
    --desk:#d3c4a0; --desk-dark:#b5a67e; --desk-edge:#8c7d5c; --desk-hi:#e0d3b0;
    --monitor:#26251f; --monitor-glow:#8fe0a0; --monitor-bez:#3a3830;
    /* Bob's keyboard and mouse. A variable rather than a literal so an office
       can issue him different hardware. (The withdrawn Adequate office was
       the one that did, with dark grey peripherals for its pale grey desk.) */
    --peripheral:#d8d3c4; --peripheral-key:#b7b19c;
    --skin:#e3b98d; --shirt:#e9edf1; --tie:#8a2b2b;
    --hair:#4a3826; --ink:#201d16;
    --danger:#c23b3b; --warn:#d9a53d; --good:#5c9a5f; --gold:#e0b23a;
    --panel:#23241d; --panel-light:#33352a; --text:#f4efe0; --text-dim:#b9b39a;
    --shade: rgba(20,17,10,0.28);
    --hi: rgba(255,255,255,0.16);
  }
  *{box-sizing:border-box;}
  /* Stops a rapid double-tap on any button from being read as the browser's
     zoom gesture on mobile. No effect on desktop -- this is purely a touch
     concern, not a layout one. */
  button, .iconbtn, .tabbtn, .supportbtn{ touch-action:manipulation; }
  /* The same double-tap protection for the whole page, since a stray tap on the
     desk or the wall zoomed the game just as readily as one on a button.
     Draggables keep their own touch-action:none and are unaffected. */
  html{ touch-action:manipulation; -webkit-text-size-adjust:100%; }
  html,body{height:100%;}
  /* A pull down at the top of the page reloads it on Android's Chrome, which
     is a shift lost to a scroll. On touch screens the page never bounces or
     refreshes itself. Not on a computer, where the same rule would switch off
     a Mac trackpad's two-finger swipe back and forward. */
  @media (pointer: coarse){ html, body{ overscroll-behavior:none; } }
  body{
    margin:0; min-height:100%; background:#111008;
    background-image: radial-gradient(ellipse at 50% 0%, #1c1a12 0%, #0d0c08 70%);
    display:flex; align-items:center; justify-content:center;
    font-family:'VT323','Courier New',monospace; color:var(--text); padding:10px;
  }
  button{ font-family:inherit; }
  /* Visible keyboard focus. Nothing in the game showed one before this --
     several spots below explicitly stripped it -- so a keyboard player had no
     way to see where they were on the page. */
  :focus-visible{ outline:3px solid var(--gold); outline-offset:2px; }

  .cabinet{
    /* Grows to fill more of the browser viewport on large screens instead of
       sitting in a small fixed box, capped by whichever of these binds first:
       an absolute ceiling (1000px), 92% of viewport width, or a height-derived
       ceiling that keeps .cabinet's total height within 88% of viewport height.
       .cabinet's height = .stage's height (3/4 of .cabinet's inner width, via
       aspect-ratio) + ~193px of titlebar/status-row/coffee-button/padding/
       border chrome above and below the stage (measured directly, not a
       theoretical estimate) + 32px of body padding. On a short window this
       naturally settles back near the original 640px rather than overflowing.
       dev.html overrides this below to also clear its fixed dev bar. */
    /* The height-derived term above is what keeps a tall game inside a short
       window, and on a desktop that is exactly right. On a phone held sideways
       it is a disaster: 390px of height puts this at about 186px wide, a
       column narrower than the coffee button. It therefore only applies where
       there is real height to work with, and never shrinks below 320px. Below
       that height the width is driven by the viewport alone and the page is
       allowed to scroll (see the short-screen rule further down). */
    width:100%; max-width:92vw; background:var(--panel);
    border:4px solid var(--ink); box-shadow:0 0 0 2px #000, 8px 8px 0 rgba(0,0,0,0.5);
    padding:10px; position:relative;
    /* The ruler everything inside is measured against. The office is drawn in
       percentages of this box and always has been, so it stretches with the
       window; the pop-ups on top of it were drawn in fixed pixels and did not,
       which meant a card covering 70% of the office in one window covered 44%
       of it in another. Naming the cabinet a container lets those pop-ups be
       sized in cqw -- 1cqw is 1% of this width -- so what a card covers stays
       constant and a placement decided once holds at every size.
       inline-size, not size: the cabinet's width comes from the rule above,
       never from its contents, but its HEIGHT does come from its contents and
       full size containment would collapse it.
       The same technique is already shipping one layer down -- see
       .desktopScreen, which has been scaling SYNERGY's icons this way for
       weeks. */
    container-type:inline-size; container-name:cabinet;
    /* The unit every pop-up is measured in. 2.217cqw is 17px in the owner's own
       ~864px-wide window -- the size all of this was laid out against -- so
       nothing moves for him; bigger windows scale up from it and smaller ones
       down, which is the whole point of the exercise.

       A custom property rather than nested em on purpose. em compounds: a 12px
       label inside a 13px chip inside a 17px panel needs three divisions to get
       right and breaks the moment anything is re-nested. This inherits
       unchanged, so every rule states its own ratio against one shared base and
       none of them can interfere with each other.

       Calibrated against the cabinet's CONTENT box, since container units
       exclude its border and padding -- 28px of the 795 it measures on screen.

       The floor is not decoration. Pure proportion puts body copy at 7px on a
       small phone, and shrinking below readable was never the point: on a small
       screen the answer is layout, not smaller type. */
    --ui: clamp(13px, 2.217cqw, 26px);
    /* The unit the game's own chrome is measured in -- the title bar, its
       buttons, the energy row, the status line and GET COFFEE. Until 22
       September 2026 all of that was fixed pixels, sized for the ~767px-wide
       cabinet everything was designed in, and it was fine because the cabinet
       was capped at 1000px. The cap is gone (see the max-width rule below),
       and on a big monitor a room twice the size over a bar that had not moved
       read as a toy with a tiny toolbar. The owner asked for the bar to grow
       with the game. So: 17px at the design width, growing in proportion above
       it, and NEVER below it -- a phone or a small window keeps exactly the
       sizes it had, and every phone rule that sets its own pixels still wins.
       --k is the same ratio as a plain number, for the icon drawings, whose
       sizes are set one by one in the markup and are scaled rather than
       re-sized. tan(atan2(a, b)) is how CSS divides two lengths into a number. */
    --bar: max(17px, 2.217cqw);
    --k: max(1, tan(atan2(100cqw, 766.8px)));
  }
  /* Fill the window. The game used to stop at 88% of the window's height and
     at 1000px wide, and sat like a postcard in the middle of a dark page; on
     22 September 2026 the owner asked for the wasted band above and below it
     to go, with the game's shape and everything inside it untouched. Now the
     cabinet grows until its stroke is 10px (the body padding) from the top and
     bottom edges, and the width follows from the height.

     Two candidates, and the smaller wins, because the cabinet's height is a
     bent line in its width: below the design width (766.8px of content) the
     chrome is a fixed 218.6px tall and the office is 3/4 of the content
     width; above it the chrome grows with the width too (see --bar), and the
     whole cabinet measures 0.9976 x content + 28px. Both lines were measured
     in a browser, not worked out from the stylesheet. Each formula is its
     line solved for width, with the 20px of body padding taken off the height
     first and a pixel of slack. The wrong one of the two always gives the
     LARGER answer, so min() picks the right one at every window height
     without needing to know which side of the bend it is on. */
  @media (min-height: 600px){
    .cabinet{ max-width:min(92vw, max(320px, calc((100vh - 240px)/0.75 + 28px)), max(320px, calc((100vh - 49px)/0.998 + 28px))); }
  }
  /* A short window -- a phone held sideways, mostly -- gets a game bigger than
     it can show at once, so it has to be scrollable rather than clipped and
     vertically centred on nothing. */
  @media (max-height: 599px){
    html, body{ height:auto; }
    body{ align-items:flex-start; padding:8px; }
  }
  .titlebar{ display:flex; align-items:center; justify-content:space-between; padding:calc(var(--bar) * 0.1176) calc(var(--bar) * 0.3529) calc(var(--bar) * 0.5882); }
  /* A slow press on anything draggable otherwise pops iOS's text-selection
     magnifier and a copy/paste bubble over the game. */
  .decorItem, .toyItem, .papers, .clockwrap, .desktopIcon, .iconbtn, .stage, .solBoard,
  .monitor, .titlebar h1, .startTitle{
    -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
  }
  /* And a long press on a toy's picture opens iOS's Save Image menu. */
  .stage img, .stage svg{ -webkit-touch-callout:none; }
  .titlebar h1{ font-family:'Press Start 2P',monospace; font-size:calc(var(--bar) * 0.7647); margin:0; color:var(--gold); letter-spacing:calc(var(--bar) * 0.0588); text-shadow:calc(var(--bar) * 0.1176) calc(var(--bar) * 0.1176) 0 var(--ink); }
  .titlebar .btns{ display:flex; gap:calc(var(--bar) * 0.3529); position:relative; }
  /* Held open from the sound button. Width is the span the panel is meant to
     cover -- the stats button's left edge to the sound button's right edge --
     which at 32px buttons and 6px gaps is three buttons plus two gaps. Right
     aligned to .btns so it stays put if buttons are ever added on the left.
     z-index because .stage is positioned and comes later in the document, so
     without it the scene paints over the panel; this is HUD chrome sitting
     above the stage, not part of the scene's own paint order. */
  /* Hidden with opacity+visibility rather than display, so it can fade rather
     than blink out when it times out or is dismissed. The delayed visibility
     transition is what keeps it hit-testable and painted for the length of the
     fade; on the way in, .show overrides that so it appears promptly. */
  /* On the computer's bar the panel hangs UPWARDS from the button, because the
     bar is at the bottom of the glass and there is nothing below it. Right
     aligned to the bar's own right edge, which is where the sound button is. */
  .volumePanel.volumeOnDesk{ top:auto; bottom:calc(100% + 6px); right:calc(var(--ui) * 0.4); }
  .volumePanel{
    position:absolute; top:calc(100% + 5px); right:0; z-index:30;
    width:calc(var(--bar) * 6.353); display:flex; flex-direction:column; gap:5px;
    background:var(--panel-light); border:2px solid var(--ink);
    box-shadow:2px 2px 0 var(--ink); padding:6px 7px;
    opacity:0; visibility:hidden; pointer-events:none;
    transition:opacity 350ms ease, visibility 0s linear 350ms;
  }
  .volumePanel.show{
    opacity:1; visibility:visible; pointer-events:auto;
    transition:opacity 120ms ease;
  }
  .volRow{ display:flex; align-items:center; gap:5px; cursor:pointer; }
  .volIcon{ font-size:11px; line-height:1; color:var(--text); width:10px; text-align:center; flex:none; }
  .volumePanel input[type=range]{
    -webkit-appearance:none; appearance:none; flex:1; min-width:0;
    height:4px; background:var(--ink); outline:none; cursor:pointer;
  }
  .volumePanel input[type=range]:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }
  .volumePanel input[type=range]::-webkit-slider-thumb{
    -webkit-appearance:none; appearance:none; width:8px; height:14px;
    background:var(--gold); border:1.5px solid var(--ink); cursor:pointer;
  }
  .volumePanel input[type=range]::-moz-range-thumb{
    width:8px; height:14px; border-radius:0;
    background:var(--gold); border:1.5px solid var(--ink); cursor:pointer;
  }
  .iconbtn{ background:var(--panel-light); border:2px solid var(--ink); color:var(--text); width:32px; height:32px; font-size:16px; cursor:pointer; display:flex; align-items:center; justify-content:center; box-shadow:2px 2px 0 var(--ink); }
  .iconbtn svg{ flex-shrink:0; }
  /* The game's own bar grows with the game (--bar); the buttons on windows and
     panels do not, since those are sized with their panels (--ui). The
     drawings keep the sizes the markup gives them and are scaled to match. */
  .titlebar .iconbtn, .soundStartBtn{ border-width:calc(var(--bar) * 0.1176); width:calc(var(--bar) * 1.882); height:calc(var(--bar) * 1.882); font-size:calc(var(--bar) * 0.9412); box-shadow:calc(var(--bar) * 0.1176) calc(var(--bar) * 0.1176) 0 var(--ink); }
  .titlebar .iconbtn svg, .soundStartBtn svg{ transform:scale(var(--k)); }
  #terminalBtn{ font-family:'Space Mono',monospace; font-weight:700; font-size:14px; color:#8fef6a; }
  /* The mug is drawn now, not an emoji, so it is sized like the other SVG
     icons on this bar rather than by font-size. */
  #supportBtn svg, .cupIcon{ width:24px; height:24px; flex:0 0 auto; }
  .coffeebtn .cupIcon{ vertical-align:calc(var(--bar) * -0.3529); margin-right:calc(var(--bar) * 0.3529); width:calc(var(--bar) * 1.529); height:calc(var(--bar) * 1.529); }
  .supportHead .cupIcon{ vertical-align:-5px; margin-right:4px; }
  .iconbtn:active{ transform:translate(2px,2px); box-shadow:none; }
  .titlebar .iconbtn:active, .soundStartBtn:active{ transform:translate(calc(var(--bar) * 0.1176), calc(var(--bar) * 0.1176)); }

  /* ---- pinned programs -------------------------------------------------
     Up to four favourites in the game's top bar. They sit in the same row as
     the game's own buttons and are drawn the same way, because a pin IS a
     button on that bar -- not a second thing living next to it.

     The gold underline is the only difference, and it earns its place: it says
     these ones are yours rather than Corporate's, and it is what tells you at a
     glance where the game's buttons stop and your row begins.

     display:contents so an empty container takes no room and no gap. Wrapping
     the pins in a box of their own would put a hole in the bar on every
     desktop that has none, which is most of them. */
  /* No marking of any kind. A gold underline was tried and the owner took it
     straight back off: a pinned program is a button on this bar like every
     other button on this bar, and a badge saying "this one is different" is a
     thing to explain rather than a thing that helps.

     display:contents so an empty container takes no room and no gap. Wrapping
     the pins in a box of their own would put a hole in the bar on every
     desktop that has none, which is most of them. */
  .topBarPins{ display:contents; }
  .topBarPin svg{ width:22px; height:22px; }
  .pinsDragging .topBarPin{ cursor:grabbing; }
  /* Contacts: a permanent button, not a pin (see #contactsBtn in beta.html).
     Drawn at a pin's size because it is SYNERGY's own icon, like theirs.
     [hidden] needs saying: .iconbtn's display:flex would otherwise beat the
     browser's own rule, and the button would show with the co-worker switch
     off. */
  #contactsBtn svg{ width:22px; height:22px; }
  #contactsBtn[hidden]{ display:none; }

  /* ---- phones ----------------------------------------------------------
     430px, not the 620px Solitaire uses, because this is the width where the
     top bar genuinely runs out of room: "CAFFEINATED BOB" at 13px plus six
     32px buttons and their gaps needs about 429px, so below that the buttons
     used to hang off the right-hand edge -- the computer, Support and sound were
     entirely unreachable on an iPhone SE, and a Pixel 7 cleared it by nothing
     at all. Worse, the overflow made the whole PAGE scroll sideways, so a
     thumb-swipe meant for the office dragged the game off-centre and any
     panel opened afterwards came up half off-screen.

     No desktop window is ever this narrow, so none of this reaches one. */
  @media (max-width: 430px){
    .titlebar{ flex-wrap:wrap; justify-content:center; row-gap:8px; }
    .titlebar h1{ width:100%; text-align:center; }
    .titlebar .btns{ justify-content:center; }
    /* 44px is the size Apple and Google both publish for a thumb. Solitaire's
       controls were raised to it already; the top bar, the panel close buttons
       and the Marketplace tabs were left behind at 32. The wrapped bar above
       is what makes the room for it. */
    .iconbtn{ width:44px; height:44px; font-size:20px; }
    #supportBtn svg{ width:28px; height:28px; }
    #terminalBtn{ font-size:18px; }
    /* Still "three buttons plus two gaps", at the size those buttons now are. */
    .volumePanel{ width:144px; }

    /* The whole bar on ONE row at every phone width, pins included, with no
       minimum size -- the owner's ruling of 13 September, matching his mockup
       ("Phone menu bar - shrinking icons mockup 9-13-26"). A stopgap before a
       larger phone redesign, so it is kept simple.

       The buttons share the row evenly and fill it, capped at 44px: with no
       pins that is the size they have always been (about 43px at 390, 38px at
       360, 34px at 320). Each pin is one more button in the same row, so every
       button shrinks. The gap between them is 4px while buttons are 38px or
       more, 3px while they are 32px or more, and 2px below that -- worked out
       here from what the button would be at each gap, since CSS has no "if".
       --pins is written by play/synergy-os.js (renderPins).

       Sizes are rounded down to whole pixels so borders and drawings stay
       crisp; each drawing is capped just inside its button so it stays
       centred. The titlebar's side padding goes on phones for the few pixels
       it gives back. Nothing here reaches a desktop window, which is never
       this narrow. */
    .titlebar{ padding-left:0; padding-right:0; }
    .titlebar .btns{
      /* Eight fixed buttons since the Camera joined the bar on 21 September
         2026: menu, Contacts, the Employee File, the Marketplace, the computer,
         the Camera, Support, sound. This number is the whole reason the row
         fits -- get it wrong and the bar does not shrink to suit, it simply
         runs off both ends, which is what the eighth button did for an hour
         before this line caught up with it. Contacts is hidden until the
         co-worker switch is on, so seven is the common case and eight is the
         one that has to fit. */
      --barN: calc(8 + var(--pins, 0));
      --barW: 100cqw;
      --barAt4: calc((var(--barW) - (var(--barN) - 1) * 4px) / var(--barN));
      --barAt3: calc((var(--barW) - (var(--barN) - 1) * 3px) / var(--barN));
      --barGap: calc(2px + clamp(0px, (var(--barAt3) - 31.99px) * 1000, 1px) + clamp(0px, (var(--barAt4) - 37.99px) * 1000, 1px));
      --barBtn: min(44px, calc((var(--barW) - (var(--barN) - 1) * var(--barGap)) / var(--barN)));
      flex-wrap:nowrap; gap:var(--barGap); max-width:100%;
    }
    .titlebar .iconbtn{
      width:var(--barBtn); height:var(--barBtn);
      width:round(down, var(--barBtn), 1px); height:round(down, var(--barBtn), 1px);
      flex:0 0 auto; padding:0;
    }
    .titlebar .iconbtn svg{ max-width:calc(var(--barBtn) - 6px); max-height:calc(var(--barBtn) - 6px); }
    #muteBtn{ font-size:min(20px, calc(var(--barBtn) * 0.5)); }

  }

  .stage{ position:relative; width:100%; aspect-ratio:4/3; background:var(--wall); border:3px solid var(--ink); overflow:hidden; }
  .ceiling{ position:absolute; top:0; left:0; right:0; height:16%; background:#c2c4c1; background-image:repeating-linear-gradient(90deg, transparent 0 78px, rgba(0,0,0,0.08) 78px 80px), repeating-linear-gradient(0deg, transparent 0 28px, rgba(0,0,0,0.06) 28px 30px); border-bottom:3px solid #8c8f8c; }
  .ceilingLight{ position:absolute; top:0; width:3%; height:15%; background:#f4f3ea; border:1.5px solid #a8a89c; }
  .wallpanel{ position:absolute; top:16%; bottom:42%; left:0; right:0; background:repeating-linear-gradient(90deg, var(--wall) 0 58px, var(--wall-line) 58px 60px); }
  .wallSide{ position:absolute; top:16%; bottom:42%; width:9%; background:#65686c; }
  .wallSide.left{ left:0; }
  .wallSide.right{ right:0; }
  .neighborGlimpse{ position:absolute; top:16%; bottom:42%; width:3%; background:#54575b; opacity:0.75; }
  .neighborGlimpse.left{ left:0; }
  .neighborGlimpse.right{ right:0; }
  .neighborDesk{ position:absolute; bottom:42%; width:2.5%; height:5%; background:#7a6b4a; opacity:0.55; }
  .wallCorner{ position:absolute; top:16%; bottom:42%; width:4px; background:#45474a; }
  .wallTrim{ position:absolute; left:0; right:0; top:16%; height:2.5%; background:#b0b3b6; border-bottom:3px solid #7c7f83; }
  .wallSeam{ position:absolute; top:18.5%; bottom:42%; width:3px; background:var(--wall-line); }
  .baseboard{ position:absolute; left:0; right:0; bottom:42%; height:3%; background:var(--wall-dark); }
  .fluorflicker{ position:absolute; inset:0 0 42% 0; background:rgba(238,243,223,0.05); animation:flicker 7s infinite steps(1); pointer-events:none; }
  @keyframes flicker{ 0%,100%{opacity:1;} 92%{opacity:1;} 93%{opacity:.4;} 94%{opacity:1;} 96%{opacity:.55;} 97%{opacity:1;} }
  .floor{ position:absolute; left:0; right:0; bottom:0; height:42%; background:repeating-linear-gradient(0deg, var(--floor) 0 10px, var(--floor-dark) 10px 11px); }

  /* ---- office skins ----
     The room shell -- ceiling, cubicle wall, floor -- as ONE background image
     per office, instead of the dozen absolutely-positioned divs above.
     Deliberately a background rather than markup: a background cannot be
     clicked, cannot be measured, and cannot land in the middle of the paint
     order, so an office skin is incapable of disturbing anything in the room.
     Everything that IS interactive -- the desk, Bob, the monitor, the clock,
     the three placement zones -- is untouched and sits on top exactly where it
     always did.
     Each skin is drawn to the SAME three bands the divs above use: ceiling
     0-16%, cubicle wall 16-58%, floor the bottom 42%. That is what makes these
     skins and not a re-layout: every stored item position is a fraction of its
     zone, so the zones must not move, so the art has to come to the zones.
     preserveAspectRatio="none" in each file plus background-size:100% 100%
     means the art stretches to the stage box, which is locked to 4/3 anyway.
     The desk's three colours ride along with the skin, since a desk is part of
     the office it stands in. */
  .stage[data-office]{ background-size:100% 100%; background-repeat:no-repeat; background-position:center; }
  .stage[data-office] > .ceiling,
  .stage[data-office] > .ceilingLight,
  .stage[data-office] > .wallpanel,
  .stage[data-office] > .wallSide,
  .stage[data-office] > .neighborGlimpse,
  .stage[data-office] > .neighborDesk,
  .stage[data-office] > .wallCorner,
  .stage[data-office] > .wallSeam,
  .stage[data-office] > .wallTrim,
  .stage[data-office] > .baseboard,
  .stage[data-office] > .floor{ display:none; }
  /* .fluorflicker is NOT hidden: it is a translucent flicker over the whole
     wall, and it reads correctly over any of the three skins. */
  .stage[data-office="officedefault"]{ background-image:url("/play/skins/office-default.svg"); --desk:#d3c4a0; --desk-dark:#b5a67e; --desk-edge:#8c7d5c; }
  .stage[data-office="officerefined"]{ background-image:url("/play/skins/office-refined.svg"); --desk:#f3f2ec; --desk-dark:#dcdbd3; --desk-edge:#a5a49c; }
  /* Beige Standard Issue: oatmeal fabric with an aluminium trim rail, yellowed
     tiles and a brown carpet that has seen things. Deliberately the most
     nineteen-nineties room in the game, which is what stops it reading as a
     recolour of the grey one. */
  .stage[data-office="officefunctional"]{ background-image:url("/play/skins/office-functional.svg"); --desk:#c9b68e; --desk-dark:#a99672; --desk-edge:#7f6e4f; }
  /* This Is Fine. The DEFAULT room's desk colours, deliberately: the joke only
     works if the office looks completely ordinary and the fire is the one
     abnormal thing in it. Nothing else restyles -- no scorched hardware, no
     warm cast on the monitor -- for the same reason. The fire itself is SMIL
     inside the SVG, which the browser plays in a background image with no
     script, so this line is the entire wiring for an animated room. */
  .stage[data-office="officethisisfine"]{ background-image:url("/play/skins/office-thisisfine.svg"); --desk:#d3c4a0; --desk-dark:#b5a67e; --desk-edge:#8c7d5c; }
  /* This Is Fine is the one room whose picture is not only a background. Its
     fire comes in two sheets the game lays into the room (see applyOfficeSkin):
     the flames against the cubicle wall over the wall decor, and the seams,
     floor rows, sparks and smoke over the floor items as well. Same geometry
     as the room behind them, so all three line up. Neither catches a finger --
     items are dragged and dropped straight through them. */
  .fireLayer{
    display:none; position:absolute; inset:0; pointer-events:none;
    background-repeat:no-repeat; background-size:100% 100%; background-position:center;
  }
  .fireLayer[data-office="officethisisfine"]{ display:block; }
  .fireWall[data-office="officethisisfine"]{ background-image:url("/play/skins/office-thisisfine-wall.svg"); }
  .fireFront[data-office="officethisisfine"]{ background-image:url("/play/skins/office-thisisfine-front.svg"); }
  /* iPhones and iPads get a picture of each room instead of the drawing.
     On the owner's iPhone 13 (Chrome, which is Safari's engine underneath)
     Functional came up as a blank grey room, although the same drawing
     opened on its own drew perfectly and desktop Safari's engine draws it
     too -- so it is the phone failing to paint the drawing as a background,
     not the drawing. A photo of the room is the one thing every iPhone
     paints. `-webkit-touch-callout` exists only on iPhone and iPad, so no
     desktop browser takes this branch. This Is Fine keeps its drawing:
     its fire moves, and a picture would stop it. Pictures are made from the
     drawings at 1600x1200; redraw a room and remake its picture. */
  @supports (-webkit-touch-callout: none){
    .stage[data-office="officedefault"]{ background-image:url("/play/skins/office-default.jpg"); }
    .stage[data-office="officerefined"]{ background-image:url("/play/skins/office-refined.jpg"); }
    .stage[data-office="officefunctional"]{ background-image:url("/play/skins/office-functional.jpg"); }
  }

  /* ---- the dark ----
     Lights off (officeDark, game.js). The room goes dark in two parts, and the
     split is the whole point. The three sheets at the bottom of #stage darken
     the office BACKGROUND only -- they are its first children, so the skin is
     all that is under them. Everything standing in the room is then darkened
     one element at a time with a filter, so the room's paint order is never
     touched: the This Is Fine fire sheets stay exactly where applyOfficeSkin
     lays them and are simply not in the list, so they burn on. Light sources
     are also not in the list: the monitor's screen (a soft green from what is
     on it), each string-light bulb in its own colour, the fire. The owner's
     rule after the first mock-ups: only the bulbs glow, in the colour they
     are, and a bad bulb's flicker takes its glow with it. */
  /* Brightness only, never a hue shift: an earlier sepia/hue-rotate version
     turned the green plant purple. The blue of the night is the room's. */
  .stage.night{ isolation:isolate; --night-dim:brightness(.42) saturate(.85); }
  .nightShade, .nightShade2, .nightWarm{ display:none; position:absolute; inset:0; pointer-events:none; }
  .stage.night .nightShade{ display:block; background:#1c2340; mix-blend-mode:multiply; opacity:.86; }
  .stage.night .nightShade2{ display:block; background:linear-gradient(180deg, rgba(8,10,22,.35), rgba(8,10,22,0) 40%, rgba(8,10,22,.25)); }
  /* This Is Fine: the fire lights the room. Under the fire sheets, like everything here. */
  .stage.night[data-office="officethisisfine"] .nightWarm{ display:block; mix-blend-mode:screen;
    background:radial-gradient(ellipse at 50% 75%, rgba(255,140,50,.30), rgba(255,140,50,.10) 45%, rgba(255,140,50,0) 80%); }
  .stage.night .fluorflicker{ display:none; }   /* the tubes are off */
  /* The three zones' ITEMS are dimmed, not the zones: a filter on a zone makes
     the whole zone its own layer, and iPhone Safari drew the wall-decor zone's
     layer over the top of the TPS box standing in front of the wall -- the
     box's upper half vanished at exactly the wall line, where that zone ends
     (the owner's phone, 27 September 2026). One item at a time looks the same
     and leaves the zones as plain boxes. */
  .stage.night .clockwrap, .stage.night .decorZone > *, .stage.night .floorZone > *, .stage.night .toyZone > *,
  .stage.night .papers, .stage.night .keyboard, .stage.night .mouse, .stage.night .cup,
  .stage.night .desk, .stage.night .deskLeg, .stage.night .chairEl, .stage.night .deskSurface, .stage.night .deskFront{ filter:var(--night-dim); }
  /* The monitor: the case, stand and foot go dark together (the owner: the
     foot and base "stay the same color as the monitor"), the screen does not,
     and its light falls on all of them and on the desk. The glow is a child of
     the monitor so it moves with it on the phone layout; it sits after .foot
     and before .mailBadge in the markup so it lights the case and never the
     badge. Not the Refined cream box's own glow rule (above): this adds to it. */
  .stage.night .monitor{ background:color-mix(in srgb, var(--monitor-bez) 40%, #1c2340); }
  .stage.night .monitor .stand, .stage.night .monitor .foot{ background:color-mix(in srgb, var(--ink) 40%, #1c2340); }
  /* Refined at night: the white rim, the stand, the foot and the well all
     take the night; the light from the glass is softer and spreads further
     than the other rooms' -- "less punchy but more spread out" (the owner). */
  .stage.night[data-office="officerefined"]{
    --rc-shell:color-mix(in srgb, #e9e3d2 42%, #1c2340); --rc-edge:color-mix(in srgb, #ddd6c2 42%, #1c2340);
    --rc-hi:color-mix(in srgb, #f4f0e2 42%, #1c2340); --rc-lo:color-mix(in srgb, #cfc8b1 42%, #1c2340);
    --rc-well:color-mix(in srgb, #253252 45%, #1c2340); --rc-dot:#05090f;
  }
  .stage.night[data-office="officerefined"] .monitor .screen{
    box-shadow:
      0 0 13.5px rgba(105,215,230,0.175), 0 0 52.5px rgba(105,215,230,0.11),
      0 0 0 .18cqw #0c1426, 0 0 0 .45cqw rgba(255,255,255,.10),
      inset 0 0 0 .2cqw #7f9ea8, inset 0 0 0 .55cqw #0a1c22, inset 0 0 1.6cqw rgba(0,0,0,.95), inset .35cqw .5cqw .7cqw rgba(255,255,255,.14);
  }
  .stage.night[data-office="officerefined"] .monitor .nightGlow{
    left:-115%; top:-95%; width:330%; height:390%;
    background:radial-gradient(ellipse closest-side at 50% 52%, rgba(115,215,230,0.12), rgba(115,215,230,0.05) 40%, rgba(115,215,230,0.015) 75%, rgba(115,215,230,0) 100%);
  }
  .stage.night[data-office="officerefined"] .deskGlow{
    background:radial-gradient(ellipse closest-side at 50% 30%, rgba(115,215,230,0.07), rgba(115,215,230,0.03) 50%, rgba(115,215,230,0) 100%);
  }
  /* Refined's coloured clocks at night: the dial takes the night by hand --
     no filter on the whole clock, so the hands' glow is not dimmed with it --
     and the glow is the hands' own drop-shadow, which cannot reach past the
     dial. Nothing else about the clock lights up. */
  /* The grey clock is not a light: at night it dims like the other clocks and
     loses its day shadow. Stated outright because Refined's own day rule
     (a drop shadow) is as specific as the night dim list and sits later. */
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="grey"]{ filter:var(--night-dim); }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="blue"],
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="teal"]{ filter:none; }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="blue"] .face,
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="teal"] .face{ fill:#141414; stroke:color-mix(in srgb, #f2f0ea 42%, #1c2340); }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="blue"] .hand{ filter:drop-shadow(0 0 1.5px #2f8fdc) drop-shadow(0 0 4px rgba(47,143,220,0.7)); }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="blue"] .cap{ filter:drop-shadow(0 0 2px #2f8fdc); }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="teal"] .hand{ filter:drop-shadow(0 0 1.5px #3fbfa8) drop-shadow(0 0 4px rgba(63,191,168,0.7)); }
  .stage.night[data-office="officerefined"] .clockwrap[data-clock-design="teal"] .cap{ filter:drop-shadow(0 0 2px #3fbfa8); }
  .nightGlow{ display:none; position:absolute; left:-66.25%; top:-46.25%; width:232.5%; height:292.5%; pointer-events:none; mix-blend-mode:screen;
    background:radial-gradient(ellipse closest-side at 50% 52%, rgba(150,205,160,.13), rgba(150,205,160,.045) 45%, rgba(150,205,160,0) 100%); }
  .stage.night .monitor .nightGlow{ display:block; }
  .stage.night .monitor .screen{ box-shadow:0 0 10.5px rgba(140,200,150,.175); }  /* the owner, 26 Sept: glow halved, spread cut a quarter */
  /* The wash on the things around the monitor -- very subtle, every room. */
  .deskGlow{ display:none; position:absolute; pointer-events:none; mix-blend-mode:screen; }
  .stage.night .deskGlow{ display:block; background:radial-gradient(ellipse closest-side at 50% 30%, rgba(150,205,160,.08), rgba(150,205,160,.03) 50%, rgba(150,205,160,0) 100%); }
  /* String Lights at night: the wire, hooks and sockets go dark with the
     room; each bulb loses its outline and glows its own colour, wide (the
     owner: "the glow radius can be bigger … at night I don't think they need
     to have a black line edging them"). A bad bulb's flicker takes the glow
     with it (badBulbFlickerNight, with the wider shadows below). */
  .stage.night .lightWire, .stage.night .lightDrop{ filter:brightness(.45); }
  .stage.night .lightHook, .stage.night .lightSocket{ filter:brightness(.45); }
  .stage.night .bulbCore{ stroke:none; filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
  .stage.night .stringLightBulb.bad .bulbCore{ animation-name:badBulbFlickerNight; }
  @keyframes badBulbFlickerNight{
    0%{ fill:var(--bulb-color); filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
    8%{ fill:var(--bulb-dark); filter:none; }
    15%{ fill:var(--bulb-color); filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
    23%{ fill:var(--bulb-mid); filter:drop-shadow(0 0 2px var(--bulb-mid)); }
    29%{ fill:var(--bulb-color); filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
    56%{ fill:var(--bulb-dark); filter:none; }
    62%{ fill:var(--bulb-color); filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
    68%{ fill:var(--bulb-mid); filter:drop-shadow(0 0 2px var(--bulb-mid)); }
    74%,100%{ fill:var(--bulb-color); filter:drop-shadow(0 0 3px var(--bulb-color)) drop-shadow(0 0 8px var(--bulb-color)) drop-shadow(0 0 16px color-mix(in srgb, var(--bulb-color) 55%, transparent)); }
  }

  /* ---- the scene's three groups ----
     The wall (clock, string lights, wall decor), the floor, and the desk with
     Bob at it -- see the note on #sceneWall in beta.html. On a desktop they are
     display:contents: no box, no containing block, nothing painted, so every
     child lays out against .stage exactly as it did before the groups existed.
     Only the phone layout below gives them a box. */
  .sceneWall, .sceneFloor, .sceneDesk{ display:contents; }
  .phoneTabs{ display:none; }

  /* ================= the phone layout =================
     body.phoneLayout, set by syncPhoneLayout() in game.js: a portrait screen
     under 600px wide, on a build that has the layout switched on (for now only
     caffeinatedbob.com/mobiletesting and the dev bar's PHONE LAYOUT button).
     The owner's design of 8 and 15 September 2026: the room edge to edge, taller
     than the desktop's, with the wall and the desk each dropped down it as a
     group; a dashboard of two tiles and GET COFFEE under it; BOB / SYNERGY /
     FILE along the bottom. Nothing here reaches a desktop.

     THE FRAME. The office drawings are 400 x 300. The phone room is a window on
     them from x 67 to 335 and y 16 to 350 -- 268 x 334, redrawn for that frame
     by tools/office-skins/build-phone.mjs, not stretched. Every group box below
     is that original 400 x 300 room placed inside the window, so a position
     stored as a fraction of the desktop room means the same spot here.

     THE DIALS. The three numbers the owner may want to change after trying it on
     his phone, and the only three: how far the desk group drops (a fraction of
     the room's height), how much it grows, and how far the wall drops. The
     boxes are worked out from them, so change these and nothing else. */
  body.phoneLayout{
    --ph-desk-drop: 0.235;
    --ph-desk-scale: 1.08;
    --ph-wall-drop: 0.073;
    /* THE ROOM FITS THE PHONE. The owner, 15 September, on his iPhone in
       Chrome: the whole page on one screen, no scrolling, on whatever phone it
       is on. So the room is sized in its own units rather than as a fixed
       shape: --u is one unit of the office drawings (the 268 units of room a
       phone shows across its width), and the stage is as tall as the screen
       leaves room for -- fitPhoneStage() in game.js works that out and writes
       the height. The top is always the same (2% off the drawing's top, at the
       owner's request: y 22.68); the bottom comes up as far as a short screen
       needs, which shortens only the plain front panel of the desk, and goes
       down as far as the drawing (y 350) on a tall one. Everything below is
       measured from that fixed top, so nothing moves when the bottom does.
       cqw is 1% of .cabinet's width, which on this layout is the screen's. */
    --u: calc(100cqw / 268);
    --ph-top: 22.68;
    /* The 400 x 300 room: x 0 is 67 units left of the screen's edge. */
    --ph-room-l: calc(-67 * var(--u));
    --ph-room-w: calc(400 * var(--u));
    --ph-room-t: calc(-1 * var(--ph-top) * var(--u));
    --ph-room-h: calc(300 * var(--u));
    /* The desk group grows about the middle of the room's bottom edge
       (x 200, y 300) and then drops. */
    --ph-desk-w: calc(400 * var(--ph-desk-scale) * var(--u));
    --ph-desk-h: calc(300 * var(--ph-desk-scale) * var(--u));
    --ph-desk-l: calc((200 - 200 * var(--ph-desk-scale) - 67) * var(--u));
    --ph-desk-t: calc((300 + 300 * var(--ph-desk-drop) - 300 * var(--ph-desk-scale) - var(--ph-top)) * var(--u));
    --ph-wall-t: calc((300 * var(--ph-wall-drop) - var(--ph-top)) * var(--u));
    padding:0; align-items:flex-start;
    background:#111008;
  }
  body.phoneLayout .cabinet{
    max-width:100%; border:0; box-shadow:none; padding:0;
    background:#111008;
    /* A screen's height at least, with the tabs held to its bottom edge: on a
       phone taller than the whole room drawing, the spare height sits between
       GET COFFEE and the tabs rather than under the tabs. */
    min-height:100vh; min-height:100dvh; display:flex; flex-direction:column;
    /* A screen sliding in starts a whole screen to the side; clipped, so the
       page never grows sideways while it moves. */
    overflow-x:clip;
  }
  body.phoneLayout .phoneTabs{ margin-top:auto; }
  /* Forced on a wide screen (the dev bar's button, or the test page opened on
     a computer): a phone-width column, so what is on screen is what a phone
     shows. ?width= on the test page picks another width. */
  body.phoneLayout.phoneForced .cabinet{ max-width:var(--ph-column, 390px); margin:0 auto; }
  body.phoneLayout.phoneForced{ justify-content:center; }
  /* Email's tabs are a thumb's height on the phone (35px measured). */
  body.phoneLayout .mailTabs .mailTab{ min-height:40px; }

  /* The top bar: the title on its own line, then every button on one row at a
     thumb's size. The same rules the narrow-screen block above gives a phone,
     repeated here so a forced layout on a wide screen matches a real phone. */
  body.phoneLayout .titlebar{ flex-wrap:wrap; justify-content:center; row-gap:6px; padding:7px 8px 7px; }
  body.phoneLayout .titlebar h1{ width:100%; text-align:center; font-size:14px; }
  body.phoneLayout .titlebar .btns{
    /* Eight, and it has to be said twice: the narrow-screen block above counts
       the same buttons for a small window, and THIS is the one a phone
       actually gets. Changing only the other one left the bar at its old size
       with two buttons hanging off the ends -- the menu and the sound, one at
       each edge, which is the failure the note above that block describes from
       the last time it happened. */
    --barN: calc(8 + var(--pins, 0));
    --barW: calc(100cqw - 16px);
    --barAt4: calc((var(--barW) - (var(--barN) - 1) * 4px) / var(--barN));
    --barAt3: calc((var(--barW) - (var(--barN) - 1) * 3px) / var(--barN));
    --barGap: calc(2px + clamp(0px, (var(--barAt3) - 31.99px) * 1000, 1px) + clamp(0px, (var(--barAt4) - 37.99px) * 1000, 1px));
    --barBtn: min(44px, calc((var(--barW) - (var(--barN) - 1) * var(--barGap)) / var(--barN)));
    justify-content:center; flex-wrap:nowrap; gap:var(--barGap); max-width:100%;
  }
  body.phoneLayout .titlebar .iconbtn{
    width:var(--barBtn); height:var(--barBtn);
    width:round(down, var(--barBtn), 1px); height:round(down, var(--barBtn), 1px);
    flex:0 0 auto; padding:0; font-size:20px;
  }
  body.phoneLayout .titlebar .iconbtn svg{ max-width:calc(var(--barBtn) - 6px); max-height:calc(var(--barBtn) - 6px); }
  body.phoneLayout #supportBtn svg{ width:28px; height:28px; }
  body.phoneLayout #muteBtn{ font-size:min(20px, calc(var(--barBtn) * 0.5)); }
  body.phoneLayout .volumePanel{ width:144px; }

  /* The room: edge to edge, no border, at the phone frame's shape. */
  /* Its height comes from fitPhoneStage(); until that has run, the shape the
     owner saw on his phone on 15 September. The drawing keeps its full 334
     units of height and is hung so its y 22.68 is the stage's top edge. */
  body.phoneLayout .stage{ aspect-ratio:auto; height:var(--ph-stage-h, calc(270.75 * var(--u))); border:0; }
  body.phoneLayout .stage[data-office]{ background-size:100% calc(334 * var(--u)); background-position:center calc((16 - var(--ph-top)) * var(--u)); }
  body.phoneLayout .fluorflicker{ inset:0 0 auto 0; height:calc((196 - var(--ph-top)) * var(--u)); }
  body.phoneLayout .stage[data-office="officedefault"]{ background-image:url("/play/skins/phone/office-default.svg"); }
  body.phoneLayout .stage[data-office="officerefined"]{ background-image:url("/play/skins/phone/office-refined.svg"); }
  body.phoneLayout .stage[data-office="officefunctional"]{ background-image:url("/play/skins/phone/office-functional.svg"); }
  body.phoneLayout .stage[data-office="officethisisfine"]{ background-image:url("/play/skins/phone/office-thisisfine.svg"); }
  /* This Is Fine's fire, on the phone: the same two sheets, drawn to the
     phone room's geometry. */
  body.phoneLayout .fireWall[data-office="officethisisfine"]{ background-image:url("/play/skins/phone/office-thisisfine-wall.svg"); }
  body.phoneLayout .fireFront[data-office="officethisisfine"]{ background-image:url("/play/skins/phone/office-thisisfine-front.svg"); }
  body.phoneLayout .fireLayer{ background-size:100% calc(334 * var(--u)); background-position:center calc((16 - var(--ph-top)) * var(--u)); }
  /* iPhones take pictures of the rooms, for the reason given above the desktop
     version of this block. This Is Fine keeps its moving drawing. */
  @supports (-webkit-touch-callout: none){
    body.phoneLayout .stage[data-office="officedefault"]{ background-image:url("/play/skins/phone/office-default.jpg"); }
    body.phoneLayout .stage[data-office="officerefined"]{ background-image:url("/play/skins/phone/office-refined.jpg"); }
    body.phoneLayout .stage[data-office="officefunctional"]{ background-image:url("/play/skins/phone/office-functional.jpg"); }
  }

  /* The groups get their boxes. No transforms: the desk group is simply a
     bigger box, so every measurement the drag code takes -- a rect here, a
     clientWidth there -- agrees with every other. */
  body.phoneLayout .sceneWall,
  body.phoneLayout .sceneFloor,
  body.phoneLayout .sceneDesk{ display:block; position:absolute; pointer-events:none; }
  body.phoneLayout .sceneWall{ left:var(--ph-room-l); width:var(--ph-room-w); top:var(--ph-wall-t); height:var(--ph-room-h); }
  body.phoneLayout .sceneFloor{ left:var(--ph-room-l); width:var(--ph-room-w); top:var(--ph-room-t); height:var(--ph-room-h); }
  body.phoneLayout .sceneDesk{ left:var(--ph-desk-l); width:var(--ph-desk-w); top:var(--ph-desk-t); height:var(--ph-desk-h); }
  /* A group has to let presses through -- the floor group's box covers the
     whole wall -- but pointer-events is inherited, so every child that takes a
     press on the desktop without saying so is given it back. The zones and Bob
     say none for themselves, and the placed items say auto, so neither is
     listed. */
  body.phoneLayout .sceneWall > .clockwrap,
  body.phoneLayout .sceneDesk > .desk,
  body.phoneLayout .sceneDesk > .deskLeg,
  body.phoneLayout .sceneDesk > .deskSurface,
  body.phoneLayout .sceneDesk > .deskFront,
  body.phoneLayout .sceneDesk > .keyboard,
  body.phoneLayout .sceneDesk > .mouse,
  body.phoneLayout .sceneDesk > .papers,
  body.phoneLayout .sceneDesk > .cup,
  body.phoneLayout .sceneDesk > .monitor{ pointer-events:auto; }
  /* The wall zone: the cubicle panel between its top (y 57 in the phone
     drawings since the walls were raised a tenth on 20 September 2026) and its
     base (y 196), inside the wall group, and across only the part of the wall a
     phone shows (x 67-335, less a little either side). The group has already
     dropped the panel by 300 x --ph-wall-drop = 21.9, so in its own terms the
     panel runs from y 35.1 to 174.1 -- 11.7% to 58.03% of the 300-unit group.
     tests/phone-layout.test.js keeps these two numbers and the skin builder's
     frame from drifting apart, which they did for a day when the walls grew and
     this band stayed where the shorter wall had put it. */
  body.phoneLayout .decorZone{ top:11.7%; height:46.33%; left:17.5%; width:65%; }
  /* Wall decor is 11% of its zone; this zone is narrower than the desktop's
     79.2%, so the same 11% would shrink every picture. Scaled back up, so a
     poster is the same size against the wall on both. */
  body.phoneLayout #decorZone > .decorItem{ width:calc(11% * 79.2 / 65); }
  /* The floor zone: all the carpet a phone shows, from the panel base (y 196)
     to the bottom of the room (y 350), the full width of the room drawing so a
     plant is the same size here as on a desktop. Where on it an item may
     actually stand is FLOOR_SHAPE's job (phoneFloorShape in game.js). */
  body.phoneLayout .floorZone{ left:0; right:auto; width:100%; top:65.3333%; bottom:auto; height:51.3333%; }
  /* No strip in front of the desk on a phone: the floor stops behind it. */
  body.phoneLayout #floorFrontZone{ display:none; }
  /* The dashboard: a wide ENERGY tile (the number, the bar, and the condition
     line folded in under it), a CV/HR tile, and GET COFFEE across both. The
     same elements as the desktop's row, laid out again -- nothing is copied, so
     every listener and test still finds them. */
  /* The owner's pick of 20 September 2026, after eight rounds of rendered
     options: the energy tile is laid out like the CV/HR tile -- the number
     over its word at the left, the top of the number level with the top of
     the bar -- with the bar to the right and the condition line centered
     under the bar. The CV/HR tile is half its old width so the bar gets the
     rest, the 100% line stays inside the bar, and the two tiles sit 5px
     apart, each running off its own edge. */
  body.phoneLayout .hud{
    display:grid; grid-template-columns:auto minmax(0, 1fr) 84px; column-gap:5px;
    grid-template-rows:auto auto auto;
    /* Edge to edge, like the room above them (the owner, 15 September): no
       side margin, and the tiles' outer sides and the coffee button's run off
       the screen, so nothing reads as a box floating short of the edge. */
    padding:8px 0 2px;
  }
  body.phoneLayout .energyrow, body.phoneLayout .barlabel{ display:contents; }
  body.phoneLayout .hud::before, body.phoneLayout .hud::after{
    content:""; background:var(--panel-light); border:3px solid var(--ink);
  }
  body.phoneLayout .hud::before{ grid-area:1 / 1 / 3 / 3; border-left:0; }
  body.phoneLayout .hud::after{ grid-area:1 / 3 / 3 / 4; border-right:0; }
  body.phoneLayout #barLabelEnergy,
  body.phoneLayout #barWrap,
  body.phoneLayout #statusRow,
  body.phoneLayout #barLabelRate{ position:relative; z-index:1; }
  /* The number over the word ENERGY, the way the rate sits over CV/HR. Both
     the number's padding and the bar's margin are 12px from the tile's top,
     and both have no line-height to spare, so their tops line up. */
  body.phoneLayout #barLabelEnergy{
    grid-area:1 / 1 / 3 / 2; align-self:start; display:flex; flex-direction:column; align-items:center; gap:5px;
    padding:12px 0 0 16px; line-height:1;
  }
  body.phoneLayout #barLabelEnergy::after{ content:"ENERGY"; font-family:'Space Mono',monospace; font-size:11px; font-weight:700; letter-spacing:1px; color:var(--text-dim); line-height:1; }
  body.phoneLayout #barWrap{ grid-area:1 / 2; align-self:end; margin:12px 16px 0 10px; flex:none; height:16px; }
  /* The 100% line stops at the bar's edges here rather than poking out above
     and below it, which the owner found bothersome on a phone. */
  body.phoneLayout .barmark100{ top:0; bottom:0; }
  body.phoneLayout #statusRow{
    grid-area:2 / 2; margin:0; padding:6px 16px 10px 10px; text-align:center; font-size:16px; line-height:1.1; min-height:0;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  body.phoneLayout #barLabelRate{
    grid-area:1 / 3 / 3 / 4; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px; white-space:normal;
  }
  body.phoneLayout .barlabelWord{ display:none; }
  body.phoneLayout .barlabelNum{ font-family:'Press Start 2P',monospace; font-size:13px; line-height:1; color:var(--gold); text-shadow:2px 2px 0 var(--ink); }
  body.phoneLayout #barLabelRate::after{ content:"CV/HR"; font-family:'Space Mono',monospace; font-size:11px; font-weight:700; letter-spacing:1px; color:var(--text-dim); }
  /* The rate's warning colours move to its number, since the tile's label
     is always the same grey. */
  body.phoneLayout .barlabelRate.rateZero .barlabelNum{ color:var(--danger); }
  body.phoneLayout .barlabelRate.rateHigh .barlabelNum{ color:var(--good); }
  body.phoneLayout #coffeeBtn{ grid-area:3 / 1 / 4 / 4; margin-top:8px; padding:13px 10px; border-left:0; border-right:0; box-shadow:none; }
  /* A press darkens it rather than nudging it sideways, which on a button as
     wide as the screen looks like the page jumping. */
  body.phoneLayout #coffeeBtn:not(:disabled):active{ transform:none; background:#2a2c23; }

  /* ---- the SYNERGY and FILE screens ----
     Each fills the screen above the tabs, over the office (which keeps running
     underneath). --ph-tabs-h is the tab row's height, written by fitPhoneStage.
     Programs opened from SYNERGY are ordinary windows and come up over all of
     it, tabs included, until they are closed. */
  .phoneSynergy{ display:none; }
  body.phoneLayout.phoneScreenSynergy .phoneSynergy,
  body.phoneLayout.phonePeekSynergy .phoneSynergy{
    display:flex; flex-direction:column; position:absolute; left:0; right:0; top:0; bottom:var(--ph-tabs-h, 66px); z-index:25;
    /* Back to the dark screen the phone had before the desktop went blue --
       the owner, 24 September 2026, while he works out what he wants from the
       wallpapers. The logo stays, in SYNERGY's own green. Black from 1
       October 2026, like the computer's (the owner: black with the green
       logo, the same strength) -- and then back to this warm, lit-from-the-
       corner glow the same day, because black lost the old-monitor feel. */
    background:radial-gradient(120% 140% at 30% 12%, #322f27 0%, var(--monitor) 55%, #15130e 100%);
    box-shadow:inset 0 0 40px 12px rgba(0,0,0,.55); overflow:hidden;
  }
  /* The same wallpaper as the computer: the dark glow with the logo centred and
     faint behind the apps. One background, both screens -- which is also what
     a bought one will be. */
  /* Printed in the green rather than shown as artwork: the logo is used as a
     stencil and the colour comes from underneath, so it is SYNERGY's own green
     on the dark screen at a tenth of full strength. */
  .phoneSynergy .deskLogo{ top:46%; width:56%; opacity:.1;
    background:var(--monitor-glow);
    -webkit-mask:url("/play/skins/synergy-logo.png") center/contain no-repeat;
    mask:url("/play/skins/synergy-logo.png") center/contain no-repeat; }
  .phSynBar{
    flex:none; display:flex; align-items:center; justify-content:space-between; padding:12px 16px;
    background:linear-gradient(180deg, #34322b, #22211b); border-bottom:1px solid #4a473d;
    font-family:'Space Mono',monospace; font-size:13px; font-weight:700; color:var(--text);
  }
  .phSynLogo{ font-family:'Press Start 2P',monospace; font-size:11px; color:var(--gold); letter-spacing:1px; }
  .phSynGrid{
    flex:1; min-height:0; overflow-y:auto; display:grid; grid-template-columns:repeat(3, 1fr);
    gap:18px 6px; padding:20px 12px; align-content:start;
  }
  .phSynApp{
    position:relative; background:none; border:0; padding:0; cursor:pointer; color:var(--text);
    display:flex; flex-direction:column; align-items:center; gap:6px; min-width:0;
  }
  .phSynApp .glyph{ width:56px; height:56px; display:flex; align-items:center; justify-content:center; }
  .phSynApp .glyph svg{ width:52px; height:52px; }
  .phSynApp .lbl{
    font-family:'Space Mono',monospace; font-size:12px; font-weight:700; line-height:1.2; text-align:center;
    text-shadow:1px 1px 0 #000; max-width:100%; overflow-wrap:anywhere;
  }
  .phSynApp:active .glyph{ transform:translate(1px, 1px); }
  .phSynBadge{
    position:absolute; top:-4px; left:calc(50% + 14px); min-width:22px; height:22px; padding:0 4px; border-radius:11px;
    background:var(--danger); color:#fff; border:2px solid var(--ink); font-family:'Space Mono',monospace; font-size:12px;
    font-weight:700; display:flex; align-items:center; justify-content:center;
  }
  .phSynDock{
    flex:none; margin:0 12px 12px; padding:8px 10px; background:rgba(255,255,255,.06); border:1px solid #4a473d;
    border-radius:20px; display:flex; justify-content:space-around;
  }
  .phSynDockApp .glyph{ width:48px; height:48px; }
  .phSynDockApp .glyph svg{ width:44px; height:44px; }
  /* SYNERGY Mobile: arranging (hold any app), folders, the dock. */
  .phSynBar .phSynDone{ font-family:'Space Mono',monospace; font-weight:700; font-size:14px; color:var(--gold); background:none; border:1px solid var(--gold); border-radius:14px; padding:2px 14px; min-height:32px; cursor:pointer; }
  .phSynEditing .phSynApp{ touch-action:none; -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
  .phSynApp{ -webkit-touch-callout:none; -webkit-user-select:none; user-select:none; }
  @keyframes phSynTremble{ 0%{ transform:rotate(-1.4deg); } 50%{ transform:rotate(1.4deg); } 100%{ transform:rotate(-1.4deg); } }
  .phSynEditing .phSynApp .glyph{ animation:phSynTremble 260ms ease-in-out infinite; }
  .phSynEditing .phSynApp:nth-child(2n) .glyph{ animation-delay:-130ms; }
  @media (prefers-reduced-motion: reduce){ .phSynEditing .phSynApp .glyph{ animation:none; } }
  .phSynLifted{ opacity:.25; }
  .phSynPlaceholder{ min-height:80px; }
  .phSynGhost{ position:fixed; z-index:99999; pointer-events:none; transform:scale(1.12); opacity:.92; background:none; border:0; display:flex; flex-direction:column; align-items:center; gap:6px; color:var(--text); }
  .phSynGhost .glyph{ width:56px; height:56px; display:flex; align-items:center; justify-content:center; }
  .phSynGhost .glyph svg{ width:52px; height:52px; }
  .phSynGhost .lbl{ display:none; }
  .phSynMergeRing .glyph{ outline:3px solid var(--gold); outline-offset:3px; border-radius:14px; }
  /* A folder tile: its apps, small, in a rounded square. */
  /* A tenth smaller than an app's icon: a folder was taking up more room than
     the things inside it (the owner, 15 September). */
  .phSynFolder .glyph{ width:50px; height:50px; }
  .phSynFolderGlyph{ background:rgba(255,255,255,.12); border:1px solid #6a6656; border-radius:13px; box-sizing:border-box; padding:4px;
    display:grid !important; grid-template-columns:repeat(3, 1fr); grid-auto-rows:13px; gap:2px; align-content:start; }
  .phSynMini{ display:flex; align-items:center; justify-content:center; }
  .phSynMini svg{ width:13px !important; height:13px !important; }
  .phSynDock:empty{ min-height:64px; }
  /* An open folder: a sheet over the dimmed grid. */
  .phSynSheetScrim{ position:absolute; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:5; }
  .phSynSheet{ width:min(88%, 360px); max-height:80%; overflow:auto; background:#2c2a22; border:1px solid #6a6656; border-radius:22px; box-shadow:0 10px 40px rgba(0,0,0,.6); padding:10px 8px 16px; }
  .phSynSheetHead{ display:flex; align-items:center; gap:8px; padding:4px 8px 8px; }
  .phSynSheetName{ flex:1; text-align:left; background:none; border:0; color:var(--text); font-family:'Space Mono',monospace; font-weight:700; font-size:18px; padding:4px; min-height:36px; }
  .phSynSheetName.phSynRenameable{ border-bottom:1px dashed var(--gold); }
  .phSynSheetClose{ width:40px; height:40px; background:rgba(255,255,255,.08); border:1px solid #6a6656; border-radius:20px; color:var(--text); font-size:16px; }
  .phSynSheetGrid{ padding:8px 4px; }
  .phSynSheetEmpty{ grid-column:1 / -1; color:#a49c88; text-align:center; font-size:13px; padding:18px 8px; }
  /* The Employee File, full screen: the owner asked for it that way on phones
     (15 September). No scrim, no border, no ✕ -- it is a screen, and the tabs
     are the way off it. */
  /* Drawn while it slides in, before it is the screen on show. */
  body.phoneLayout #employeeOverlay.phonePeek{ display:flex; }
  body.phoneLayout #employeeOverlay.phoneScreen,
  body.phoneLayout #employeeOverlay.phonePeek{
    bottom:var(--ph-tabs-h, 66px); padding:0; background:#111008; align-items:stretch; justify-content:stretch; z-index:25;
  }
  body.phoneLayout #employeeOverlay.phoneScreen #employeePanel,
  body.phoneLayout #employeeOverlay.phonePeek #employeePanel{
    max-width:none; max-height:none; width:100%; height:100%; border:0; box-shadow:none;
  }
  /* THE TOP BAR'S COMPUTER BUTTON IS A PHONE ON A PHONE. The owner, 15
     September: "on mobile can we have different icons on the top menu bar than
     what we see on desktop? in particular, I might be interested in an old
     school mobile device instead of old computer monitor." He chose the
     smartphone from the drawings (round two). Both pictures are in the button
     and the layout picks one, so nothing has to be drawn in script and the
     four builds stay generated from one file. */
  .iconPhone{ display:none; }
  body.phoneLayout .iconDesk{ display:none; }
  body.phoneLayout .iconPhone{ display:inline; }
  /* The attachment viewer's page buttons, a thumb's height on the phone (33px measured). */
  body.phoneLayout #deckPrev, body.phoneLayout #deckNext{ min-height:44px; }
  /* The Leaderboard, opened from the File on a phone: a screen of its own over
     it, above the tabs, closed by the SAME ✕ every other panel in the game is
     closed by -- drawn inside the corner here rather than hanging off it, which
     is where it sits on a window with room around it.

     It opened with "‹ File" at the top left until 22 September 2026, borrowed
     from how a phone's own settings screens go back. The owner: "the leaderboard
     menu on mobile needs an x in the top right corner. File on the left just
     doesn't really make sense in the context of how the rest of the game works.
     X is more familiar to the players." Every other panel here closes with an ✕
     and this one is not special. */
  body.phoneLayout #leaderboardOverlay.phoneFull{ bottom:var(--ph-tabs-h, 66px); padding:0; background:#111008; align-items:stretch; justify-content:stretch; }
  body.phoneLayout #leaderboardOverlay.phoneFull #leaderboardPanel{ max-width:none; max-height:none; width:100%; height:100%; border:0; box-shadow:none; }
  body.phoneLayout #leaderboardOverlay.phoneFull #leaderboardPanel > .panelClose{
    top:6px; right:6px; width:44px; height:44px; font-size:18px;
  }
  /* And the title keeps clear of it. */
  body.phoneLayout #leaderboardOverlay.phoneFull #leaderboardOverlayHeading{ padding-right:52px; }
  /* THE PAPER GOES CLEAR TO THE EDGES, on the two forms that are a sheet of
     paper: the TPS report and the After Hours Authorization. The owner, 22
     September 2026: "we don't gain anything from having edges around the paper.
     I want the paper to go clear to the edges and everything resized
     accordingly on both."

     There were three frames stacked around each sheet -- the dark outside the
     panel, the panel's own ink border, and the padding of the panel's body --
     which on a 390 screen spent 38 pixels of width on a document that has
     nothing to spare. On a computer those edges are what make it read as a
     window with a sheet of paper in it; on a phone the sheet IS the screen.

     The paper keeps its own inside margin, so the type is not against the
     glass -- that margin is now the only one there is. */
  body.phoneLayout #afterHoursOverlay{ padding:0; }
  body.phoneLayout #tpsOverlay .panelBox,
  body.phoneLayout #afterHoursOverlay .panelBox{ border:0; box-shadow:none; max-width:none; }
  body.phoneLayout #afterHoursOverlay .panelBox{ max-height:100%; height:100%; }
  body.phoneLayout #tpsOverlay #tpsBody,
  body.phoneLayout #afterHoursOverlay #afterHoursBody{ padding:0; }
  body.phoneLayout #tpsOverlay .tpsPaper{ border:0; }
  /* With the panel flush to the screen its ✕ has nothing to hang off -- it sits
     14px outside the box everywhere else -- so it comes inside, thumb-sized,
     and the heading keeps clear of it. The TPS report has the phone's own
     program chrome above it and needs none of this. */
  body.phoneLayout #afterHoursOverlay .panelBox > .panelClose{
    top:6px; right:6px; width:44px; height:44px; font-size:18px;
  }
  body.phoneLayout #afterHoursOverlay #afterHoursHeading{ padding-right:52px; }
  body.phoneLayout #employeeOverlay.phoneScreen #employeePanel > .panelClose,
  body.phoneLayout #employeeOverlay.phonePeek #employeePanel > .panelClose{ display:none; }
  /* The glide between screens is a transform written by the script, frame by
     frame (phoneGlide in game.js), with no blur of any kind: the owner's ruling
     of 15 September. What the browser must not do is start a sideways pan of
     its own before the script has the finger, so on the three screens only
     up-and-down is left to it. Things that drag keep touch-action:none. */
  body.phoneLayout .titlebar, body.phoneLayout #stage, body.phoneLayout .hud,
  body.phoneLayout .phoneSynergy, body.phoneLayout #employeeOverlay.phoneScreen{ touch-action:pan-y; }

  /* BOB / SYNERGY / FILE. */
  body.phoneLayout .phoneTabs{
    display:grid; grid-template-columns:repeat(3, 1fr); row-gap:0; padding:8px 8px 0;
    padding-bottom:env(safe-area-inset-bottom, 0px);
  }
  .phoneTabDots{ grid-column:1 / 4; display:flex; justify-content:center; gap:9px; }
  .phoneTabDots span{ width:10px; height:10px; border-radius:50%; border:2px solid var(--text); }
  .phoneTabDots span.on{ background:var(--text); }
  .phoneTab{
    background:none; border:0; color:var(--text-dim); font-family:'Press Start 2P',monospace; font-size:11px;
    letter-spacing:1px; padding:6px 0; min-height:40px; cursor:pointer;
  }
  .phoneTab.on{ color:var(--gold); }

  /* 10% smaller than it was (12.5% -> 11.25% -> 10.125%): the clock was
     eating more of the wall than it earns, and the wall zone below is
     smaller now. The numerals do NOT shrink with it -- buildClockNumerals
     scales its font up by the same 10% so they stay exactly the size they
     were on screen, which is the readable limit. Change one and change the
     other. */
  .clockwrap{ position:absolute; top:25.5%; left:50%; width:10.125%; aspect-ratio:1/1; cursor:grab; touch-action:none; transform:translate(-50%,-50%); }
  .clockwrap svg{ width:100%; height:100%; display:block; }

  /* Sized so that wall decor ends up with exactly the clock's travel area,
     which is the reference for anything hung on the cubicle wall.
     makeDraggable keeps an item wholly inside its zone, so a decor item's
     centre is clamped to the zone inset by half the item's own size -- these
     four values are the solution to that inset landing on the clock's own
     bounds (x 0.0968-0.9032, y 0.209-0.521 of the stage, see the clockEl
     makeFreeDraggable call). Every wall item is .decorItem at a uniform 11% of
     the zone width (.bigItem/33% is only the plant, which lives on the floor in
     .deskDecorZone), so one rect is exact for all of them, and .stage is locked
     to aspect-ratio 4/3, so the vertical solution holds at any viewport size.
     Changing the zone, the item width, or the stage aspect breaks the match --
     re-solve rather than nudging these by eye. */
  /* Also re-measured by the owner (dev picker, "Play area"): left 10.6, top
     15.5, w 79.2, h 41.1. Smaller than it was, because the cubicle wall in the
     new offices is a narrower panel between two angled sides -- the old box let
     wall decor hang past the panel and onto the side walls. The clock is
     clamped to this same box (decorZoneClamp), so it follows automatically. */
  .decorZone{ position:absolute; top:15.5%; left:10.6%; width:79.2%; height:41.1%; pointer-events:none; }
  .decorItem{ position:absolute; width:11%; aspect-ratio:1/1; cursor:grab; touch-action:none; user-select:none; pointer-events:auto; }
  /* The floor: where the base of a free-standing item may be put down. Matches
     the visible floor art -- .floor runs 0%-42% from the bottom and .baseboard
     marks the wall join at 42% -- so the top of this band is exactly where the
     floor stops, and nothing can be stood up the wall. Only the front 1% is
     held back -- practically the whole floor is usable, right down to the frame
     edge. Stopping any higher leaves a visible strip of floor that nothing can
     be stood on, and at 18% it put the entire front of the room -- everything
     nearer than the desk's front panel at 19.5% -- out of reach.
     This bounds the item's BASE only; its artwork is free to rise above the
     band, which is what lets a tall plant stand against the cubicle wall.
     Height is 42% - 1% = 41%.
     Was 1%-41% from the bottom and 98% wide, which made the box for the 33%-wide
     plant TALLER than the zone containing it -- the clamp below then resolved to
     Math.max(0.539, Math.min(0.461, y)), pinning the plant to one horizontal rail
     it could never leave, with its pot clipped off the bottom of the stage. */
  /* Re-measured by the owner against the new office art (dev picker, "Play
     area" frame): left -0.1, right 99.8, top 58.4, bottom 100.4. This is only
     the BOUNDING BOX -- the floor itself is a hexagon, because the two angled
     cubicle walls cut a wedge out of each back corner. The box is what item
     positions are fractions of; FLOOR_SHAPE in game.js is the shape they are
     actually held inside. NOTE: item positions are
     stored as fractions of THIS box, so changing it shifts anything already
     placed -- see the note in loadCareer. */
  .floorZone{ position:absolute; left:-0.1%; right:0.2%; bottom:-0.4%; height:42%; pointer-events:none; }
  /* The plant, and only the plant.

     Its box is square and a third of the floor zone wide; the plant drawn in it
     fills the middle 58% horizontally and 78% vertically (measured with
     getBBox). The whole box answered the pointer, so the plant could be picked
     up from a hand's width away from any leaf -- the owner: "the grab area is
     way bigger than the actual item, especially horizontally and a little bit
     vertically as well."

     The box itself cannot shrink: every plant anybody has placed is stored as a
     position within it, and resizing would move them all. So the box stays and
     the CLIP changes. clip-path is hit-tested as well as painted, so this
     trims the dead space away from the pointer without moving anything -- and,
     unlike taking the element out of hit testing, it leaves the pointer capture
     that dragging depends on completely alone.

     The insets are the art's own bounds on the 24-unit icon grid, with about
     half a unit of margin: left 4.5/24, right (24-19.3)/24, top 2.4/24,
     bottom (24-22)/24. Nothing is clipped that is ever drawn. */
  /* Bigger than the 33% it was (the owner, 24 September 2026): first 5% --
     "the plant needs to be bigger in general -- not the resizing of it but the
     actual base size should go up by about 5% overall" -- then 10%, the same
     evening: "let's make the plant 10% bigger. I want to see how that feels." Safe to change now that a floor
     item is pinned at the base of its artwork: it grows about the spot it
     stands on and no placed plant moves. The depth effect scales on top. */
  .decorItem.bigItem{ width:calc(33% * 1.10); clip-path: inset(10% 19.6% 8.3% 18.75%); }
  /* Smaller than the plant, larger than a picture frame: a banker's box read as
     an object standing in the room. Its aspect and width come from the cropped
     viewBox in ITEM_VIEWBOXES, so the element hugs the artwork instead of
     carrying a square of empty floor around with it -- that padding was grab
     area, and it made the plant hard to put down anywhere nearby. Rendered size
     is unchanged; only the invisible margin is gone. */
  .decorItem.floorBox{ width:10.455%; aspect-ratio:15.4/14.1; }
  /* String Lights (renderStringLights). The wrap has no pointer events of its
     own, so it never blocks the wall decor under it; the wire's stroke and the
     bulbs do, so the string can be grabbed by its wire and dragged up and
     down the ceiling, and a bulb still answers a click (toggleBulb). Its top
     and its scale are written by applyLightsDepth: higher is nearer and bigger. */
  .stringLightsFull{ position:absolute; top:5%; left:0; width:100%; height:10%; pointer-events:none; cursor:grab; }  /* left/width/top/height are rewritten by applyLightsDepth */
  .stringLightsFull svg{ width:100%; height:100%; overflow:visible; }
  /* The string may be wider than the room; `.stage` already has overflow:hidden (game.css:312), which is where it ends. */
  .lightWire{ fill:none; stroke:#4a4436; stroke-width:2; pointer-events:stroke; }
  .lightHook{ fill:#6b655a; pointer-events:auto; }
  .lightDrop{ stroke:#4a4436; stroke-width:1.5; }
  .lightSocket{ fill:#3a3a38; }
  .decorItem:active{ cursor:grabbing; }
  .decorItem svg{ width:100%; height:100%; overflow:visible; }

  .desk{ position:absolute; left:16%; right:16%; bottom:8%; height:32.5%; background:var(--desk-dark); border-top:4px solid var(--desk-edge); }
  .deskFront{ position:absolute; left:16%; right:16%; bottom:19.2%; height:8.5%; background:var(--desk-dark); }
  .deskFront::before{ content:""; position:absolute; left:12%; width:3px; top:15%; bottom:15%; background:var(--desk-dark); opacity:.4; }
  .deskFront::after{ content:""; position:absolute; right:12%; width:3px; top:15%; bottom:15%; background:var(--desk-dark); opacity:.4; }
  /* The desk's surface band, repainted in front of Bob. .desk sits behind him
     and so cannot occlude anything; this covers the identical span (top 59.5%
     to 72.3%, matching .desk's top edge and .deskFront's top edge) but paints
     AFTER .charwrap in the scene markup, so it is what hides his lower half.
     Bob is sized and placed against these two edges -- hands on the top, feet
     on the bottom -- so his lower half is exactly this band's height and the
     surface swallows it whole when he sits down. Keep it flush with both edges.
     This is the only part of the desk in the light var(--desk); .desk itself,
     .deskFront and the legs are all var(--desk-dark), so the body reads as one
     colour and the surface is the single lighter plane sitting on top of it. */
  .deskSurface{ position:absolute; left:16%; right:16%; bottom:27.7%; height:12.8%; background:var(--desk); }

  .keyboard{ position:absolute; left:42.9%; bottom:35.5%; width:17%; height:4%; background:var(--peripheral); border:2px solid var(--ink); }
  .keyboard::after{ content:""; position:absolute; inset:15% 8%; background:repeating-linear-gradient(90deg, var(--peripheral-key) 0 3px, transparent 3px 5px); }
  /* Flipped end-for-end: the mouse is symmetric left-to-right, so swapping the
     top and bottom corner radii is the same 180-degree rotation without a
     transform (which would spawn a stacking context for no reason). Order is
     top-left, top-right, bottom-right, bottom-left. */
  .mouse{ position:absolute; left:38.9%; bottom:35.5%; width:2.6%; height:4%; background:var(--peripheral); border:2px solid var(--ink); border-radius:20% 20% 40% 40%; }
  /* Pressing the keyboard or the mouse makes Bob type or click (game.js,
     triggerTypingBurst). The mouse is a sliver -- 16px across on a phone -- so
     it answers a little way around itself, but only a hair towards the
     keyboard, which sits just to its right. */
  .keyboard, .mouse{ cursor:pointer; }
  .mouse::after{ content:""; position:absolute; top:-8px; bottom:-8px; left:-10px; right:-3px; }
  /* This declaration IS the paper's standard spawn: resetPaperStackPosition
     clears the saved position and inline styles each new day so the stack falls
     back to exactly here, which is why the coordinates live in one place rather
     than being duplicated as a JS constant. Positioned so the stapler's own
     spawn lands on the LEFT side of the pile -- move one and the other needs
     re-measuring against it. */
  .papers{ position:absolute; left:23.18%; bottom:31.79%; width:11%; height:3%; cursor:grab; touch-action:none; }
  /* Ten stapleable sheets in the footprint the three static ones used to
     occupy. The offsets are percentages, not the concept's pixels: the whole
     stack is only ~15px tall in game, so 2.67% per sheet is what keeps the top
     sheet (24% + 72% height) inside the element. Removing sheets shortens the
     stack by ~24% overall, which reads as the pile going down even though any
     single sheet is a fraction of a pixel. */
  /* Each sheet's height on the pile, tilt and paper tone are set inline by
     renderPaperStack, derived from PAPER_SHEETS_PER_STACK. They used to be a
     hardcoded ladder of :nth-child rules, which only covered ten sheets and
     would have silently left everything above that unpositioned the moment the
     stack got deeper. The tilt arrives as a custom property rather than an
     inline transform specifically so .removing can still override the
     transform -- an inline transform would outrank the class and kill the
     stapling animation. */
  .paperSheet{
    position:absolute; left:0; right:0; height:72%; overflow:hidden;
    background:#efe9d6; border:1.5px solid var(--ink);
    transform-origin:50% 80%;
    transform:rotate(var(--tilt,0deg));
    transition:opacity 90ms ease-out, transform 110ms ease-out;
  }
  .paperSheet.removing{ opacity:0; transform:translateY(12%) scale(.97) rotate(0deg); }
  .papers.gone{ display:none; }
  /* A pile that was dragged off the delivery spot and left unfinished. Same
     look and same drag as the pile at the spot; only its position differs, and
     that is written on the element by renderPaperExtras. */
  .papersExtra{ left:auto; }
  /* Document artwork stretches to the sheet with preserveAspectRatio="none",
     so it is drawn in a 100x55 box with deliberately heavy strokes -- a sheet
     is about 76x11 real pixels, so anything under ~4 units of stroke weight
     disappears entirely at this scale. */
  .paperArt{ position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; }

  .monitor{ position:absolute; left:59%; bottom:41%; width:21.25%; height:19.55%; background:var(--monitor-bez); border:3px solid var(--ink); cursor:pointer; transform:perspective(340px) rotateY(-14deg); transform-style:preserve-3d; }
  .monitor:hover{ filter:brightness(1.08); }
  /* THE REFINED MONITOR (the owner, 28-29 September 2026, after fifteen rounds
     of mock-ups). The terminal from the severed floor, drawn flat but built to
     read as a solid, turned toward Bob: a cream case that shades to a warmer
     rim, a rear housing peeking over the top (two vents) and out to the right,
     a knob on the side, a navy well with the camera dot and the button, and a
     swivel neck on a wide flat slab. The glass is a thick, rounded tube, and the
     refining game runs edge to edge inside its shape.

     Same box the monitor has in every room (height, and so the desk and the
     click target), moved and widened by the owner's numbers: left 58.446% (60.446% at first, then 2% further left),
     bottom 39.2067%, width 23.562%, turned 29 degrees and drawn 3% smaller. The parts are children of .monitor (beta.html):
     .rPlate, .rNeck, .rHood, .rFace, .rWell, .screen, .rKnob, painted back to
     front in that order; every one is display:none in the other rooms. The
     colours are variables so the night can mix them toward the dark. */
  .monitor .rPlate, .monitor .rNeck, .monitor .rHood, .monitor .rFace, .monitor .rWell, .monitor .rKnob, .screen .rGrain{ display:none; }
  .stage[data-office="officerefined"]{
    --rc-shell:#e9e3d2; --rc-edge:#ddd6c2; --rc-hi:#f4f0e2; --rc-lo:#cfc8b1;
    --rc-well:#253252; --rc-dot:#0a1220;
  }
  /* The refining machine (the owner, 27 September 2026): the monitor opens
     nothing in Refined, so no pointing hand, no hover glow and no unread-mail
     badge; a drag on a touch screen steers the numbers instead of scrolling. */
  .stage[data-office="officerefined"] .monitor{
    cursor:default; left:58.446%; bottom:39.2067%; width:23.562%;
    transform:perspective(40cqw) rotateY(-29deg) scale(.97);
    background:transparent; border:0; border-radius:0; box-shadow:none;
  }
  .stage[data-office="officerefined"] .monitor:hover{ filter:none; }
  .stage[data-office="officerefined"] .monitor .mailBadge{ display:none !important; }
  .stage[data-office="officerefined"] .monitor .screen{ touch-action:none; }
  .stage[data-office="officerefined"] .monitor::before{ content:none; }
  .stage[data-office="officerefined"] .monitor .stand,
  .stage[data-office="officerefined"] .monitor .foot{ display:none; }
  /* The base: a slab lying on the desk, a sliver under the case, a short neck. */
  .stage[data-office="officerefined"] .monitor .rPlate{ display:block; position:absolute; left:50%; bottom:-11.5%; width:76%; height:10.5%; transform:translateX(-50%); overflow:visible; }
  .stage[data-office="officerefined"] .monitor .rPlate polygon, .stage[data-office="officerefined"] .monitor .rPlate path{ stroke:#2a2721; stroke-width:2; vector-effect:non-scaling-stroke; stroke-linejoin:round; }
  .stage[data-office="officerefined"] .monitor .rPlate .top{ fill:var(--rc-shell); }
  .stage[data-office="officerefined"] .monitor .rPlate .lip{ fill:var(--rc-lo); }
  .stage[data-office="officerefined"] .monitor .rPlate .shine{ fill:var(--rc-hi); stroke:none; opacity:.7; }
  .stage[data-office="officerefined"] .monitor .rNeck{
    display:block; position:absolute; left:50%; bottom:-3%; width:27%; height:9%; transform:translateX(-50%);
    border:2px solid #2a2721; border-radius:30% / 22%;
    background:linear-gradient(90deg, var(--rc-edge), var(--rc-hi) 38%, var(--rc-shell) 60%, var(--rc-lo));
  }
  /* The rear housing: only its top edge and its right side show past the case,
     which is what gives the flat drawing its depth. Half the height it had at
     the start, at the owner's word. */
  .stage[data-office="officerefined"] .monitor .rHood{
    display:block; position:absolute; left:10%; top:-3.5%; width:88%; height:98%;
    border:3px solid #2a2721; border-radius:13% 15% 10% 10% / 18% 18% 12% 12%;
    background:linear-gradient(180deg, var(--rc-shell) 0%, var(--rc-lo) 100%);
  }
  .stage[data-office="officerefined"] .monitor .rHood i{ position:absolute; top:.3%; height:1.8%; background:#2a2721; opacity:.8; transform:skewX(-24deg); border-radius:2px; }
  .stage[data-office="officerefined"] .monitor .rHood i:nth-child(1){ left:20%; width:26%; }
  .stage[data-office="officerefined"] .monitor .rHood i:nth-child(2){ left:54%; width:26%; }
  /* The front: a rim that shades from a light cream in the middle to a warmer
     one at the edge, then the navy well the glass sits in. */
  .stage[data-office="officerefined"] .monitor .rFace{
    display:block; position:absolute; inset:0; border:3px solid #2a2721;
    border-radius:10% 10% 9% 9% / 12% 12% 11% 11%;
    background:radial-gradient(ellipse 90% 85% at 42% 36%, var(--rc-hi) 0%, var(--rc-shell) 52%, var(--rc-edge) 100%);
    box-shadow:inset 0 0 0 2px var(--rc-edge), inset 0 -4px 0 var(--rc-lo), inset 0 2px 0 rgba(255,255,255,.6);
  }
  .stage[data-office="officerefined"] .monitor .rWell{
    display:block; position:absolute; inset:5% 5% 6% 5%; pointer-events:none;
    border-radius:8% 8% 7% 7% / 10% 10% 9% 9%;
    background:radial-gradient(circle at 50% 6%, var(--rc-dot) 1.6%, transparent 2.2%), radial-gradient(circle at 91% 87%, var(--rc-dot) 2.2%, transparent 3%), var(--rc-well);
    box-shadow:inset 0 2px 0 rgba(0,0,0,.3);
  }
  .stage[data-office="officerefined"] .monitor .rKnob{
    display:block; position:absolute; right:-6%; top:32%; width:8.5%; height:26%;
    border:2px solid #2a2721; border-radius:42% / 30%;
    background:linear-gradient(90deg, #4a505a, #0d1014 72%); box-shadow:inset 0 1px 0 rgba(255,255,255,.18);
  }
  /* THE GLASS. A thick, rounded tube: a bright edge, a dark ring inside it, a
     soft fall-off toward the corners, a highlight at the top left; over the
     game, scan lines, a vignette, a faint wash of light and a little grain
     (the owner: "very subtle, but just enough to sell an old tube TV"). The
     game (game.js, refinementGridSvg) fills it edge to edge, so the rounded
     corners are the game's corners. 10% brighter, and softened by a glow round
     the numbers -- "it's too crisp". Sized as the owner set it: 58.125% x
     70.47% of the case, 14.69% in from the left, 15.32% down. */
  .stage[data-office="officerefined"] .monitor .screen{
    left:14.6875%; top:15.315%; width:58.125%; height:70.47%; right:auto; bottom:auto;
    border-radius:13% / 17%; background:#04100f;
    box-shadow:
      0 0 1.5cqw .2cqw rgba(127,208,220,.20),
      0 0 0 .18cqw #0c1426,
      0 0 0 .45cqw rgba(255,255,255,.10),
      inset 0 0 0 .2cqw #d5dcde,
      inset 0 0 0 .55cqw #0a1c22,
      inset 0 0 1.6cqw rgba(0,0,0,.95),
      inset .35cqw .5cqw .7cqw rgba(255,255,255,.14);
  }
  .stage[data-office="officerefined"] .monitor .screen svg{
    position:absolute; inset:0; width:100%; height:100%;
    /* Its own layer: the numbers move every frame, and without this the case, the
       well and everything else in the monitor was redrawn with them (measured 29
       September 2026: half the drawing work, and the stutter when dragging an item
       across the base). */
    will-change:transform;
    filter:brightness(1.1) blur(.035cqw) drop-shadow(0 0 .12cqw rgba(127,208,220,.55)) drop-shadow(0 0 .5cqw rgba(127,208,220,.30));
  }
  .stage[data-office="officerefined"] .monitor .screen::before{
    content:''; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
    background:
      radial-gradient(ellipse 60% 34% at 30% 12%, rgba(255,255,255,.22), rgba(255,255,255,0) 100%),
      radial-gradient(ellipse 72% 66% at 50% 50%, rgba(140,225,235,.08), rgba(140,225,235,0) 100%),
      radial-gradient(ellipse 100% 100% at 50% 50%, rgba(0,0,0,0) 48%, rgba(0,0,0,.48) 100%),
      repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, rgba(0,0,0,0) 1px 3px);
  }
  .stage[data-office="officerefined"] .monitor .screen::after{
    content:''; position:absolute; inset:0; pointer-events:none; border-radius:inherit; box-shadow:inset 0 0 2.4cqw rgba(0,0,0,.6);
  }
  .stage[data-office="officerefined"] .monitor .screen .rGrain{
    display:block; position:absolute; inset:0; pointer-events:none; border-radius:inherit; opacity:.11; mix-blend-mode:screen;
    background-size:150px 150px;
    background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='150' height='150'><filter id='n' x='0' y='0' width='100%' height='100%'><feTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='saturate' values='0'/><feComponentTransfer><feFuncR type='linear' slope='1.8' intercept='-.55'/><feFuncG type='linear' slope='1.8' intercept='-.55'/><feFuncB type='linear' slope='1.8' intercept='-.55'/></feComponentTransfer></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  }
  /* The numbers move every frame, and a glow redrawn every frame is the one
     costly thing here: on the phone layout the screen keeps its brightness and
     drops the blur and the halos. */
  body.phoneLayout .stage[data-office="officerefined"] .monitor .screen svg{ filter:brightness(1.1); }
  /* On the phone layout the room is cropped to the middle of the desk, between
     the paper stack and the TPS box, and there is only 134px there between the
     keyboard and the edge. The monitor is drawn at 88% (the owner: 10% bigger than
     the 80% it had) and turned back to 14 degrees instead of 29 ("too distorted"
     on a phone). The slant of the top edge was still too much, so the perspective
     is weakened until it is half what it was (2.6 degrees down to 1.3 on the glass's
     top edge): 80% of the width, where the desktop's is 40%. Placed 3% of the
     play area up from where it was in the second round of phone changes (2%,
     2%, 1% and 2% back down, about 4.8px per 1% on a 390px-wide phone) and 2.5% to the right
     (9.75px, in two moves), which puts the knob a few pixels past the edge of
     the room (the owner asked for that, about half the knob) and the case 4px clear of
     the keyboard's end. The turn's perspective is a share of the width, so it looks the same at any size. */
  body.phoneLayout .stage[data-office="officerefined"] .monitor{ left:58.41%; bottom:38.3%; transform:perspective(80cqw) rotateY(-14deg) scale(.88); }
  /* The console: navy all over and one unit -- the keyboard's slab reaches
     left over the mouse's box (its ::before) with no seam -- with the raised
     keys of keys-refined.svg in it and a small dark ball where the mouse was,
     nothing round it. Neither box moves and both stay clickable;
     .mouse::after, the hit-area extender, is untouched. */
  .stage[data-office="officerefined"] .keyboard{
    background:#253252; border-color:#2a2721; border-radius:4px; overflow:visible;
    box-shadow: inset 0 -3px 0 rgba(0,0,0,0.28), inset 0 2px 0 rgba(255,255,255,0.14);
  }
  .stage[data-office="officerefined"] .keyboard::before{
    content:''; position:absolute; top:-2px; bottom:-2px; left:-31%; width:31%; background:#253252;
    border:2px solid #2a2721; border-right:0; border-radius:4px 0 0 4px;
    box-shadow: inset 0 -3px 0 rgba(0,0,0,0.28), inset 0 2px 0 rgba(255,255,255,0.14);
  }
  .stage[data-office="officerefined"] .keyboard::after{
    inset:12% 3%; background:url("/play/skins/keys-refined.svg") center/100% 100% no-repeat;
  }
  .stage[data-office="officerefined"] .mouse{ background:transparent; border:0; border-radius:0; box-shadow:none; }
  .stage[data-office="officerefined"] .mouse::before{
    content:''; position:absolute; left:20%; top:18%; width:60%; height:60%; border-radius:50%; box-sizing:border-box;
    background:radial-gradient(circle at 36% 30%, #4a545e, #0b0f13 72%); border:1px solid #2a2721; box-shadow:0 1px 0 rgba(0,0,0,0.35);
  }
  /* The Refined clock. Three designs, cycled by a click (clockPressed):
     blue hands, teal hands, and a light grey face with black hands. By day a
     small soft shadow falls down and a little right ("roughly 5:15pm, so more
     bottom than right" -- the owner). At night the two coloured designs keep
     their face and let only the HANDS glow, in their own colour, inside the
     dial; the grey design has no glow and dims like anything else. */
  .stage[data-office="officerefined"] .clockwrap{ filter:drop-shadow(1px 3px 2px rgba(0,0,0,0.28));
    /* Its own layer, and no shadow while it is in the hand: a drop-shadow filter on
       something moved by left/top left grey streaks on the white cubicle walls on
       iPhone Safari, which repaints the filter's old footprint late. */
    will-change:transform; }
  .stage[data-office="officerefined"] .clockwrap.freeDragging{ filter:none; }
  .stage[data-office="officerefined"] .clockwrap .face{ fill:#1f1f1f; stroke:#f2f0ea; stroke-width:6; }
  .stage[data-office="officerefined"] .clockwrap .hand{ stroke:#2f8fdc; }
  .stage[data-office="officerefined"] .clockwrap #hourHand{ stroke-width:4; }
  .stage[data-office="officerefined"] .clockwrap #minHand{ stroke-width:3; }
  .stage[data-office="officerefined"] .clockwrap .cap{ fill:#2f8fdc; }
  .stage[data-office="officerefined"] .clockwrap[data-clock-design="teal"] .hand{ stroke:#3fbfa8; }
  .stage[data-office="officerefined"] .clockwrap[data-clock-design="teal"] .cap{ fill:#3fbfa8; }
  .stage[data-office="officerefined"] .clockwrap[data-clock-design="grey"] .face{ fill:#d9d9d6; stroke:#f6f6f3; }
  .stage[data-office="officerefined"] .clockwrap[data-clock-design="grey"] .hand{ stroke:#1f1f1f; }
  .stage[data-office="officerefined"] .clockwrap[data-clock-design="grey"] .cap{ fill:#1f1f1f; }
  .monitor .screen{ position:absolute; inset:14%; background:var(--monitor); overflow:hidden; container-type:size; }
  /* The screensaver's word (play/game.js saverAnimate): laid over the glass and moved by the browser's own animation. Sized and placed in the glass's units (cqh/cqw). */
  .saverWord{ position:absolute; left:0; top:0; z-index:2; pointer-events:none; white-space:nowrap; line-height:1; color:#8fe0a0; font-family:'Press Start 2P', monospace; will-change:transform; }
  .monitor .screen svg{ width:100%; height:100%; }
  /* The monitor's base. A child of .monitor, so it picks up the same
     perspective/rotateY skew and stays put if the monitor ever moves. Sized by
     what actually renders rather than by the raw numbers: the perspective
     transform means the layout box and the painted box differ, and most of the
     element sits hidden behind the monitor body. bottom:-14%/height:14% puts
     1.89 points of it below the monitor at 5.12 wide -- the ~2%x5% asked for --
     with its bottom edge at 61.6%, resting on the desk surface (top 59.5%).
     .monitor paints after .deskSurface in the scene markup, without which the
     surface buries the base. */
  .monitor .stand{ position:absolute; left:50%; bottom:-14%; width:18.3%; height:14%; background:var(--ink); transform:translateX(-50%); }
  /* The foot the stand rests on, twice the stand's width and ~1.5 points tall.
     Also a child of .monitor, so it inherits the same skew. Like the stand,
     these numbers are chosen by what renders, not by the arithmetic: at
     width:36.8%/height:5.7% it paints 7.25 wide by 1.53 tall, against the
     stand's 3.6. It overlaps the stand's bottom edge slightly so the two
     read as one piece, and its own bottom lands at 62.3%, on the surface. */
  .monitor .foot{ position:absolute; left:50%; bottom:-20%; width:36.8%; height:5.7%; background:var(--ink); transform:translateX(-50%); }
  .mailBadge{ position:absolute; top:-9px; right:-9px; background:var(--danger); color:#fff; border:2px solid var(--ink); border-radius:50%; width:20px; height:20px; font-size:12px; display:flex; align-items:center; justify-content:center; font-family:'Space Mono',monospace; font-weight:700; }

  /* Matches .deskSurface exactly, so a desk toy can only ever be placed on the
     desk's surface -- the same way .decorZone bounds wall decor to the cubicle
     wall. Keep these four values in sync with .deskSurface. */
  .toyZone{ position:absolute; left:16%; right:16%; bottom:27.7%; height:12.8%; pointer-events:none; }
  .toyItem{ position:absolute; width:9%; aspect-ratio:1/1; cursor:grab; touch-action:none; user-select:none; pointer-events:auto; }
  /* The Pen Holder is the one desk toy issued narrower than the standard 9%.
     It ships equipped on every new career and has to share the strip of desk
     left of the keyboard with the paper stack and the stapler, and at 9% there
     is no gap on that strip wide enough to take it (3.27% left of the papers,
     1.47% between the stapler and the mouse, against a 6.07%-of-stage box).
     4.8% fits the left gap exactly, and a narrow pen cup is the right shape for
     the object anyway -- the static pen holder element this replaced was
     hand-sized at 4.5% of the stage for the same reason. Since raised from that
     original 4.8% by a quarter, so it reads at desk scale rather than as a
     thimble; it no longer fits the gap left of the paper stack, which is fine
     now that it isn't placed on the desk by default. */
  /* aspect-ratio matches the icon's 20x28 viewBox, so the cup FILLS its box
     instead of letterboxing inside the default square one. That is what makes
     it render 20% wider and 40% taller than the old 20x24-in-a-square version
     at the same 4.8% width -- the extra width was already there as dead space. */
  .toyItem[data-item-id="penholder"]{ width:6%; aspect-ratio:20/28; }
  /* The Desk Calendar: a page-a-day block, drawn 44x50. */
  .toyItem[data-item-id="deskcalendar"]{ width:5.4%; aspect-ratio:44/50; }
  /* Sizes against the rest of the desk (the owner, 16 September 2026): the
     Fidget Cube and the Rubber Duck read too big -- a tenth smaller, then the
     cube 5% and the duck 10% smaller again; the Desk Plant a twentieth bigger,
     and its Marketplace picture with it. */
  .toyItem[data-item-id="fidget"]{ width:7.7%; }
  .toyItem[data-item-id="duck"]{ width:7.29%; }
  .toyItem[data-item-id="succulent"]{ width:9.45%; }

  /* Red Stapler. The top presses in and the base lifts a fraction, so the thing
     compresses like a real stapler without its resting silhouette moving at all.
     transform-box:view-box means these px values are viewBox user units -- 24ths
     of the icon -- which is why sub-pixel numbers are the right magnitude here. */
  .staplerTop, .staplerBase{ transform-box:view-box; }
  .toyItem.clacking .staplerTop{ animation:staplerTopClack 150ms cubic-bezier(.35,0,.25,1); }
  .toyItem.clacking .staplerBase{ animation:staplerBaseClack 150ms cubic-bezier(.35,0,.25,1); }
  @keyframes staplerTopClack{
    0%{ transform:translateY(0); } 28%{ transform:translateY(.78px); }
    48%{ transform:translateY(.78px); } 75%{ transform:translateY(-.10px); }
    100%{ transform:translateY(0); }
  }
  @keyframes staplerBaseClack{
    0%{ transform:translateY(0); } 28%{ transform:translateY(-.18px); }
    48%{ transform:translateY(-.18px); } 75%{ transform:translateY(.04px); }
    100%{ transform:translateY(0); }
  }

  /* Rubber Duck. The body takes most of the squeeze; the head compresses less
     and settles toward the body, then both overshoot on release. fill-box so
     each group scales about its own centre rather than the icon's origin. */
  .duckBody, .duckHead{ transform-box:fill-box; transform-origin:center; }
  .toyItem.squeezing .duckBody{ transform:scaleX(.90) scaleY(.82); }
  .toyItem.squeezing .duckHead{ transform:translate(-.25px,1.05px) scaleX(.96) scaleY(.92); }
  .toyItem.releasing .duckBody{ animation:duckBodyRelease .34s cubic-bezier(.2,.8,.25,1); }
  .toyItem.releasing .duckHead{ animation:duckHeadRelease .34s cubic-bezier(.2,.8,.25,1); }
  @keyframes duckBodyRelease{
    0%{ transform:scaleX(.90) scaleY(.82); } 48%{ transform:scaleX(1.035) scaleY(1.055); }
    72%{ transform:scaleX(.985) scaleY(.98); } 100%{ transform:scaleX(1) scaleY(1); }
  }
  @keyframes duckHeadRelease{
    0%{ transform:translate(-.25px,1.05px) scaleX(.96) scaleY(.92); }
    48%{ transform:translate(.1px,-.25px) scaleX(1.015) scaleY(1.025); }
    72%{ transform:translate(0,.08px) scaleX(.995) scaleY(.995); }
    100%{ transform:translate(0,0) scaleX(1) scaleY(1); }
  }

  /* Mini Zen Garden. The rake sweeps left-to-right across the tray; the jostle
     is applied to the inner <svg>, NOT the .toyItem wrapper, because the
     wrapper carries the translate(-50%,-50%) centring that syncMultiSlot sets
     inline -- animating the wrapper would fight it and teleport the tray. */
  .sandLine{ fill:none; stroke:#8d7348; stroke-width:.72; stroke-linecap:round; stroke-linejoin:round; }
  .zenStone{ transition:transform .25s ease; transform-box:fill-box; transform-origin:center; }
  .zenRake{ transform-origin:8px 7px; transform-box:view-box; }
  .zenRake.raking{ animation:zenRakeSweep .52s cubic-bezier(.35,.1,.25,1); }
  @keyframes zenRakeSweep{
    0%{ transform:translateX(0) rotate(0); } 16%{ transform:translateX(-1.2px) rotate(-7deg); }
    72%{ transform:translateX(8.5px) rotate(5deg); } 100%{ transform:translateX(0) rotate(0); }
  }
  .zenJostle{ animation:zenJostle .24s ease-out; }
  @keyframes zenJostle{
    0%{ transform:rotate(0); } 22%{ transform:rotate(-1.5deg); }
    48%{ transform:rotate(1.2deg); } 75%{ transform:rotate(-.5deg); } 100%{ transform:rotate(0); }
  }

  /* Pen Holder. will-change keeps the per-frame rattle off the main raster
     path; the rest of each pen's transform is written inline by makePenHolder. */
  .pen{ transform-box:view-box; will-change:transform; }

  /* String Lights. A failing bulb is not a glow or an opacity blink -- it is the
     flat fill stepping to a darker variant and back, which is the only thing
     that reads as "broken" in this art style. steps(1,end) keeps every change
     an instant cut rather than a fade. */
  .stringLightBulb{ pointer-events:auto; cursor:pointer; }
  .bulbCore{ fill:var(--bulb-color); stroke:#201d16; stroke-width:1.5; }
  .stringLightBulb.bad .bulbCore{ animation:badBulbFlicker 1.35s steps(1,end) infinite; }
  @keyframes badBulbFlicker{
    0%{ fill:var(--bulb-color); }  8%{ fill:var(--bulb-dark); } 15%{ fill:var(--bulb-color); }
    23%{ fill:var(--bulb-mid); }  29%{ fill:var(--bulb-color); } 51%{ fill:var(--bulb-color); }
    56%{ fill:var(--bulb-dark); } 62%{ fill:var(--bulb-color); } 68%{ fill:var(--bulb-mid); }
    74%{ fill:var(--bulb-color); } 100%{ fill:var(--bulb-color); }
  }
  /* One tiny voltage wobble when a bulb is clicked back to life, then solid. */
  .stringLightBulb.fixedFlash .bulbCore{ animation:fixedBulbSettle 280ms steps(1,end) 1; }
  @keyframes fixedBulbSettle{
    0%{ fill:var(--bulb-dark); } 28%{ fill:var(--bulb-color); } 46%{ fill:var(--bulb-mid); }
    64%{ fill:var(--bulb-color); } 100%{ fill:var(--bulb-color); }
  }
  .toyItem:active{ cursor:grabbing; }
  .toyItem svg{ width:100%; height:100%; overflow:visible; }
  /* Fidget Cube solve/scramble feedback (makeFidgetCubeGame) -- applied to
     the svg itself, not the .toyItem wrapper, since the wrapper's own
     transform:translate(-50%,-50%) is what keeps it centered on its
     left/top position and an animation here would fight that. */
  @keyframes cubeSolveKick{ 0%{ transform:rotate(0deg) scale(1); } 45%{ transform:rotate(-2deg) scale(1.03); } 100%{ transform:rotate(0deg) scale(1); } }
  @keyframes cubeScrambleKick{ 0%{ transform:rotate(0deg); } 20%{ transform:rotate(-6deg); } 45%{ transform:rotate(5deg); } 70%{ transform:rotate(-3deg); } 100%{ transform:rotate(0deg); } }
  .cubeSolveKick{ animation:cubeSolveKick .18s ease-out; }
  .cubeScrambleKick{ animation:cubeScrambleKick .34s ease-out; }

  /* Bob's mug while he is holding it but not mid-sip -- NOT a desk object.
     It is placed against the straight-arm left hand (the same hand that grips
     #pCupInHand during a sip), measured at 46.2%/57.36% with Bob at the desk,
     so it has to move whenever .charwrap does. Parked anywhere else it reads
     as a stray mug sitting on the desk. Note the mug is not centred on the
     hand: within its 20-wide viewBox the body is x2-16 and the handle x16-20,
     so `left` is offset to bring the HANDLE onto the hand and hang the body
     clear to its left, rather than covering the hand with the mug's body. */
  .cup{ position:absolute; width:4%; height:7%; transition:left .5s cubic-bezier(.4,0,.3,1), bottom .5s cubic-bezier(.4,0,.3,1), transform .5s ease; left:42.2%; bottom:39.2%; display:none; }
  .cup.show{ display:block; }
  .cup svg{ width:100%; height:100%; overflow:visible; }

  /* left matches the walk-in start in applyPose(); every seated pose overrides
     it inline, so this only shows for the frame before the first pose applies. */
  .charwrap{ position:absolute; bottom:26.92%; left:-2%; width:26%; pointer-events:none; }
  .chairEl{ position:absolute; bottom:37.92%; left:39.375%; width:11%; pointer-events:none; }
  .chairEl svg{ width:100%; display:block; overflow:visible; }
  .deskLeg{ position:absolute; bottom:0; width:2.4%; background:var(--desk-dark); border:2px solid var(--desk-edge); }
  .charwrap svg{ width:100%; overflow:visible; display:block; }
  .charwrap.hidden{ visibility:hidden; }
  /* Bob is inert except his head, which toggles the headphones. A descendant can
     opt back in even though .charwrap is pointer-events:none, so this is the only
     clickable part of him. .decorZone paints before .charwrap, so his head is
     already drawn in front of wall decor and now takes the click there too.
     visibleFill, not all: the *visible* values respect visibility, so the hit
     target goes dead with .charwrap.hidden (the 'offscreen' pose) instead of
     leaving a clickable ghost of Bob's head behind. The Fill part means the
     transparent interior is still a target, which a paint-based value would
     refuse. */
  .charwrap #pHeadHit{ pointer-events:visibleFill; cursor:pointer; }
  /* The tap lifts the arm rather than pressing it down. Bob's hands rest at
     59.35% and .deskSurface starts at 59.39% and paints in front of him, so
     there is only 0.04 of headroom below them -- pressing down 3px (0.87 stage
     points) buried 0.83 of each hand behind the desk on every keystroke. Going
     up keeps the whole hand visible and still reads as typing, since it returns
     to rest on the keys. Do not flip these back to a positive translateY
     without first giving the hands room to travel into. */
  @keyframes typeTapA{ 0%{ transform:translateY(0); } 35%{ transform:translateY(-3px); } 100%{ transform:translateY(0); } }
  @keyframes typeTapB{ 0%{ transform:translateY(0); } 35%{ transform:translateY(-3px); } 100%{ transform:translateY(0); } }
  #pArmR.typingBurstA, #pArmL.typingBurstA{ animation:typeTapA 0.22s ease-out; }
  #pArmR.typingBurstB, #pArmL.typingBurstB{ animation:typeTapB 0.22s ease-out; }
  @keyframes spin{ to{ transform:rotate(360deg); } }
  .swirl{ animation:spin 0.9s linear infinite; }

  /* Sleeping z's: each one rises off Bob's head, drifts right, grows a little
     and fades. All three share one path and one duration; the stagger below is
     what turns it into a stream instead of a pulse. transform-box:fill-box so
     the scale grows from each glyph's own centre rather than the SVG origin --
     without it they slide away toward the corner as they grow. */
  @keyframes sleepZFloat{
    0%   { opacity:0; transform:translate(0,0) scale(.55); }
    18%  { opacity:1; }
    70%  { opacity:.85; }
    100% { opacity:0; transform:translate(15px,-27px) scale(1.3); }
  }
  /* backwards fill matters: without it the two delayed z's sit fully opaque and
     motionless on top of the first one until their delay elapses, so the effect
     opens with three stacked z's that suddenly jump into motion. */
  .sleepZ{ animation:sleepZFloat 2.7s linear infinite; animation-fill-mode:backwards; transform-box:fill-box; transform-origin:50% 50%; }
  .sleepZ1{ animation-delay:0.9s; }
  .sleepZ2{ animation-delay:1.8s; }

  .bubble{ position:absolute; background:var(--panel); color:var(--text); border:2px solid var(--ink); padding:3px 7px; font-size:13px; white-space:nowrap; opacity:0; pointer-events:none; z-index:5; }
  .bubble.play{ animation:bubblefloat 3.2s ease-out forwards; }
  @keyframes bubblefloat{ 0%{opacity:0; transform:translateY(0);} 12%{opacity:1; transform:translateY(-4px);} 80%{opacity:1; transform:translateY(-14px);} 100%{opacity:0; transform:translateY(-20px);} }

  .closedOverlay{ position:absolute; inset:0; background:rgba(10,10,8,0.72); display:none; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:10%; z-index:6;}
  #clockInBtn{ position:relative; z-index:2; }
  .closedOverlay.show{ display:flex; }
  .closedOverlay .big{ font-size:22px; color:var(--text); margin-bottom:8px; }
  .closedOverlay .small{ font-size:16px; color:var(--text-dim); }
  /* Sized to sit with the EMPTY CUBICLE line rather than shout over it -- this
     is a way out of a dead screen, not a call to action competing with it. */
  #clockInBtn{ margin-top:20px; font-size:12px; padding:10px 14px; }
  /* THE SAME BUTTON ON A PHONE. This used to be a set shared with AFTER HOURS
     AUTHORIZATION and RETURN TO TITLE MENU, both gone with the rest of the
     scrim (see renderAwayBar); CLOCK IN is short enough to need none of the
     one-width-for-the-set or step-down-the-type machinery that pair needed,
     but it keeps the title menu's look and the 44px thumb floor. */
  body.phoneLayout #clockInBtn{
    width:max-content; max-width:100%;
    font-size:clamp(10px, 2.4vw, 13px);
    padding:14px;
    min-height:44px;
    white-space:nowrap;
  }

  .hud{ padding:calc(var(--bar) * 0.5882) calc(var(--bar) * 0.2353) calc(var(--bar) * 0.1176); }
  .energyrow{ display:flex; align-items:center; gap:calc(var(--bar) * 0.4706); margin-bottom:calc(var(--bar) * 0.4706); }
  .barwrap{ flex:1; height:calc(var(--bar) * 1.294); background:var(--panel-light); border:calc(var(--bar) * 0.1176) solid var(--ink); position:relative; overflow:visible; }
  .barfill{ position:absolute; left:0; top:0; bottom:0; width:0%; background:var(--good); }
  .barfill.zone-red-low{ background:var(--danger); }
  .barfill.zone-yellow-low{ background:#d9c23a; }
  .barfill.zone-green{ background:var(--good); }
  .barfill.zone-yellow-high{ background:#d9c23a; }
  .barfill.zone-red-high{ background:var(--danger); animation:pulse 1s infinite; }
  .barfill.zone-candy{ background-image:repeating-linear-gradient(45deg, #c23b3b 0 8px, #f4f1e8 8px 16px); animation:pulse .4s infinite; }
  @keyframes pulse{ 0%,100%{filter:brightness(1);} 50%{filter:brightness(1.35);} }
  .barmark100{ position:absolute; top:calc(var(--bar) * -0.2353); bottom:calc(var(--bar) * -0.2353); width:calc(var(--bar) * 0.1176); background:var(--ink); left:66.6%; }
  /* Dev builds only -- the class is added by enableEnergyBarScrubbing(), which
     never runs outside dev, so the live game's bar keeps the default cursor. */
  .barwrap.devScrub{ cursor:ew-resize; }
  /* Two stacked lines now (energy, then the live earning rate), so it needs a
     wider column than the old bare "25%" and no longer sits on the bar's
     centre line -- align to the top so the energy figure stays level with the
     bar it belongs to. */
  .barlabel{ font-size:calc(var(--bar) * 0.9412); min-width:calc(var(--bar) * 6.588); text-align:right; align-self:flex-start; line-height:1.15; }
  .barlabelLine{ white-space:nowrap; }
  /* The rate carries its own warning. Red means Bob is earning literally
     nothing right now -- asleep, crashed out, off the clock, or past the daily
     cap -- which is the one state a player needs to notice without reading.
     Green is the band worth aiming for. Everything in between stays plain, so
     the colour only ever means something. */
  /* No font-size of its own: it inherits .barlabel's 16px so the two lines
     match. It used to be 14px, which read as a footnote to the energy
     percentage rather than the other half of the same readout -- and the
     earn rate is the number the whole game turns on. */
  .barlabelRate{ color:var(--text); }
  .barlabelRate.rateZero{ color:var(--danger); }
  .barlabelRate.rateHigh{ color:var(--good); }
  .statusrow{ font-size:calc(var(--bar) * 0.9412); color:var(--text-dim); margin-bottom:calc(var(--bar) * 0.5882); min-height:calc(var(--bar) * 1.176); }
  .coffeebtn{ display:block; width:100%; background:var(--panel-light); border:calc(var(--bar) * 0.1765) solid var(--ink); color:var(--text); font-family:'Press Start 2P',monospace; font-size:calc(var(--bar) * 0.7059); padding:calc(var(--bar) * 0.8235) calc(var(--bar) * 0.5882); cursor:pointer; box-shadow:calc(var(--bar) * 0.2353) calc(var(--bar) * 0.2353) 0 var(--ink); letter-spacing:calc(var(--bar) * 0.0588); }
  .coffeebtn:not(:disabled):hover{ background:#3d3f31; }
  .coffeebtn:not(:disabled):active{ transform:translate(calc(var(--bar) * 0.2353), calc(var(--bar) * 0.2353)); box-shadow:none; }
  .coffeebtn:disabled{ opacity:.5; cursor:default; }

  /* THE DESKTOP DASHBOARD (the owner, 27 September 2026): the desk's bottom
     panel laid out like the phone's -- an ENERGY tile (the number over its
     word, the bar beside it, Bob's condition under the bar), a CV/HR tile of
     its own, and GET COFFEE across both -- "the 3 of them kinda just sit
     neatly". The same elements as ever, placed on a grid; the phone's rules
     (body.phoneLayout, further up) still win on a phone. Sized in --bar like
     everything else here, and the two tiles are drawn the way GET COFFEE is --
     the same border and the same drop shadow -- so the three read as a set. */
  body:not(.phoneLayout) .hud{
    display:grid; grid-template-columns:auto minmax(0, 1fr) calc(var(--bar) * 6.2);
    column-gap:calc(var(--bar) * 0.5882); grid-template-rows:auto auto auto;
  }
  body:not(.phoneLayout) .energyrow, body:not(.phoneLayout) .barlabel{ display:contents; }
  body:not(.phoneLayout) .hud::before, body:not(.phoneLayout) .hud::after{
    content:""; background:var(--panel-light); border:calc(var(--bar) * 0.1765) solid var(--ink);
    box-shadow:calc(var(--bar) * 0.2353) calc(var(--bar) * 0.2353) 0 var(--ink);
  }
  body:not(.phoneLayout) .hud::before{ grid-area:1 / 1 / 3 / 3; }
  body:not(.phoneLayout) .hud::after{ grid-area:1 / 3 / 3 / 4; }
  body:not(.phoneLayout) #barLabelEnergy,
  body:not(.phoneLayout) #barWrap,
  body:not(.phoneLayout) #statusRow,
  body:not(.phoneLayout) #barLabelRate{ position:relative; z-index:1; }
  /* The number over the word ENERGY, the way the rate sits over CV/HR, its top
     level with the top of the bar. */
  body:not(.phoneLayout) #barLabelEnergy{
    grid-area:1 / 1 / 3 / 2; align-self:start; display:flex; flex-direction:column; align-items:center; gap:calc(var(--bar) * 0.35);
    padding:calc(var(--bar) * 0.8235) 0 0 calc(var(--bar) * 1.06); line-height:1;
  }
  body:not(.phoneLayout) #barLabelEnergy::after,
  body:not(.phoneLayout) #barLabelRate::after{
    font-family:'Space Mono',monospace; font-size:calc(var(--bar) * 0.68); font-weight:700; letter-spacing:calc(var(--bar) * 0.06);
    color:var(--text-dim); line-height:1;
  }
  body:not(.phoneLayout) #barLabelEnergy::after{ content:"ENERGY"; }
  body:not(.phoneLayout) #barLabelRate::after{ content:"CV/HR"; }
  body:not(.phoneLayout) #barWrap{ grid-area:1 / 2; align-self:end; margin:calc(var(--bar) * 0.8235) calc(var(--bar) * 1.06) 0 calc(var(--bar) * 0.7); flex:none; }
  body:not(.phoneLayout) .barmark100{ top:0; bottom:0; }
  body:not(.phoneLayout) #statusRow{
    grid-area:2 / 2; margin:0; padding:calc(var(--bar) * 0.4) calc(var(--bar) * 1.06) calc(var(--bar) * 0.7) calc(var(--bar) * 0.7);
    text-align:center; line-height:1.1; min-height:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  body:not(.phoneLayout) #barLabelRate{
    grid-area:1 / 3 / 3 / 4; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:calc(var(--bar) * 0.35); white-space:normal;
  }
  body:not(.phoneLayout) .barlabelWord{ display:none; }
  body:not(.phoneLayout) .barlabelNum{ font-family:'Press Start 2P',monospace; font-size:calc(var(--bar) * 0.95); line-height:1; color:var(--gold); text-shadow:calc(var(--bar) * 0.12) calc(var(--bar) * 0.12) 0 var(--ink); }
  body:not(.phoneLayout) .barlabelRate.rateZero .barlabelNum{ color:var(--danger); }
  body:not(.phoneLayout) .barlabelRate.rateHigh .barlabelNum{ color:var(--good); }
  body:not(.phoneLayout) #coffeeBtn{ grid-area:3 / 1 / 4 / 4; margin-top:calc(var(--bar) * 0.7); }

  /* ---- Bob away (renderAwayBar) ---- */
  .hud{ position:relative; }
  .hud.away .energyrow, .hud.away .barlabel, .hud.away #statusRow, .hud.away #coffeeBtn{ visibility:hidden; } /* space kept, nothing moves */
  .hud.away::before, .hud.away::after{ visibility:hidden; }
  /* The countdown and the Overtime / Incident Report button are both drawn
     exactly as Get Coffee is (.coffeebtn, no cup) and sized to its box. The
     countdown is a button too: pressing it opens the Working Hours card (the
     owner, 26 September 2026, in place of a "?" beside it). */
  .awayRow, .awayBtn{ position:absolute; margin:0; display:flex; align-items:center; justify-content:center; box-sizing:border-box; white-space:nowrap; color:var(--gold); }
  .awayRow[hidden], .awayBtn[hidden]{ display:none; }
  .awayBtn.overtime{ background:var(--good); color:var(--ink); }
  .awayBtn.incident{ background:var(--danger); color:var(--ink); }
  .awayRow.er{ font-size:calc(var(--bar) * 0.62); }   /* the long line: still one line, only a little smaller */
  body.phoneLayout .awayRow, body.phoneLayout .awayBtn{ border-left:0; border-right:0; box-shadow:none; }   /* as Get Coffee on a phone */
  body.phoneLayout .awayRow{ font-size:10px; letter-spacing:.5px; }
  body.phoneLayout .awayRow.er{ font-size:9px; letter-spacing:0; line-height:2; flex-direction:column; } /* two lines on a phone */
  body.phoneLayout .awayRow.er .erL{ display:flex; align-items:center; }

  /* Everything on this screen is measured in --bar (see .cabinet), so the
     title, the menu and the footer grow with the game on a big window and
     keep their old sizes everywhere else. */
  .startscreen{ position:absolute; inset:0; background:var(--panel); display:flex; flex-direction:column; align-items:center; gap:calc(var(--bar) * 1.059); text-align:center; padding:calc(var(--bar) * 1.176); z-index:20; overflow-y:auto; scrollbar-width:none; }
  .startscreen::-webkit-scrollbar{ display:none; }
  /* max-width so a fixed-width child cannot make this panel wider than the
     screen. It is a shrink-to-fit flex item, so without a cap here its own
     width came from the 348px button stack inside it -- which made the
     stack's max-width:100% resolve to 348px and clamp nothing at all. */
  .startscreen .startscreenPanel{ margin:auto 0; position:relative; z-index:10; max-width:100%; }
  /* The scene is drawn at one fixed scale (800px tiles, see fitStartBgViewBoxes
     in game.js) and everything about it -- the tile width, the scroll distance,
     the viewBox fitting -- assumes that. So rather than teach each of those
     about the bigger window, the whole background is laid out at the design
     size and scaled up as one picture: 1/k the size, then scaled by k, which
     lands it exactly on the cabinet with the ground still on the bottom edge.
     The ResizeObserver sees the smaller box and fits the sky to it as before. */
  .startBg{ position:absolute; inset:0; width:calc(100% / var(--k)); height:calc(100% / var(--k)); transform:scale(var(--k)); transform-origin:0 0; overflow:hidden; z-index:0; }
  .startBgTrack{ position:absolute; inset:0; display:flex; will-change:transform; }
  .startBgTile{ position:relative; width:800px; height:100%; flex:none; }
  .startBgTile svg{ width:100%; height:100%; display:block; }
  #startBgClouds{ animation:startBgScroll 200s linear infinite; z-index:0; }
  #startBgSkyline{ animation:startBgScroll 130s linear infinite; z-index:1; }
  #startBgFg{ animation:startBgScroll 60s linear infinite; z-index:2; }
  @keyframes startBgScroll{ from{ transform:translateX(0); } to{ transform:translateX(-800px); } }
  .startBgDark{ position:absolute; inset:0; background:rgba(10,10,8,0.62); z-index:3; pointer-events:none; }
  .soundStartBtn{ position:absolute; right:calc(var(--bar) * 0.8235); bottom:calc(var(--bar) * 0.8235); z-index:11; }
  .startscreen h1{ font-family:'Press Start 2P',monospace; font-size:calc(var(--bar) * 1.294); color:var(--gold); text-shadow:calc(var(--bar) * 0.1765) calc(var(--bar) * 0.1765) 0 var(--ink); margin:0; }
  .betaTag{ font-size:12px; color:var(--text-dim); text-shadow:none; vertical-align:middle; }
  /* THE TITLE STAYS ON ONE LINE. On a phone "CAFFEINATED BOB BETA" wrapped, and
     BETA sat alone on a second line under the title like a word left behind.
     The title shrinks to fit the screen instead, and BETA becomes a small
     boxed tag beside it -- the shape a version label has on every product,
     and one that reads as a label rather than as part of the name.
     Nothing here reaches a desktop: the size only shrinks where it has to. */
  .startscreen h1{ white-space:nowrap; font-size:min(calc(var(--bar) * 1.294), 4.0vw); }
  .betaTag{
    display:inline-block; font-size:0.45em; letter-spacing:1px; color:var(--text-dim);
    border:2px solid currentColor; border-radius:3px; padding:2px 4px; vertical-align:middle;
    position:relative; top:-0.15em;
  }
  /* A little air under the title -- about a hundredth of the screen, the
     owner's own measure, so it grows with the screen rather than being a
     number that is right on one phone and wrong on the next. */
  .startscreen p{ color:var(--text-dim); font-size:var(--bar); max-width:calc(var(--bar) * 27.06); margin:min(10px, 1vh) 0 calc(var(--bar) * 0.9412); }
  /* On a phone the break is written into the line, so the last line reads
     "coffee and a friend." instead of breaking wherever the width happens to
     land. The type gives up a little size so the first half holds one line. */
  .phoneBreak{ display:none; }
  @media (max-width:600px){
    .startscreen p{ font-size:min(17px, 3.6vw); }
    .phoneBreak{ display:inline; }
  }
  .startbtn{ background:var(--good); border:calc(var(--bar) * 0.1765) solid var(--ink); color:var(--ink); font-family:'Press Start 2P',monospace; font-size:calc(var(--bar) * 0.7647); padding:calc(var(--bar) * 0.8235) calc(var(--bar) * 1.294); cursor:pointer; box-shadow:calc(var(--bar) * 0.2353) calc(var(--bar) * 0.2353) 0 var(--ink); }
  /* max-width so the stack cannot grow wider than the screen it is in: the
     buttons' own max-width:100% resolves against THIS, and without it a
     348px stack simply hung off both sides of a 320px phone. */
  .startMenuBtns{ display:flex; flex-direction:column; align-items:center; gap:calc(var(--bar) * 0.7059); max-width:100%; }
  /* One width for every label in both menus, so the two read as the same menu
     with one section swapped rather than as two different screens. 348px is the
     measured natural width of the longest label anywhere in either -- APPLICANT
     ONBOARDING, at 310px -- plus roughly 19px of air either side, the same air
     the menu has always had. Rename a button to anything longer and this needs
     re-measuring rather than guessing.
     The font scales down on a narrow screen because 310px of label does not fit
     a 320px phone at any padding. clamp holds it at 13px on anything desktop-
     sized, so the priority platform never sees the smaller type. */
  .startMenuBtns .startbtn{ width:calc(var(--bar) * 19.24); max-width:100%; font-size:clamp(10px, 2.4vw, calc(var(--bar) * 0.7647)); }
  .startbtn:active{ transform:translate(calc(var(--bar) * 0.2353), calc(var(--bar) * 0.2353)); box-shadow:none; }
  .devnote{ font-size:13px; color:var(--warn); border:1px dashed var(--warn); padding:6px 10px; max-width:380px; }
  /* Padded clear of the sound button, which floats over this at right:14px and
     a higher z-index. Without it the last line ran underneath and the word
     "Policy" was clipped off the Terms link on a phone -- a legal link, half
     visible. Symmetric so the centring stays true. */
  /* In the flow, as the last item in the start screen's flex column -- NOT
     absolutely positioned, which is what it used to be. Absolute took it out of
     the layout entirely, so the menu had no idea it was there: adding a fourth
     button was enough to grow the stack straight down through the footer on a
     phone. In flow, the panel's `margin:auto 0` centres it in whatever is left
     above the footer, and the two can never occupy the same pixels.
     The horizontal padding keeps the text clear of the sound button, which
     floats over the bottom-right corner at a higher z-index; without it the
     last line ran underneath and the Terms link was clipped mid-word. */
  .startFooter{ position:relative; z-index:10; align-self:stretch; text-align:center; font-size:calc(var(--bar) * 0.7059); color:var(--text-dim); line-height:1.6; padding:0 calc(var(--bar) * 3.294); }
  /* Each phrase wraps as a whole or not at all. Two lines on any real desktop,
     but a phone is narrow enough to break one -- and left to itself it broke
     mid-phrase, stranding "LLC." alone on one line and "Development" on
     another. Wrapping at the joins instead keeps every line a complete thought
     however many of them there are. */
  .startFooter .fchunk{ white-space:nowrap; }
  a, a:visited{ color:#4da6ff; }
  a:hover{ color:#7cc0ff; }
  /* The two long-read panels get ~10% more width, so the Terms and the Handbook
     wrap over fewer lines. Only these two: the rest are short forms and menus
     that read fine as they are. Both still yield to the .overlay/.startscreen
     padding on narrow screens, so this only widens where there is room.
     The Terms panel needs BOTH rules, and the paragraph one is the rule that
     actually does the work. The panel shrinks to fit its contents, so its width
     is set by the widest thing in it -- the paragraphs -- and .panelBox's cap
     never binds at all. 557 plus 16px of body padding and a 3px border each
     side renders at 595, about 10% up from the 544 it sat at. The cap is raised
     with it only so it stays out of the way; it is headroom, not a width.
     The Handbook keeps its own 572, since only the Terms panel was asked for. */
  #tosGateContent .panelBox{ max-width:calc(var(--ui) * 37.0554); }
  #readmeOverlay .panelBox{ max-width:calc(var(--ui) * 33.6438); }
  #tosGateContent .panelBody p{ max-width:calc(var(--ui) * 32.7616); }
  /* The Terms gate is the one long-read panel that lives on the START SCREEN
     rather than inside an .overlay, and that difference is what broke its
     scrolling. .panelBox caps its own height at 92%, which needs a containing
     block with a REAL height to be 92% OF. An .overlay has one, because it is
     inset:0. This panel's container did not -- its height came from its own
     contents, so the cap resolved to 92% of however tall the Terms happened to
     be, which constrains nothing. The panel grew past the bottom of the screen,
     and .startscreen's own overflow:auto began scrolling the title screen
     underneath it: two scrollbars at once, the outer one dragging the street
     and the sky up past a panel that was supposed to be still.
     Giving the container the screen's own height puts that cap back on real
     ground. The panel now fits, .panelBody does the only scrolling there is,
     and the start screen never has anything to scroll. min-height:0 because a
     flex item refuses to shrink below its content without it, which would put
     the whole problem straight back. */
  #tosGateContent{ height:100%; min-height:0; display:flex; align-items:center; justify-content:center; }
  /* The Employee File stacks two things that used to be separate panels, so the
     file itself needs a rule between it and whatever sits above -- the sign-in
     form on the title screen, the way out in-game -- or the two read as one
     run-on block. :not(:first-child) keeps it off when the stats are the only
     section showing. */
  /* Toggled from renderEmployeePanel rather than keyed off :not(:first-child):
     the sections above this one are hidden by `display:none` in most states, and
     a CSS sibling selector counts DOM position, not visibility -- so the rule
     drew a divider under nothing on the Applicant's panel. */
  #empStatsBody.hasSectionAbove,
  #empAccountSection.hasSectionAbove{ margin-top:14px; padding-top:14px; border-top:2px solid var(--ink); }
  /* The file's header: who this record belongs to, before anything about how
     they are performing. Reads as two stamped fields on a personnel form. */
  /* RETURN TO TITLE MENU has no rules of its own any more: it is the third
     child of .empActions and takes that stack's layout and 8px gap like the two
     above it. It used to be position:sticky at the bottom of the scrolling file
     so it was always on screen -- and that is exactly what made it sit on top of
     the LEADERBOARD button as soon as the file scrolled. */
  /* Was itself the flex row of two stamped fields. It is now the block that
     holds whichever version of the identity is on show -- the two fields, or
     the engraved plate that replaces them -- so the row moved inside it. */
  .empIdentity{ margin-bottom:14px; }
  .empIdentity:empty{ display:none; }
  .empIdRow{ display:flex; gap:10px; }
  .empIdField{ flex:1; background:var(--panel-light); border:2px solid var(--ink); padding:8px 10px; }
  /* The nameplate itself: one brushed plate with two screws, carrying the name
     large and the badge number and start date beneath it. It REPLACES the two
     fields rather than joining them, which is the owner's design and the reason
     claiming one is worth doing -- the file gets tidier, not busier.
     The second line wraps rather than shrinking, so a long start date on a
     narrow phone takes two lines instead of overflowing the plate. */
  .empPlate{ position:relative; text-align:center; padding:11px 18px 9px;
    background:linear-gradient(180deg,#3f382a 0%,#2b261c 100%); border:2px solid var(--ink);
    box-shadow:inset 0 1px 0 rgba(255,255,255,0.10), 3px 3px 0 rgba(0,0,0,0.35); }
  .empPlateName{ font-family:'Press Start 2P',monospace; color:var(--gold); letter-spacing:.10em;
    font-size:max(11px, calc(var(--ui) * 0.94)); text-shadow:0 1px 0 rgba(0,0,0,0.6);
    overflow-wrap:anywhere; }
  .empPlateSub{ font-family:'Space Mono',monospace; color:var(--text-dim); margin-top:5px;
    letter-spacing:.06em; font-size:max(8px, calc(var(--ui) * 0.62)); }
  .empPlateScrew{ position:absolute; left:8px; top:50%; transform:translateY(-50%);
    width:6px; height:6px; border-radius:50%; background:#191512;
    box-shadow:inset 0 1px 1px rgba(255,255,255,0.25); }
  .empPlateScrewR{ left:auto; right:8px; }
  /* The empty state, under the two fields: an unengraved blank waiting to be
     filled in, which is what a dashed border reads as on a form. */
  /* Under the plate, and deliberately quieter than the invitation it replaces:
     claiming a nameplate is something to advertise, changing one is not. */
  .empPlateChange{ text-align:right; margin-top:5px; }
  .empPlateChange a{ font-family:'Space Mono',monospace; color:var(--text-dim);
    font-size:max(8px, calc(var(--ui) * 0.58)); letter-spacing:.06em; }
  .empPlateChange a:hover{ color:var(--gold); }
  .empPlateInvite{ margin-top:10px; border:2px dashed var(--text-dim); padding:10px 12px; text-align:center; }
  .empPlateInviteLine{ font-family:'Space Mono',monospace; color:var(--text-dim);
    font-size:max(9px, calc(var(--ui) * 0.7)); }
  .empPlateInvite a{ display:inline-block; margin-top:4px; font-family:'Space Mono',monospace;
    color:var(--gold); font-size:max(8px, calc(var(--ui) * 0.62)); letter-spacing:.06em; }
  .empIdLbl{ font-size:max(8px, calc(var(--ui) * 0.7059)); letter-spacing:.08em; text-transform:uppercase; color:var(--text-dim); }
  .empIdVal{ font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 1.0)); font-weight:700; color:var(--gold); margin-top:2px; }
  /* Form NPC-1. Shaped like the After Hours authorization it sits beside: a
     masthead, a form number, a wall of small print, and a stamp at the end. */
  .npcFormMeta{ display:flex; justify-content:space-between; gap:12px;
    font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.6));
    color:var(--text-dim); letter-spacing:.06em; margin-bottom:10px; }
  .npcFine{ font-size:max(8px, calc(var(--ui) * 0.62)); color:var(--text-dim);
    line-height:1.5; margin-top:10px; }
  /* The approval, stamped rather than announced. Tilted a little, the way a
     stamp lands, and in the game's green rather than its gold so it reads as a
     decision made about the form rather than another heading on it. */
  .npcStamp{ display:inline-block; font-family:'Press Start 2P',monospace;
    font-size:max(10px, calc(var(--ui) * 0.85)); color:var(--good);
    border:3px solid var(--good); padding:8px 14px; margin:4px 0 12px;
    transform:rotate(-6deg); opacity:.9; letter-spacing:.08em; }
  #tosGateContent .panelBox{ text-align:left; }
  /* Side by side, and both of them real buttons. The strength of a click-to-
     agree gate rests on the refusal being as available as the acceptance, so
     DECLINE is not grey micro-text under the green one -- it is the same size
     and the same shape, in a quieter colour. Wraps to two rows rather than
     shrinking on a narrow phone. */
  .tosGateBtns{ display:flex; gap:10px; justify-content:center; margin:16px 0 0; }
  /* Share the row evenly and shrink rather than wrap, so the two stay side by
     side all the way down to a small phone. */
  .tosGateBtns .startbtn{ flex:1 1 0; min-width:0; max-width:190px; padding:14px 8px;
    white-space:nowrap; font-size:clamp(10px, 2.6vw, 13px); }
  .tosDecline{ background:var(--panel-light); color:var(--text); }
  .scrollIndicatorTrack{ position:absolute; top:12px; bottom:12px; right:6px; width:6px; border-radius:3px; background:rgba(0,0,0,0.25); z-index:25; display:none; }
  .scrollIndicatorTrack.show{ display:block; }
  .scrollIndicatorThumb{ position:absolute; left:0; right:0; border-radius:3px; background:var(--text-dim); opacity:0.6; }

  .overlay{ position:absolute; inset:0; background:rgba(8,8,6,0.85); display:none; align-items:center; justify-content:center; z-index:30; padding:16px; }
  .overlay.show{ display:flex; }
  /* max-width in the same unit as the type inside it: a panel that grew its
     words but not its box would just wrap more. width:100% still caps it to
     whatever room the overlay has, which is what keeps phones as they were. */
  .panelBox{ font-size:var(--ui); position:relative; background:var(--panel); border:3px solid var(--ink); width:100%; max-width:calc(var(--ui) * 30.5853); max-height:92%; display:flex; flex-direction:column; box-shadow:6px 6px 0 rgba(0,0,0,0.5); }
  /* The one-device question. Narrower than .panelBox's 520 because it is a
     choice, not a document -- and it has to come AFTER that rule to win, since
     the two have the same specificity and it was silently doing nothing above
     it. Its two answers sit side by side, with the one that changes nothing on
     the right. */
  /* After Hours Authorization. Shaped like the TPS report on purpose -- a
     masthead, a reference number, a wall of policy, and boxes to tick. */
  .ahBox{ max-width:calc(var(--ui) * 32.9381); }
  .ahPaper{ background:#f4f1e8; color:#201d16; padding:14px 16px; font-size:max(8px, calc(var(--ui) * 0.8824)); line-height:1.5; }
  .ahPaper p{ margin:0 0 10px; }
  .ahPaper .sectionhead{ color:#6b5526; margin:14px 0 6px; }
  .ahMast{ display:flex; justify-content:space-between; gap:12px; border-bottom:2px solid #201d16; padding-bottom:6px; margin-bottom:10px; font-size:max(8px, calc(var(--ui) * 0.7059)); }
  .ahRow{ display:flex; gap:10px; align-items:baseline; margin-bottom:8px; }
  .ahRow label{ font-size:max(8px, calc(var(--ui) * 0.6471)); letter-spacing:.06em; color:#6b5526; min-width:120px; }
  .ahRow .formfield{ width:90px; margin:0; }
  .ahAck{ display:flex; gap:8px; align-items:flex-start; margin-bottom:8px; font-size:max(8px, calc(var(--ui) * 0.7647)); cursor:pointer; }
  .ahAck input{ margin-top:3px; flex:none; }
  .ahBtns{ display:flex; justify-content:center; margin-top:14px; }
  .ahProblems{ color:#8a2b2b; font-size:max(8px, calc(var(--ui) * 0.7647)); margin:8px 0; }
  .ahProblems ul{ margin:4px 0 0; padding-left:18px; }
  .ahNote{ background:#fdf6e8; border-left:3px solid #d9a53d; padding:6px 9px; font-size:max(8px, calc(var(--ui) * 0.7647)); }
  .ahDenied{ color:#8a2b2b; font-weight:700; }
  .ahGranted{ color:#3f6b3f; font-weight:700; font-size:max(8px, calc(var(--ui) * 1.0)); letter-spacing:.05em; }
  /* The Incident Report's own small bits, layered on the ahPaper/ahRow/ahAck
     rules above. Width and box-sizing are already .formfield's job -- the
     textarea gets both classes -- so the only override this form needs is a
     shorter minimum height than .formfield's textarea rule gives it, and that
     has to out-specificity `textarea.formfield` (an element plus a class) to
     win, which a single class of its own cannot do. */
  .irQ{ margin:10px 0 4px; font-weight:bold; }
  .irHint{ font-size:.85em; opacity:.7; margin:0 0 4px; }
  .irText.formfield{ min-height:64px; }
  .irOpt{ display:flex; gap:8px; align-items:center; margin:3px 0; }
  .irDisabled{ opacity:.55; font-style:italic; }
  .sessionBox{ max-width:calc(var(--ui) * 25.2917); }
  .sessionBox .panelBody p{ margin:0 0 12px; }
  .sessionBtns{ display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:16px; }
  /* Narrower than .sessionBox because it is read standing up on a phone, and
     a short line is the whole point of the panel. The buttons stack rather
     than sit side by side: RETURN TO MENU is a real choice, not a cancel, and
     two 20-character labels squeezed onto one phone-width row read as one
     button that has broken in half. */
  /* The one-off explainer cards. Same furniture as the device advisory above --
     a headed panel with one button -- because they do the same job: a short
     thing said once, read, and dismissed. Slightly wider, because these carry
     two paragraphs and the advisory carries three short ones. */
  .discoveryBox{ max-width:calc(var(--ui) * 26); }
  .discoveryBox .panelBody p{ margin:0 0 12px; }
  .discoveryBox .panelBody p:last-of-type{ margin-bottom:16px; }

  /* Daily Alignment, the Desk Calendar's page (play/daily-alignment.js). Today's
     page only; the page inside is drawn like the paper block on the desk, red
     date band and all. */
  .dacRow{ display:flex; align-items:center; justify-content:center; width:100%; }
  .dacBox{ max-width:min(calc(var(--ui) * 26), 100%); }
  /* JUST THE SHEET (the owner, 2 October 2026: "I also don't need the 'daily
     alignment' card behind it. All we really need is the calendar sheet itself").
     The panel keeps its place in the markup, so the click-outside test and the
     overlay's heading id still work, but draws nothing. The heading stays for
     screen readers (aria-labelledby) with the usual clip-to-1px hiding. */
  .dacBox{ background:none; border:0; box-shadow:none; }
  .dacBox .panelBody{ padding:0; }
  .dacBox .supportHead{ position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0; overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; }
  /* The X: in the band's own red, a shade darker, so it is there without being
     the loudest thing on the page ("present but not highly noticeable"). */
  /* FLUSH RIGHT (the owner, 3 October 2026: "move the x and box it's in over slightly so that
     the right edge of that box is aligned with the right edge of the bar"): right:0 is
     the band's padding edge, and the band has no side border. Still centred top to bottom. */
  .dacX{ position:absolute; top:50%; right:0; transform:translateY(-50%); width:32px; height:32px; padding:0; display:flex; align-items:center; justify-content:center;
    font:inherit; font-size:14px; line-height:1; color:rgba(244,241,232,.6); background:#a93232; border:1px solid #8e2828; cursor:pointer; }
  .dacX:hover, .dacX:focus-visible{ color:#f4f1e8; background:#962c2c; }
  /* The cross is drawn, not typed (the owner, 3 October 2026: "make sure the x is
     centered horizontally and vertically in the box from the center of the x"). The
     U+2715 glyph's own metrics sit it off the box's middle whatever the flex says;
     two bars crossed at the exact centre of a 14px square do not. They take the
     button's colour, so the hover change still works. */
  .dacXMark{ position:relative; display:block; width:14px; height:14px; flex:0 0 auto; }
  .dacXMark::before, .dacXMark::after{ content:""; position:absolute; left:0; top:50%; width:100%; height:2px; margin-top:-1px; background:currentColor; }
  .dacXMark::before{ transform:rotate(45deg); }
  .dacXMark::after{ transform:rotate(-45deg); }
  /* The plaque view (play/game.js openPlaque): the plaque itself, large, on the
     dim. Nothing else -- no panel, no button. */
  .plaqueOverlay{ cursor:pointer; }
  .plaqueBig{ width:min(78%, 620px); cursor:default; }
  .plaqueBig svg{ display:block; width:100%; height:auto; filter:drop-shadow(6px 6px 0 rgba(0,0,0,.55)); }
  .dacPage{ background:#f4f1e8; border:3px solid var(--ink); box-shadow:4px 4px 0 var(--ink); color:var(--ink); margin:2px 4px 4px; }
  .dacBand{ position:relative; background:#c23b3b; border-bottom:3px solid var(--ink); color:#f4f1e8; text-align:center;
    font-family:'Press Start 2P',monospace; font-size:max(7px, calc(var(--ui) * 0.6)); letter-spacing:1px; padding:.9em 1em; }
  .dacInner{ text-align:center; padding:1em 1.1em .9em; }
  .dacNum{ font-family:'Press Start 2P',monospace; font-size:2em; line-height:1; margin:.1em 0 .5em; }
  .dacTag{ display:inline-block; font-family:'Press Start 2P',monospace; font-size:max(7px, calc(var(--ui) * 0.5)); letter-spacing:1px; border:2px solid var(--ink); padding:.55em .7em; margin-bottom:.9em; }
  .dacTag.dacL{ background:var(--ink); color:#f4f1e8; }
  .dacTag.dacB{ background:var(--good); color:var(--ink); }
  .dacTag.dacD{ background:var(--gold); color:var(--ink); }
  .dacText{ margin:0; line-height:1.5; min-height:4.5em; }
  .dacBy{ margin:.4em 0 0; font-style:italic; opacity:.7; }
  .dacFoot{ margin-top:.9em; font-size:.75em; opacity:.6; }
  .dacEdge{ height:9px; border-top:2px solid var(--ink); background:repeating-linear-gradient(#e3ddc9 0 2px, #b7b19c 2px 3px); }
  @media (max-width:520px){
    .dacBand{ letter-spacing:0; padding:.7em .5em; line-height:1.6; }
    .dacSep{ display:none; }
    .dacWd, .dacMd{ display:block; }
    .dacInner{ padding:1em .8em .8em; }
  }
  /* The line that only an Applicant sees: what this particular work does not
     pay them, and the offer to change that. Set apart rather than buried in the
     paragraph above it, so the rule reads as a rule rather than as a caveat. */
  .discoveryApplicant{ color:var(--text-dim); font-style:italic; }

  /* ---- calculator (a desktop program) ----
     Narrower than the other panels on purpose: a calculator that stretches to
     a 520px dialog stops reading as a calculator. */
  .calcBox{ max-width:calc(var(--ui) * 18.9394); }
  .calcBody{ padding:14px; }
  /* Tabular figures and a fixed height so the answer does not jitter sideways
     as it is typed, and the window does not resize when the digits run long.
     Right-aligned, like every calculator ever made. */
  .calcDisplay{
    display:block; width:100%; box-sizing:border-box;
    background:#20261f; border:2px solid var(--ink); color:#8fe08f;
    font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 1.5294)); font-weight:700;
    font-variant-numeric:tabular-nums; letter-spacing:1px;
    text-align:right; padding:12px 12px; margin-bottom:12px;
    min-height:52px; line-height:28px;
    overflow:hidden; text-overflow:clip; white-space:nowrap; direction:ltr;
  }
  .calcPad{ display:grid; grid-template-columns:repeat(4, 1fr); gap:8px; }
  .calcKey{
    font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 1.0588)); font-weight:700;
    background:var(--panel-light); border:2px solid var(--ink); color:var(--text);
    padding:14px 0; cursor:pointer; user-select:none;
  }
  .calcKey:hover{ background:#4a4c3c; }
  .calcKey:active{ transform:translate(1px,1px); }
  .calcKey:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
  /* The three across the top are the ones that throw work away or reinterpret
     it, so they read as lighter and cooler than the digits. */
  .calcFn{ background:#5a5c4c; }
  .calcFn:hover{ background:#6a6c58; }
  /* Operators carry the same gold the rest of the game uses for "this is the
     action", which also makes the column scannable at a glance. */
  .calcOp{ background:var(--gold); border-color:var(--ink); color:#241f10; }
  .calcOp:hover{ background:#f0cf63; }
  /* Zero spans two columns, as it does on the calculator this is copying. */
  .calcZero{ grid-column:span 2; }
  .panelClose{ position:absolute; top:-14px; right:-14px; z-index:5; }
  /* The terminal's full-screen toggle, tucked just inside the close button.
     Backslash is the one panel whose content is genuinely limited by the
     panel: a fixed-height scrollback that SYNERGY fills faster than anything
     else in the game, on a box sized for a settings dialog. */
  .panelMaximize{ position:absolute; top:-14px; right:24px; z-index:5; }
  /* The Marketplace keeps the standard panel width. It was widened by 10% a
     side for the office cards and that was the wrong lever: it changed the
     whole shop's column count to make room for one new category. The offices
     scroll sideways in their own row instead (see .storeGrid.officeRow), which
     is what a growing range actually needs. The tab row only has to be wide
     enough for the six categories, and at this width it is. */
  /* flex-shrink:0 is load-bearing, not tidiness.

     A panel is a flex column capped at 92% of the overlay, and when its
     contents do not fit, the browser shrinks whichever items it is allowed to.
     A heading or a balance bar protects itself: a flex item's automatic minimum
     size is its content, so there is nothing to take. A tab row with
     overflow-x:auto on it does NOT -- any overflow other than visible drops that
     automatic minimum to zero -- so the row became the one thing in the panel
     that could be squashed, and the fuller the shelf underneath it, the harder
     it was squashed.

     Measured on an iPhone 12 with the Marketplace open: 24px of tab row on
     OFFICES, which fits; 19px on DESK and MUGS; 15px on OUTFITS, the category
     with the most items. The row was being cropped in proportion to how much
     was on sale under it, which is exactly how the owner described finding it.

     The shelf is the part that is supposed to absorb it -- it scrolls, and it
     is the only thing here that should. */
  .tabs{ display:flex; border-bottom:3px solid var(--ink); flex-shrink:0; }
  .tabsScrollWrap{ flex-shrink:0; }
  /* Same reasoning, one level up: the two bars above the tabs are never the
     thing to give way either. Stated rather than left to the automatic minimum,
     which is a rule about content and would stop protecting them the moment
     either one gained an overflow of its own. */
  .supportHead, .cvBalanceBar{ flex-shrink:0; }
  /* Six categories filling one row, and never wrapping: a second line of tabs
     would push the shelf down, and the onboarding card that sits under the
     first row of items is measured off the shelf.
     flex:1 0 auto rather than the plain flex:1 above -- grow to share out the
     spare width so the row is continuous edge to edge, but never shrink below
     the label, so OUTFITS and OFFICES cannot be clipped. On a window too narrow
     for six the row scrolls sideways instead. The scrollbar is hidden: a
     visible track would add height to the row, which is the thing being
     avoided. */
  #customizeTabs{ flex-wrap:nowrap; overflow-x:auto; scrollbar-width:none; }
  #customizeTabs::-webkit-scrollbar{ display:none; }
  #customizeTabs .tabbtn{ flex:1 0 auto; white-space:nowrap; }
  .tabbtn{ flex:1; background:var(--panel-light); border:none; color:var(--text-dim); font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.5882)); padding:12px 4px; cursor:pointer; }
  .tabbtn.active{ background:var(--panel); color:var(--gold); }

  /* ---- the leaderboard ----
     Ten categories in ONE row, scrolling sideways. They were wrapped onto three
     lines first and the owner did not like it: a block of stacked tabs reads as
     a menu of its own rather than as the top of the thing underneath it, and
     the eye has to hunt for which one is on.

     So: no wrapping, no stretching, each tab as wide as its label, and the row
     scrolls. leaderboardSelect() brings the active one into view, since a tab
     marked active off the end of the row is worse than no marking at all.
     A visible slim track is deliberate -- it is the only thing telling somebody
     with a mouse that there is more to the right. */
  .tabs.tabsScroll{ flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:var(--text-dim) transparent; }
  .tabs.tabsScroll .tabbtn{ flex:0 0 auto; white-space:nowrap; padding-left:14px; padding-right:14px; }
  .tabs.tabsScroll::-webkit-scrollbar{ height:6px; }
  .tabs.tabsScroll::-webkit-scrollbar-thumb{ background:var(--text-dim); }
  .tabs.tabsScroll::-webkit-scrollbar-track{ background:transparent; }
  /* The fade at whichever end still has tabs behind it, toggled by
     leaderboardSyncTabOverflow() from a real measurement -- so a window wide
     enough for all ten shows neither.
     It hangs off a WRAPPER rather than off the scrolling row itself: a
     pseudo-element inside a horizontal scroller is a flex item like any other,
     so it scrolls away with the tabs instead of staying at the edge. The first
     attempt did exactly that and drew nothing at all.
     pointer-events:none so a fade never eats a click meant for the tab under it. */
  .tabsScrollWrap{ position:relative; }
  .tabsScrollWrap::before, .tabsScrollWrap::after{ content:''; position:absolute; top:0; bottom:0; width:30px; pointer-events:none; opacity:0; transition:opacity .15s; z-index:2; }
  .tabsScrollWrap::before{ left:0; background:linear-gradient(to right, var(--panel-light) 30%, rgba(35,36,29,0)); }
  .tabsScrollWrap::after{ right:0; background:linear-gradient(to left, var(--panel-light) 30%, rgba(35,36,29,0)); }
  .tabsScrollWrap.moreLeft::before{ opacity:1; }
  .tabsScrollWrap.moreRight::after{ opacity:1; }
  /* An ordinary panel, and deliberately so. It filled the whole office for an
     afternoon -- the owner's idea, then his second thought: "overkill". So it
     is the same box every other panel in this game is, sized in --ui like the
     rest of them, which is what makes it work on a phone without a phone rule
     of its own. A little wider than the default, because this one holds a
     table with as many as four columns and the others hold prose.

     .panelBox already supplies width:100%, max-height:92% and the column
     layout; only the width ceiling and the scrolling body are ours. */
  /* Wide enough for the Overall board's eleven columns, which measure about
     488px and do not shrink -- they are digits. At 34 the body came out 436px
     and the widest board on the panel scrolled sideways on a DESKTOP too, not
     just a phone; the owner reported the phone half of that on 8 September.
     A phone is unaffected by this number: .panelBox is width:100% and the
     overlay's padding caps it long before this does. */
  #leaderboardPanel{ max-width:calc(var(--ui) * 39); }
  /* overflow-x too: the TPS board carries four columns, and on a narrow window
     that is the one table here wide enough to need somewhere to go. */
  #leaderboardPanel .panelBody{ flex:1; min-height:0; overflow-y:auto; overflow-x:auto; }
  /* ==== LEADERBOARD PERIOD ==== */
  /* The ALL TIME / THIS MONTH switch, drawn in the same .tabbtn language as
     the category tabs directly below it. A hair of separation so its own
     border-bottom does not fuse straight onto that row's. */
  .lbPeriod{ margin-bottom:2px; }
  /* THIS MONTH's own reason to have a clock on it, coloured like every other
     number on this panel the player is meant to actually notice. */
  .lbMonthNote{ color:var(--gold); font-weight:700; }
  /* ==== end ==== */
  .lbHead{ font-family:'Press Start 2P',monospace; font-size:max(9px, calc(var(--ui) * 0.7059)); color:var(--gold); margin-bottom:4px; }
  .lbNote{ font-size:max(9px, calc(var(--ui) * 0.6471)); color:var(--text-dim); margin-bottom:12px; }
  .lbTable{ width:100%; border-collapse:collapse; font-family:'Space Mono',monospace; font-size:max(10px, calc(var(--ui) * 0.7647)); }
  .lbTable th{ text-align:left; color:var(--text-dim); font-weight:400; font-size:max(9px, calc(var(--ui) * 0.6471)); padding:0 8px 6px 0; border-bottom:2px solid var(--ink); }
  .lbTable td{ padding:5px 8px 5px 0; border-bottom:1px solid rgba(32,29,22,0.35); color:var(--text); }
  .lbTable .lbRank{ width:3.5em; color:var(--text-dim); }
  /* A row with a nameplate on it: the chosen name first, the badge number under
     it in small print. The number is never dropped -- identity on this board is
     ultimately the badge, whatever a name is made to look like.
     break-word rather than nowrap: sixteen characters is longer than a badge
     number, and on a phone the column has to give way by wrapping rather than
     by pushing the score off the side of the panel. */
  .lbTable .lbWho{ overflow-wrap:anywhere; }
  .lbTable .lbNameRow{ display:flex; align-items:baseline; gap:0; }
  .lbTable .lbName{ font-weight:700; min-width:0; overflow-wrap:anywhere; }
  /* Quiet until wanted. A report flag that competes with the name beside it
     invites presses that were never about the name. */
  .lbFlag{ background:none; border:none; padding:0 0 0 6px; cursor:pointer;
    color:var(--text-dim); opacity:.4; font-size:max(9px, calc(var(--ui) * 0.62)); line-height:1; }
  .lbFlag:hover{ opacity:1; color:var(--danger); }
  .lbFlag:disabled{ opacity:.7; color:var(--good); cursor:default; }
  .lbTable .lbBadgeSub{ font-size:max(8px, calc(var(--ui) * 0.55)); color:var(--text-dim); }
  .lbTable .lbVal{ text-align:right; padding-right:0; white-space:nowrap; }
  /* The Overall board is twelve columns wide and is meant to be pushed
     sideways -- the owner asked for the slider. Its cells get a little breathing
     room back and the average placing, which is the column the board is sorted
     by, is set off from the nine it is made of. */
  .lbTable .lbVal + .lbVal{ padding-left:14px; }
  /* Left aligned rather than right, at the owner's request -- twelve narrow
     numbers stacked ragged-right read as a wall the eye has to hunt across;
     lined up on the left they read as a row instead. The gap between columns
     moves from the left of each number (needed for a right-aligned one) to
     the right of it, so it doesn't collapse against the shorter values. */
  /* The Overall board carries ten value columns beside the Employee one, so the
     browser had been giving that column whatever was left over -- about six
     characters, which turned "The Bobfather" into four stacked lines. Sixteen
     characters is the longest a nameplate can be, and 9.5em of monospace holds
     it on one line at every size the panel is drawn at; the numbers beside it
     are short and give up the width without complaint. */
  /* On the Overall board the name does not wrap. Ten value columns sit beside
     the Employee one, so the browser had been giving it whatever was left --
     about six characters, which turned "The Bobfather" into four stacked lines.
     min-width alone could not fix that: .lbWho carries overflow-wrap:anywhere so
     a long name gives way on a phone, and the browser will happily break a word
     rather than honour a minimum. So this board says NOT to break it, and the
     panel it sits in already scrolls sideways if the whole table outgrows it.
     The narrow boards keep their wrapping -- there the name has room, and a
     phone would rather wrap than scroll. */
  .lbTable.lbOverall .lbWho{ overflow-wrap:normal; }
  .lbTable.lbOverall .lbName{ white-space:nowrap; overflow-wrap:normal; }
  /* 9px, not 14. Nine columns of digits at 14px of gutter measured 488px wide
     and came 19px over the panel on a 1366x768 laptop -- the most common
     screen there is -- so the whole board scrolled sideways there to hide
     nineteen pixels. Tighter gutters are a cheaper price than either a
     sideways drag or dropping the columns on a screen with room for them. */
  .lbTable.lbOverall .lbVal{ text-align:left; padding-right:9px; }
  .lbTable.lbOverall .lbVal + .lbVal{ padding-left:0; }
  .lbTable .lbAvg{ color:var(--gold); border-left:2px solid var(--ink); padding-left:14px; }
  .lbTable tr.lbMine td.lbAvg{ font-weight:700; }
  /* The reader's own row. Gold and bold, which is the same pair the game uses
     for anything that belongs to the player rather than to the office. */
  .lbTable tr.lbMine td{ color:var(--gold); font-weight:700; }
  /* Somebody ranked past the top twenty-five still gets to see their own row,
     set apart from the list above it rather than pretending to be part of it. */
  .lbTable tr.lbGap td{ border-top:2px dashed var(--text-dim); padding-top:10px; }
  /* ---- the Overall board on a phone ----

     Overall carries eleven columns: a place, a badge, the eight boards it
     averages, and the average itself. That is about 400px of table in a 312px
     panel on an iPhone, so the right-hand columns were simply cut off and the
     only way to reach them was to drag the table sideways -- which the owner
     reported on 8 September as the leaderboard being "cut off the screen",
     wanting it bound so it only scrolls up and down.

     The eight are the workings, not the answer. The board's own note says so:
     it ranks you on your AVERAGE placing across them, and each of those eight
     is a tap away on its own tab, at full width, where it is legible. So on a
     narrow glass the workings go and the answer stays.

     Only the Overall board, and only its detail columns -- .lbAvg carries
     .lbVal too, so it has to be excluded by name or the answer goes with the
     workings. Every other board has one or two columns and already fits.

     The tab strip above still scrolls sideways. That is deliberate and the
     owner's own call: ten categories in one row, with a fade at whichever end
     has more behind it.

     Keyed on the PANEL's own width, not the viewport's and not glassNarrow.
     glassNarrow is the wrong signal here for a reason worth writing down: it
     is set by syncGlass() in synergy-os.js, which measures an element that
     only exists once SYNERGY's desktop has been opened -- and the leaderboard
     opens from the title screen too, where that has never happened. A rule
     keyed on it is simply absent on the exact screen this bug was reported
     from. The panel's own width is the thing that actually decides whether
     the table fits, so that is what asks the question.

     460px because the full Overall table measures 488px wide and does not
     depend on the window: it is content-driven, eleven columns of digits. */
  #leaderboardPanel .panelBody{ container-type:inline-size; container-name:lbBody; }
  @container lbBody (max-width: 460px){
    .lbTable.lbOverall .lbVal:not(.lbAvg){ display:none; }
    .lbTable.lbOverall .lbAvg{ border-left:0; padding-left:0; text-align:right; }
  }
  /* With the workings gone there is nothing left to reach sideways for, so the
     body stops offering it -- a table that fits must not still pan. Above the
     threshold the whole table is there and overflow-x:auto still applies. */
  @container lbBody (max-width: 460px){
    #leaderboardPanel .panelBody{ overflow-x:hidden; }
  }
  .lbYou{ margin-top:14px; padding-top:10px; border-top:3px solid var(--ink); font-family:'Press Start 2P',monospace; font-size:max(9px, calc(var(--ui) * 0.7059)); color:var(--gold); }
  .lbYou.lbYouMuted{ color:var(--text-dim); }
  .lbMsg{ color:var(--text-dim); padding:18px 0; text-align:center; }
  /* A sortable column heading. A button rather than a clickable th: the header
     is the only sorting control in the game and it has to be reachable from a
     keyboard. Styled to read as a heading until it is hovered. */
  .lbSort{ background:none; border:0; padding:0; margin:0; cursor:pointer; font:inherit; color:var(--text-dim); text-transform:inherit; letter-spacing:inherit; }
  .lbSort:hover{ color:var(--text); text-decoration:underline; }
  .lbSort.active{ color:var(--gold); }
  .lbSort.active::after{ content:' \25BC'; font-size:0.8em; }
  .lbRetry{ display:block; margin:10px auto 0; }

  /* ---- Corporate's review of the day ------------------------------------
     The rating is the loudest thing on the panel, because the whole joke is a
     grade nobody asked for. Coloured by band: red at the bottom, the gold the
     game already uses for anything that belongs to the player at the top. */
  #dayEndPanel{ max-width:calc(var(--ui) * 28); }
  .dayEndRating{ font-family:'Press Start 2P',monospace; font-size:max(10px, calc(var(--ui) * 0.8235));
    text-align:center; margin:4px 0 10px; line-height:1.5; }
  .dayEndRating.rating0{ color:var(--danger); }
  .dayEndRating.rating1{ color:#d98a3a; }
  .dayEndRating.rating2{ color:var(--text-dim); }
  .dayEndRating.rating3{ color:var(--good); }
  .dayEndRating.rating4{ color:var(--gold); }
  /* Corporate's own words. Italic, because it is a quotation from nobody. */
  .dayEndBlurb{ font-style:italic; color:var(--text-dim); text-align:center;
    margin-bottom:14px; line-height:1.6; }
  .dayEndIncident{ color:var(--danger); text-align:center; margin-bottom:14px; line-height:1.6; }
  /* margin-bottom overrides the shared .supportbtns rule. With the note
     below now taking no room while it is empty, that inherited 16px sat on
     top of the panel's own padding and left the button floating in more air
     than anything else in the panel has. */
  /* Both classes, because the shared .supportbtns rule is defined further
     down this file and would otherwise win on source order. With the note
     below taking no room while empty, that inherited bottom margin sat on
     top of the panel's own padding and left the button in more air than
     anything else in the panel. */
  .dayEndBtns{ margin-top:16px; }
  .supportbtns.dayEndBtns{ margin-bottom:0; }
  /* Says whether the share landed. It used to hold its line even when empty,
     to stop the button shifting when it fills -- but empty is the state a
     player actually sits looking at, and reserving a line there left the panel
     with noticeably more air under the button than it has anywhere else. So
     the space is claimed only when there is something to say. */
  .dayEndShareNote{ min-height:1.4em; margin-top:8px; text-align:center;
    color:var(--text-dim); font-size:max(9px, calc(var(--ui) * 0.7059)); }
  .dayEndShareNote:empty{ min-height:0; margin-top:0; }
  /* The Employee File's heading carries an invite button beside its title, so
     it is a row rather than a centred line. Everything else using .supportHead
     is unaffected -- only the file's own heading takes .headRow. */
  .supportHead.headRow{ display:flex; align-items:center; justify-content:center; gap:0.7em; }
  .empShareBtn{ background:var(--panel-light); border:2px solid var(--ink); color:var(--gold);
    box-shadow:2px 2px 0 var(--ink); cursor:pointer; padding:0.28em 0.4em; line-height:0;
    display:flex; align-items:center; flex:none; }
  .empShareBtn:hover{ color:var(--text); }
  /* The same button beside a section heading rather than the panel title, so
     it takes the smaller of the two sizes. .sectionhead is a block by default;
     .headRow turns it into a row only on the heading that carries a button. */
  .sectionhead.headRow{ display:flex; align-items:center; gap:0.7em; }
  .empShareBtn.todayShareBtn{ padding:0.22em 0.32em; }
  .empShareBtn.todayShareBtn svg{ width:0.82em; height:0.82em; }
  .empShareBtn:active{ transform:translate(2px,2px); box-shadow:none; }
  .empShareBtn:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
  .empShareBtn svg{ display:block; width:1.05em; height:1.05em; }
  .supportHead{ background:var(--panel-light); border-bottom:3px solid var(--ink); font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.7059)); color:var(--gold); padding:14px 16px; text-align:center; }
  /* Two of these can sit one under the other in the Applicant File -- SET UP
     EMPLOYEE FILE and RETURN TO TITLE MENU -- and the gap between the two rows
     read as a break in the panel rather than two buttons on the same errand. */
  #empApplySection .supportbtns{ margin-bottom:0; }
  .supportbtns{ display:flex; gap:10px; margin:16px 0; }
  /* The Employee File's way-out buttons, stacked rather than spaced. They were
     three separate .supportbtns rows, each carrying 16px above and below, which
     put 32px of nothing between every pair -- the owner: "remove the big
     unnecessary gap between the two buttons, they can just be stacked
     vertically." One margin around the stack, one gap inside it. */
  /* Directly under the career record, with the same 8px that separates the two
     buttons from each other, so the whole thing reads as one stack rather than
     as a block that has been pushed away from the file it belongs to. */
  .empActions{ display:flex; flex-direction:column; gap:8px; margin:0; }
  /* Each button in its own wrapper div, and a wrapper full of inline-block
     leaves a line-box's worth of descender space under it -- which put 18px
     between buttons that were asked to sit 8px apart. Flex wrappers have no
     line box to leave. */
  .empActions > div{ display:flex; }
  .empActions .supportbtn{ width:100%; }
  .supportbtn{ flex:1; background:var(--panel-light); border:2px solid var(--ink); color:var(--gold); font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.8235)); padding:14px 6px; cursor:pointer; box-shadow:3px 3px 0 var(--ink); }
  .supportbtn:active{ transform:translate(3px,3px); box-shadow:none; }
  .supportStepper{ flex:1; display:flex; align-items:stretch; }
  .supportStepper .stepperGive{ flex:1; min-width:0; display:flex; align-items:center; justify-content:center; border-right:none; }
  .supportStepper .stepperGive span{ overflow:hidden; text-overflow:clip; white-space:nowrap; }
  .stepperArrows{ display:flex; flex-direction:column; flex:0 0 22px; border:2px solid var(--ink); border-left:none; box-shadow:3px 3px 0 var(--ink); }
  .stepperArrowBtn{ flex:1; background:var(--panel-light); border:none; color:var(--gold); font-size:max(8px, calc(var(--ui) * 0.4706)); cursor:pointer; line-height:1; padding:0; }
  .stepperArrowBtn:first-child{ border-bottom:2px solid var(--ink); }
  .stepperArrowBtn:hover{ filter:brightness(1.2); }
  .stepperArrowBtn:active{ background:var(--ink); }
  .supportStep{ display:none; }
  .supportStep.show{ display:block; }
  .supportOffNote{ display:none; }
  .supportStep.supportOff .supportbtns{ display:none; }
  .supportStep.supportOff .supportOffNote{ display:block; font-style:italic; }
  .supportError{ color:#e58989; font-size:max(8px, calc(var(--ui) * 0.7647)); margin:8px 0 0; }
  .supportFieldLabel{ display:block; font-size:max(8px, calc(var(--ui) * 0.8235)); color:var(--text-dim); margin-bottom:4px; }
  /* Employee File panel + the (usually invisible) Turnstile slot under the start menu */
  /* The second-tab message. Sits with the start menu buttons, in the warning
     colour rather than the error colour: nothing has gone wrong, the game is
     just already open somewhere else. */
  /* Sits with the button it belongs to rather than down in the footer, which
     would read as legal boilerplate. Quiet on purpose: a caption, not a
     control, so the four buttons stay the only things that look pressable.
     The email truncates rather than wrapping -- a long address must not push
     "Sign out" onto a second line where nobody looks for it. */
  .signedInAs{ margin-top:12px; font-size:13px; color:var(--text-dim); text-align:center; max-width:100%; }
  .signedInAs .who{ display:inline-block; max-width:min(240px, 46vw); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; color:var(--text); }
  .signedInAs .sep{ opacity:.55; }
  .deskTakenNotice{ margin-top:14px; max-width:420px; font-size:14px; line-height:1.5; color:var(--warn); text-align:center; }
  /* Pinned to the bottom of the viewport rather than sitting in the title
     screen's flow, so it is reachable during a shift as well -- the Support
     panel needs a token too, and the title screen is display:none by then.
     'interaction-only' means Cloudflare renders nothing here until it actually
     wants the player to confirm something, so in the ordinary case this box
     has no content at all.
     Deliberately NOT the pointer-events:none-on-the-box /
     pointer-events:auto-on-the-iframe trick that used to be here. It never
     worked in ANY browser, not just Safari as first suspected: Turnstile's
     interactive challenge is not a plain <iframe> child of this box, so
     ".turnstileSlot iframe" matched nothing, and pointer-events:none on the
     box was blocking every click regardless. Confirmed directly with
     Cloudflare's own always-interactive test sitekey (3x00000000000000000000FF):
     elementFromPoint on the rendered challenge hit the page background with
     the old rule, and the widget itself with this one. width:fit-content
     makes the box exactly as big as whatever Cloudflare puts in it (nothing,
     most of the time), so there is never an
     invisible oversized area to worry about blocking clicks in the first
     place, and normal pointer-events works everywhere. */
  /* bottom is overwritten inline by positionTurnstileSlot() on browsers that
     expose window.visualViewport, to lift the widget clear of an open
     on-screen keyboard. This 8px stays the resting value and the fallback. */
  .turnstileSlot{ position:fixed; left:50%; transform:translateX(-50%); bottom:8px; z-index:9999; width:fit-content; display:flex; justify-content:center; opacity:1; visibility:visible; transition:opacity .4s ease; }
  .turnstileSlot iframe{ max-width:100%; }
  /* A finished challenge has no reason to keep sitting on screen; JS adds this
     class a couple seconds after it settles and removes it again before the
     next one (the widget is reused via turnstile.reset()).
     visibility:hidden alongside the fade, not opacity alone: this element is
     position:fixed WITH a transform, which puts it on its own compositing
     layer, and Safari has a long history of not repainting a cross-origin
     iframe inside one when only its ancestor's opacity changes -- the reported
     symptom being a solved challenge that never goes away. visibility is
     honoured by the compositor rather than the painter, so it cannot be missed
     that way, and it takes the widget out of hit-testing outright.
     The 0s-delayed-.4s transition holds visibility:visible until the opacity
     fade has finished, so the box still fades rather than vanishing; removing
     the class falls back to the base rule above, where visibility snaps back
     immediately and the next challenge is never invisible. */
  .turnstileSlot--done{ opacity:0; visibility:hidden; pointer-events:none; transition:opacity .4s ease, visibility 0s linear .4s; }
  /* Still in use: the Terms line under the sign-in form, and the note about the
     optional email on the feedback form. */
  .empNote{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim); margin:0 0 6px; }
  /* The caption under the buttons, in the size and colour that note has --
     smaller than body copy and in the dim ink -- because it is a footnote on
     the file rather than something anybody needs to read twice.
     The margin is the other half: a <p> carries a 1em top margin by default,
     and that was the space between it and SIGN OUT. 8px instead, the same gap
     the two buttons keep from each other, so the caption reads as the end of
     that stack rather than a paragraph adrift below it. */
  /* No margin of its own: the caption sits directly under the account block's
     top border, and the 14px between them is that block's padding -- the same
     14px that separates SIGN OUT from the line on the other side. The owner
     asked for exactly that symmetry, and a margin here would break it. */
  #empSignedInText{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim);
    margin:0; }
  .empStatus{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim); margin:10px 0 0; min-height:1.2em; }
  .emailOptInRow{ display:flex; align-items:center; gap:8px; font-size:max(8px, calc(var(--ui) * 0.8824)); cursor:pointer; margin-bottom:4px; }
  .emailOptInRow input{ width:16px; height:16px; cursor:pointer; flex-shrink:0; }
  /* flex:1 and min-height:0 are what make overflow-y actually work here.

     .panelBox is a flex column with a max-height. A flex item's default
     min-height is `auto`, which means "never shrink below your content" -- so
     the body kept its full natural height, the scrollbar had almost nothing to
     travel, and a long email stuck partway down and would not reach the reply
     box. The owner's description: "it sort of hangs/gets stuck before it
     scrolls all the way down to the reply button."

     min-height:0 lets it shrink; flex:1 lets it take the room that is actually
     there. Nothing changes for a short panel -- the box sizes to its content
     and there is no spare space to grow into -- so this only bites at the
     moment the panel is too tall, which is the moment it was broken.

     The leaderboard, the terminal and the Assistant each carry their own copy
     of these two lines further down this file, added one at a time as somebody
     hit this on that panel. They are now redundant rather than wrong, and are
     left alone: this is the base every panel should have had. */
  .panelBody{ flex:1 1 auto; min-height:0; padding:14px 16px; overflow-y:auto; font-size:max(8px, calc(var(--ui) * 1.0)); }
  .statgrid{ display:grid; grid-template-columns:1fr auto; gap:6px 14px; margin-bottom:14px; }
  .statgrid .lbl{ color:var(--text-dim); }
  .statgrid .val{ text-align:right; }
  .sectionhead{ font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.7059)); color:var(--gold); margin:14px 0 8px 0; letter-spacing:1px; }

  /* stats overlay: three-tier hierarchy, deliberately its own visual style */
  .statsHeadline{ display:flex; gap:10px; margin-bottom:6px; }
  /* A button now, because each one opens the card explaining it. The reset is
     what keeps it looking exactly like the box it has always been rather than
     like a button, and min-width:0 stops the three of them refusing to shrink
     below their content on a narrow phone -- flex items default to auto. */
  .statsHeadline .stat{ flex:1; min-width:0; position:relative; background:var(--panel-light); border:2px solid var(--ink); padding:12px 8px; text-align:center;
    font:inherit; color:inherit; display:block; cursor:pointer; }
  /* A figure that opens an explanation has to look like it does. The same
     3px offset shadow every other button in a panel carries, and the same
     press -- players already read that as "this is a button", which a small
     question mark in the corner never quite did. */
  .statsHeadline .stat{ box-shadow:3px 3px 0 var(--ink); }
  .statsHeadline .stat:active{ transform:translate(3px,3px); box-shadow:none; }
  .statsHeadline .stat:hover{ border-color:var(--gold); }
  .statsHeadline .stat:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
  .statsHeadline .statVal{ font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 1.0)); color:var(--gold); }
  .statsHeadline .statLbl{ font-size:max(8px, calc(var(--ui) * 0.7059)); color:var(--text-dim); margin-top:4px; text-transform:uppercase; letter-spacing:0.5px; }
  /* THE THREE HEADLINE FIGURES ON A NARROW PHONE. Billable Time is a
     seven-character clock from the first minute of every shift, and at 320 it
     ran off the right edge of the screen itself -- the tile gave it 69px and
     it wanted 77, even with fitType() already stepped down to its floor (the
     phone sweep, 20 September 2026). The box was the problem, not the type: a
     third of a 320 screen minus 10px gaps minus 8px of padding each side is
     not enough for seven monospace characters at a readable size. So the
     padding and the gap come in on the narrowest phones, which buys the room
     back without shrinking the figure below eleven pixels. Wider phones and
     every desktop are untouched. */
  @media (max-width: 360px){
    body.phoneLayout .statsHeadline{ gap:6px; }
    body.phoneLayout .statsHeadline .stat{ padding:12px 3px; }
  }
  .statsCvToday{ margin-bottom:4px; }
  .statsRecord .val{ color:var(--text-dim); }
  /* The handbook's closing link strip. Styled after .startFooter, which it
     deliberately does not reuse -- that class is position:absolute against the
     bottom of the start screen and would tear straight out of this panel. */
  .handbookResources{ display:flex; flex-wrap:wrap; gap:6px 18px; justify-content:center; text-align:center; font-size:max(8px, calc(var(--ui) * 0.8824)); line-height:1.6; color:var(--text-dim); margin-top:4px; }
  #readmeOverlay .sectionhead{ margin-top:24px; }
  #readmeOverlay .sectionhead + p{ margin-top:0; }
  .daylist{ list-style:none; padding:0; margin:0; font-size:max(8px, calc(var(--ui) * 0.8824)); }
  .daylist li{ display:flex; justify-content:space-between; padding:3px 0; border-bottom:1px solid var(--panel-light); }
  .profit-pos{ color:var(--good); } .profit-neg{ color:var(--danger); }

  .cvBalanceBar{ display:flex; align-items:baseline; gap:8px; background:var(--panel-light); border:2px solid var(--ink); padding:10px 12px; margin-bottom:10px; }
  .cvBalanceBar span{ font-size:max(8px, calc(var(--ui) * 1.1765)); color:var(--gold); }
  .cvBalanceBar strong{ font-size:max(8px, calc(var(--ui) * 1.1765)); color:var(--gold); font-weight:normal; }
  .outfitnote{ font-size:max(8px, calc(var(--ui) * 0.8235)); color:var(--text-dim); margin-bottom:10px; }

  /* store: grid of cards (browse) + a single-item detail view swapped into
     the same panel body, mirroring the existing mail list/detail pattern */
  .storeGrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(94px,1fr)); gap:10px; }
  .storeCard{ background:var(--panel-light); border:2px solid var(--ink); padding:10px 6px 8px; cursor:pointer; position:relative; display:flex; flex-direction:column; align-items:center; gap:5px; text-align:center; }
  .storeCard.equipped{ outline:2px solid var(--gold); outline-offset:2px; }
  .storeCard.dayblocked{ opacity:.5; }
  .storeCard .icon{ width:34px; height:34px; }
  .storeCard[data-key="succulent"] .icon svg, .storeDetail .icon[data-item="succulent"] svg{ transform:scale(1.05); }
  /* The plant is drawn tall and narrow inside its 24x24 box -- the art spans
     about 38% of the box's width -- so at the shared 34px it reads far smaller
     than icons that fill their box corner to corner. It gets its own size here
     rather than a wider drawing, because the drawing's proportions are pinned
     by the floor geometry (see ITEM_ICONS.plant). The negative margins keep the
     card the same height as its neighbours in the grid. */
  /* An office is a picture of a room, not a 24x24 pictogram, so its card is
     twice as wide as everything else and carries a real thumbnail. Three of
     them span the six columns a widened panel gives, so the whole range is on
     screen at once; a fourth would wrap to the next row on its own. */
  /* Offices are pictures of rooms, so they get their own row rather than a
     cell in the grid: one line, twice the width each, scrolling sideways. Three
     fit today and the row simply grows -- which is the point, since the whole
     shop should not have to get wider every time an office is added. */
  .storeGrid.officeRow{ display:flex; grid-template-columns:none; overflow-x:auto; scroll-snap-type:x proximity; padding-bottom:6px; }
  .storeGrid.officeRow .storeCard{ flex:0 0 clamp(150px, 42%, 230px); scroll-snap-align:start; }
  .storeGrid.officeRow::-webkit-scrollbar{ height:6px; }
  .storeGrid.officeRow::-webkit-scrollbar-thumb{ background:var(--text-dim); }
  .storeGrid.officeRow::-webkit-scrollbar-track{ background:transparent; }
  /* The office row's arrows, phones only -- a desktop shows the scrollbar and
     has room for the whole shelf. Over the row's ends, in the game's own
     ink-bordered button style, and shown only when there is something that
     way to go. */
  .officeRowWrap{ position:relative; }
  /* 37% down the row, which is the middle of the office picture -- the cards
     carry a name and a price under it, so the row's own middle sits low. */
  .officeArrow{
    display:none; position:absolute; top:calc(37% - 22px); width:30px; height:44px; padding:0;
    border:3px solid var(--ink); background:var(--panel-light); color:var(--text);
    font-family:'Press Start 2P',monospace; font-size:15px; line-height:1; cursor:pointer;
    box-shadow:2px 2px 0 var(--ink); z-index:2;
  }
  .officeArrow.show{ display:block; }
  .officeArrow.left{ left:-3px; }
  .officeArrow.right{ right:-3px; }
  .officeArrow:not(:disabled):active{ transform:translate(2px,2px); box-shadow:none; }
  .storeCard.officeCard .officeThumb{ width:100%; aspect-ratio:4/3; display:block; border:2px solid var(--ink); background:#8f9296; object-fit:cover; }
  .storeCard.officeCard.locked .officeThumb{ filter:grayscale(0.35) brightness(0.82); }
  .storeDetail .officeThumb{ width:100%; max-width:340px; aspect-ratio:4/3; display:block; margin:0 auto 8px; border:3px solid var(--ink); }
  .storeCard[data-key="plant"] .icon{ width:54px; height:54px; margin:-8px 0 -6px; }
  .storeCard .nm{ font-size:max(8px, calc(var(--ui) * 0.7059)); line-height:1.25; min-height:2.4em; display:flex; align-items:center; }
  .storeCard .price{ font-size:max(8px, calc(var(--ui) * 0.7059)); color:var(--gold); }
  /* Not for sale yet. Greyed rather than hidden: the point is that the player
     can see what is coming, not that the shelf looks empty. */
  .storeCard.soon{ opacity:0.55; cursor:default; }
  .price.soon{ color:var(--text-dim); letter-spacing:0.5px; }
  .storeCard .tag{ font-size:max(8px, calc(var(--ui) * 0.5882)); color:var(--warn); line-height:1.2; margin-top:1px; }
  /* The Employee of the Month Plaque's card: which months are behind the ×N
     count on the name line above -- the engraving itself cannot say so at
     phone widths (see plaqueArt). Small and dim on purpose (a list, not a
     headline), and left to wrap onto as many lines as a long win streak
     needs rather than truncated -- a player with a dozen wins should still be
     able to read all of them. */
  .storeCard .wonMonths{ font-size:max(7px, calc(var(--ui) * 0.5294)); color:var(--text-dim); line-height:1.35; margin-top:-2px; }
  .storeCard .statechip{ position:absolute; top:3px; right:3px; font-size:max(8px, calc(var(--ui) * 0.5294)); text-transform:uppercase; letter-spacing:0.5px; padding:2px 4px; border:1px solid var(--ink); background:var(--panel); color:var(--text-dim); }
  .storeCard .statechip.equipped{ background:var(--gold); color:var(--ink); border-color:var(--ink); }
  .storeCard .previewMini{ position:absolute; top:3px; left:3px; font-size:max(8px, calc(var(--ui) * 0.5882)); width:18px; height:18px; padding:0; display:flex; align-items:center; justify-content:center; background:var(--panel); border:1px solid var(--ink); color:var(--text); cursor:pointer; }
  .storeCard .previewMini.stop{ background:var(--danger); color:#fff; }

  /* ============================================================================
     STAGE 2F -- the Employee of the Month Plaque, the shop's one AWARDED item.
     Never for sale (see award:true, SHOP_ITEMS): the price cell reads AWARDED
     instead of a number, in gold like an ordinary price rather than greyed out
     like .price.soon -- this was earned, not merely announced. ========== */
  .price.awarded{ color:var(--gold); letter-spacing:0.5px; }
  /* ========================================================================= */

  .storeDetail{ text-align:center; padding:4px 6px; }
  .storeDetail .back{ float:left; background:var(--panel-light); border:2px solid var(--ink); color:var(--text); font-size:max(8px, calc(var(--ui) * 0.8235)); padding:5px 10px; cursor:pointer; margin-bottom:14px; }
  .storeDetail .icon{ width:88px; height:88px; margin:8px auto 12px; clear:both; }
  .storeDetail .nm{ font-size:max(8px, calc(var(--ui) * 1.1176)); color:var(--gold); margin-bottom:6px; }
  .storeDetail .price{ font-size:max(8px, calc(var(--ui) * 0.9412)); color:var(--text); margin-bottom:8px; }
  .storeDetail .tag{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--warn); margin-bottom:4px; }
  .storeDetail .note{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim); margin:8px 0; }
  .storeDetail .usdbadge{ display:inline-block; font-size:max(8px, calc(var(--ui) * 0.7059)); color:var(--text-dim); background:var(--panel); border:1px dashed var(--text-dim); border-radius:3px; padding:5px 10px; margin:10px 0; cursor:pointer; }
  .storeDetail .previewBtn{ display:block; width:100%; font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text); background:var(--panel); border:2px solid var(--ink); padding:9px 10px; margin-bottom:10px; cursor:pointer; font-family:inherit; }
  .storeDetail .actionBtn{ display:block; width:100%; background:var(--good); border:3px solid var(--ink); color:var(--ink); font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.7059)); padding:14px 10px; cursor:pointer; box-shadow:4px 4px 0 var(--ink); letter-spacing:0.5px; margin-top:6px; }
  .storeDetail .actionBtn:disabled{ background:var(--panel); color:var(--text-dim); cursor:not-allowed; box-shadow:none; }

  /* Above the tutorial layer (70), not below it. A toast fired DURING the tour
     -- the refusal when a locked icon is clicked, the thank-you when an
     Applicant finishes a pile of paper -- used to be painted under the dim and
     under the card, which is where the owner found them: legible only if you
     already knew they were there. A toast is a four-second message; if it is
     worth showing at all it is worth showing on top. Still below the Turnstile
     slot (9999), so a robot check is never buried. */
  .toastwrap{ position:absolute; top:8px; left:50%; transform:translateX(-50%); display:flex; flex-direction:column; gap:6px; z-index:80; align-items:center; pointer-events:none; }
  .toast{ background:var(--gold); color:var(--ink); border:2px solid var(--ink); font-size:14px; padding:6px 12px; box-shadow:3px 3px 0 rgba(0,0,0,.5); animation:toastIn 4.5s ease forwards; }
  @keyframes toastIn{ 0%{opacity:0; transform:translateY(-8px);} 8%{opacity:1; transform:translateY(0);} 85%{opacity:1;} 100%{opacity:0; transform:translateY(-6px);} }
  /* same toast, positioned at a specific point (e.g. the cursor) instead of
     the fixed top-center wrap -- left/top set inline per instance in JS. */
  .toastNear{ position:absolute; z-index:80; pointer-events:none; white-space:nowrap; }
  /* ---- guided tutorial ----
     The layer covers the whole cabinet at a z-index above every overlay (30),
     but BELOW the toasts (80) -- a four-second message has to be readable over
     the dim -- and below the Turnstile slot (9999) so a robot check is never
     buried. pointer-events:none is the load-bearing part: the
     player has to be able to actually click the button being pointed at, so
     only the card itself takes input. */
  /* The layer itself catches nothing. What blocks the office is the dim
     panels inside it, and what does NOT block it is the gaps between them --
     see .tutorialDim. */
  .tutorialLayer{ position:absolute; inset:0; z-index:70; display:none; pointer-events:none; }
  /* ---- the dim, and the ropes ----
     The dim used to be one enormous shadow cast outward by the ring, which
     made the ring and the dim the same object. That bought a cut-out for free
     and cost everything else: a shadow cannot be clicked, so nothing outside
     the highlight was ever actually blocked -- a player could press Return to
     Title mid-tour and break it -- and a shadow has exactly one hole, so a
     step could never light two things at once, nor light something that had to
     stay unclickable.

     It is panels now: opaque rectangles tiling the office AROUND the holes,
     recomputed with the rings. They darken and they swallow clicks, which are
     the same job. A hole is simply a gap nothing was painted over -- full
     brightness, and the click lands on the real game underneath. */
  /* Underneath everything else in the layer, and that is not cosmetic. The
     panels are appended to the layer as they are needed, which puts them AFTER
     the card in document order -- and with no z-index that means on top of it,
     so the tour's own buttons stop responding. Found by hit-testing rather than
     by looking: the card still LOOKS right, because the panels tiling around a
     hole leave the card's area painted exactly once either way. */
  .tutorialDim{ position:absolute; background:rgba(8,8,6,0.72); pointer-events:auto; z-index:0; }
  /* A hole that must be SEEN but not touched -- a panel being explained before
     the player is meant to poke at it. Invisible, and it stops the click. */
  .tutorialHoleBlock{ position:absolute; background:transparent; pointer-events:auto; z-index:1; }
  /* The opposite of a lid: a hole painted over so it does not read as a
     highlight, while clicks still reach straight through it. pointer-events is
     the whole trick. */
  .tutorialShade{ position:absolute; background:rgba(8,8,6,0.72); pointer-events:none; z-index:1; }
  .tutorialLayer.clear .tutorialShade,
  .tutorialLayer.lit .tutorialShade{ background:transparent; }
  /* The opening beat: the office at full brightness with nothing on it, and
     still nothing clickable. The panels are all there and still blocking; they
     just aren't painted. */
  .tutorialLayer.clear .tutorialDim{ background:transparent; }
  /* Lit but locked: the office at full brightness with the panels still there
     swallowing every click. The difference from `clear` is that this one leaves
     the card and its buttons on screen -- it is for a card the player is meant
     to read while they watch, not for a beat with nothing on it. */
  .tutorialLayer.lit .tutorialDim{ background:transparent; }
  /* A quiet beat: the office roped off and empty while something happens in it. */
  .tutorialLayer.beat .tutorialRing,
  .tutorialLayer.beat .tutorialRing2{ display:none !important; }
  .tutorialLayer.clear .tutorialCard,
  .tutorialLayer.clear .tutorialRing,
  .tutorialLayer.clear .tutorialRing2{ display:none !important; }
  /* Lifted over the layer so it keeps working while everything else is inert.
     .cabinet is position:relative with no z-index, so this and the layer share
     a stacking context and 80 beats 70. Only the opening beat and the welcome
     card grant this; from the first real step the panels cover it like
     everything else, and the way out is the skip line under the card. */
  .tutorialEscape{ position:relative; z-index:80; }
  .tutorialLayer.show{ display:block; }
  /* Just the gold frame now. The dim it used to cast lives in .tutorialDim. */
  .tutorialRing{ position:absolute; z-index:2; border:3px solid var(--gold); pointer-events:none; transition:top .18s ease, left .18s ease, width .18s ease, height .18s ease; }
  .tutorialRing.pulse{ animation:tutorialPulse 1.6s ease-in-out infinite; }
  @keyframes tutorialPulse{ 0%,100%{ border-color:var(--gold); } 50%{ border-color:#fff3c4; } }
  /* A step that points at nothing still has a ring element; it just shows no
     frame. The dim no longer depends on it existing anywhere in particular,
     which is what used to make this fiddly. */
  .tutorialRing.noTarget{ display:none; }
  /* ONE number sizes this whole card. Everything inside is expressed in em, so
     it all rides this base -- which means the card's proportions are fixed and
     only its scale changes with the window.

     The middle term is the real behaviour: 2.217cqw is 17px in the owner's own
     ~864px-wide window, which is the size all of this was designed against, so
     nothing moves for him. Bigger windows scale up from there; smaller ones
     scale down until the floor catches them.
     Calibrated against the cabinet's CONTENT box -- container units exclude its
     border and padding, which is 28px of the 795 it measures on screen.

     The floor is not decoration. Pure proportional scaling puts this at 7px on
     a 360px phone, and shrinking below readable was never the point of any of
     this -- on a small screen the answer is layout, not smaller type. The
     ceiling stops a 1000px cabinet from turning the tour into a billboard. */
  .tutorialCard{ font-size:var(--ui); }
  /* Width the same way: proportional through the middle, with a floor so the
     card does not turn into a column on a phone, and never wider than the
     office it sits in. */
  .tutorialCard{ position:absolute; z-index:3; left:50%; transform:translateX(-50%); width:min(88%, max(280px, 56.06cqw)); background:var(--panel); border:3px solid var(--ink); box-shadow:6px 6px 0 rgba(0,0,0,0.5); padding:0.82em 0.94em 0.71em; pointer-events:auto; text-align:center; }
  .tutorialCard.atTop{ top:16px; }
  .tutorialCard.atBottom{ bottom:16px; }
  /* A step that names its own spot. The two classes above are the fallback --
     they only choose a half of the screen, which is why boxes kept ending up
     across the cubicle from the thing they were pointing at. `.placed` clears
     them and lets tutorialReposition write real coordinates. */
  .tutorialCard.placed{ top:auto; bottom:auto; }
  /* A card can be taller than the office it sits in on a short phone -- the
     opening one is. Cap it and let its own contents scroll rather than letting
     the buttons fall off the bottom of the screen. No effect anywhere the card
     already fits, which is every desktop. */
  .tutorialCard{ max-height:calc(100% - 2em); overflow-y:auto; }
  /* A card that must stay on the left half of the office. It is placed hard
     left and its width is capped so its right edge can never cross the middle
     of the play area -- the owner's rule for the Email card: "do not move the
     right edge of the card past the x 50% coordinate ... It needs to stay on
     the left side of the screen and never cover the computer screen."

     44cqw rather than 50: the play area is inset within the cabinet the
     container units measure, and the card starts at its left wall rather than
     the cabinet's. */
  .tutorialCard.leftHalf{ width:min(88%, max(240px, 38cqw)); }
  .tutorialCard.placedRight{ left:auto; transform:none; }
  /* A card whose left edge is placed explicitly, rather than being centred
     across the office. transform:none because the centring is a -50% shift. */
  .tutorialCard.placedLeft{ transform:none; }
  /* max(), not a bare fraction: these three are the smallest text on the card,
     and by the time the base has bottomed out on a phone a plain 0.53em of it
     is 7px. Above the floor they scale in lockstep with everything else. */
  .tutorialProgress{ font-family:'Press Start 2P',monospace; font-size:max(8px, 0.53em); color:var(--gold); letter-spacing:1px; margin-bottom:0.47em; }
  /* The opening card is a panel rather than a step, so it wears the same gold
     header band every other panel in the game wears. Bled out to the card's
     edges with negative margins that undo .tutorialCard's own padding. */
  .tutorialHead{ display:none; margin:-0.82em -0.94em 0.71em; font-size:max(9px, 0.65em); }
  .tutorialCard.hasHead .tutorialHead{ display:block; }
  /* pre-line so a step can hold a paragraph break. Everything else in the
     script is a single paragraph and is unaffected. */
  /* Centred, like everything else on the card.

     This has been round three times. Justified first, which on a box this
     narrow opened rivers of white space and stretched short lines to the
     margins -- the owner called it "weird issues". Then flush left, which reads
     well under a full paragraph and badly under the one-line instructions that
     make up most of the tour: a single short sentence hard against the left
     edge of a centred card looks like a mistake. Centred is what the title, the
     buttons and the skip line already do, so the card now has one axis instead
     of two. */
  .tutorialText{ font-size:1em; line-height:1.5; color:var(--text); margin-bottom:0.71em; white-space:pre-line; text-align:center; }
  /* Two paragraphs of prose centred line by line reads as a poster rather than
     as writing, and the ragged edges fight the eye on the way down. Only the
     cards that carry real copy ask for this; the one-line instructions are
     short enough that centring them still looks deliberate. */
  /* Kept as the escape hatch for a card that ever needs to break ranks. No
     step sets it today. */
  .tutorialCard.alignLeft .tutorialText{ text-align:left; }
  /* The failsafe navigation, floated under whatever the hidden card got out of
     the way of. Same three controls in the same order as the card's own, so it
     reads as the card's bottom edge rather than as a new thing to learn.
     Deliberately no panel behind it: it sits on a lit hole, and a box drawn
     round it would look like part of the email it is underneath. */
  .tutorialFloatBar{ position:absolute; z-index:3; left:50%; transform:translateX(-50%); font-size:var(--ui); pointer-events:auto; text-align:center; }
  .tutorialBtns{ display:flex; gap:0.47em; justify-content:center; flex-wrap:wrap; }
  /* The job offer at the end stacks its two answers instead of setting them
     side by side, so accepting reads as the decision and declining as the
     alternative underneath it rather than its equal. Both stay one line tall --
     THINK ABOUT IT is the longer label and must not wrap, or the two buttons
     end up different heights. */
  .tutorialBtns.stacked{ flex-direction:column; align-items:center; }
  /* Wider than they were, and centred. CONTINUE AS APPLICANT is the longest
     label in the tour and it was sitting hard against the right edge of its own
     button -- .startbtn is a flex child elsewhere, so it inherited an alignment
     that only makes sense in a row. */
  .tutorialBtns.stacked .startbtn{ width:min(25em, 100%); white-space:nowrap; text-align:center; }
  /* em against the CARD's base, not against the button's own font-size, which
     is what an em inside a font-size declaration would mean. Hence the padding
     is written in em of the button's own (already scaled) size. */
  /* The failsafe row is a SIBLING of the card, not a child of it, so this rule
     used to miss it entirely and its two buttons rendered at the game's full
     button size -- visibly chunkier than the identical pair on every card. */
  .tutorialCard .startbtn,
  .tutorialFloatBar .startbtn{ font-size:max(9px, 0.65em); padding:0.91em 1.27em; }
  /* The quiet way forward on a step that is waiting for the player to do
     something. Always present, so nobody can be trapped by a step whose target
     they cannot reach. */
  /* The one way past the sign-in desk, so it has to read as a real offer
     rather than fine print -- but quieter than SEND CODE, which is still the
     thing most people should press. The link is the only clickable part. */
  .empApplicantNote{ margin:10px 0 0; text-align:center; color:var(--text-dim); font-size:max(8px, calc(var(--ui) * 0.8824)); line-height:1.4; }
  .empApplicantNote a{ color:#7fc4ef; text-decoration:underline; }
  .empApplicantNote a:hover{ color:#a9dcff; }
  /* The way out, on its own line under the buttons. GO BACK sits up in the
     button row with the card's own answers -- it is a thing you press to move
     around the tour, not a footnote. */
  .tutorialNav{ display:flex; justify-content:center; align-items:center; margin-top:0.59em; }
  .tutorialSkip{ display:block; margin:0; background:none; border:none; color:var(--text-dim); font-family:'VT323',monospace; font-size:max(11px, 0.88em); text-decoration:underline; cursor:pointer; }
  .tutorialSkip:hover{ color:var(--gold); }
  /* The same shape and size as the card's own answers, in the quieter colour
     the Terms gate's DECLINE uses -- so a row of three reads as two answers
     and a way back rather than three equal choices.
     Leftmost, because the way forward belongs on the right: that is where a
     reader of English expects the next thing to be, and every wizard and
     installer they have ever used puts it there. */
  .tutorialBack{ background:var(--panel-light); color:var(--text); }
  /* Except when the buttons stack, where "first" is top rather than left and
     the way out does not belong above the offer. */
  .tutorialBtns.stacked .tutorialBack{ order:1; }
  /* On a card that names its own way out, that is an answer rather than a
     navigation control -- so it wears the same green as the answer beside it. */
  .tutorialBack.asAnswer{ background:var(--good); color:var(--ink); }
  /* When a step names its own third answer it is a real choice rather than a
     way out, so it reads as a button instead of a footnote. */
  .tutorialSkip.asButton{ font-family:'Press Start 2P',monospace; font-size:max(9px, 0.59em); color:var(--gold); text-decoration:none; border:2px solid var(--ink); background:var(--panel-light); padding:0.9em 1.4em; box-shadow:3px 3px 0 var(--ink); }
  .tutorialSkip.asButton:active{ transform:translate(3px,3px); box-shadow:none; }

  /* ---- a phone: the card docks UNDER the office instead of on it ----------
     On a phone held upright the office is about 331x248, and this card's
     readable floor (280px wide, 13px type, set above) covers most of it
     wherever it is put -- and every hand-measured `place` coordinate in the
     script was taken on a desktop, so none of them mean anything at that size.
     So on a phone it is not put in the office at all: it becomes a sheet
     pinned to the bottom of the screen, below the cabinet, and the ring and
     the dim do the pointing on their own. There is ~350px of empty screen
     under the cabinet on an 844px phone doing nothing else.

     game.js decides, at the same 620px line Solitaire leaves the cabinet at,
     and toggles both classes here -- .docked on the card and .tourDocked on
     the body. Deliberately NOT body:has(.tutorialCard.docked): :has() only
     reached Firefox in December 2023, nearly a year after the container
     queries this game already needs, and its failure mode here would be the
     sheet sitting on the office -- exactly the bug this fixes.

     Last in the section on purpose: it ties with .leftHalf, .atTop and
     .atBottom on specificity and has to win on order. (game.js strips those
     classes while docked as well -- belt and braces.) */
  .tutorialCard.docked{
    position:fixed; left:0; right:0; bottom:0; top:auto;
    width:auto; max-width:none; transform:none;
    max-height:40vh; overflow-y:auto;
    border-width:3px 0 0; box-shadow:0 -6px 0 rgba(0,0,0,0.5);
  }
  /* While the sheet is up the office moves to the top of the screen and the
     page keeps the sheet's room free underneath it, so the sheet can never sit
     on the coffee button -- the page scrolls instead, the way the short-screen
     rule near .cabinet already lets it.

     --tourDockH is the sheet's MEASURED height, written by tutorialSetDockRoom
     every tick. A flat 40vh would reserve 256px on a 640px phone for a card
     that is often nearer 160, and every pixel over-reserved is a pixel the
     player has to scroll past to reach the bottom of the office. The fallback
     covers the tick before the first measurement lands. */
  body.tourDocked{ align-items:flex-start; padding-bottom:calc(var(--tourDockH, 40vh) + 16px); }
  /* Both, and set from JS rather than with :has() for the reason above. The
     short-screen rule near .cabinet already frees these two together; the page
     needs to be allowed to grow past the viewport for the reserved room below
     the office to be reachable at all. */
  html.tourDocked, body.tourDocked{ height:auto; }
  /* THE PHONE LAYOUT IS NO EXCEPTION, and it was tempting to make it one. Its
     whole premise is the page on one screen with no scrolling (the owner, 15
     September 2026), so the first attempt took the sheet's height out of the
     ROOM instead of adding it to the page. It cannot work: on a 664px phone the
     top bar, the shortest room the desk can survive, the dashboard and the tabs
     already come to 648, and the sheet is another 135-210 on top. The room has
     nothing left to give -- shrinking it past its floor cuts off the desk, and
     two of the nine cards point at things ON the desk.

     So the page grows here exactly as it does everywhere else, and the tour
     does the scrolling for the player: tutorialScrollDockedIntoView() in
     game.js keeps whatever the card is pointing at in the band of screen above
     the sheet, on every card. Nothing is ever out of reach, and the scrolling
     stops the moment the tour ends. */

  /* The one toast that does not fade -- see showReloadToast in game.js. */
  /* pointer-events:auto, because .toastwrap above has them off so an ordinary
     toast never swallows a click meant for the room behind it. This one has
     BUTTONS: without this, Reload and ✕ on "New shift schedule posted." could
     not be pressed at all, on any device (found by the owner on his phone,
     15 September; it was true on a desktop too). */
  .toast.persistent{ animation:none; opacity:1; display:flex; align-items:center; gap:10px; pointer-events:auto; }
  /* And a thumb can hit them on a phone. */
  /* Full width on a phone, so the sentence reads on one line instead of three
     words to a line in a narrow box over the title. */
  body.phoneLayout .toast.persistent{ font-size:15px; width:calc(100vw - 24px); max-width:420px; box-sizing:border-box; }
  body.phoneLayout .toast.persistent > span{ flex:1 1 auto; min-width:0; }
  body.phoneLayout .toastReloadBtn, body.phoneLayout .toastDismissBtn{ min-height:40px; padding:4px 14px; font-size:15px; }
  .toastReloadBtn, .toastDismissBtn{ font-family:'VT323',monospace; background:var(--ink); color:var(--gold); border:2px solid var(--ink); padding:2px 10px; cursor:pointer; font-size:14px; }
  .toastDismissBtn{ padding:2px 7px; color:var(--text-dim); }
  .toastReloadBtn:hover{ background:#33352a; }

  /* inbox */
  .mailTabs{ display:flex; border-bottom:3px solid var(--ink); }
  .mailTab{ flex:1; background:var(--panel-light); border:none; color:var(--text-dim); font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647)); padding:10px 4px; cursor:pointer; }
  .mailTab.active{ background:var(--panel); color:var(--gold); }
  /* The Employee File's TODAY | THIS MONTH | ALL TIME tabs borrow Email's
     folder tabs; today's review button sits at the right end of the row. */
  .empTabsRow{ display:flex; align-items:center; gap:8px; margin:10px 0 12px; }
  .empTabsRow .empTabs{ flex:1; min-width:0; border:2px solid var(--ink); }
  .empTabs .mailTab{ padding:8px 4px; white-space:nowrap; }
  .mailTab .cnt{ background:var(--danger); color:#fff; border-radius:8px; padding:0 5px; font-size:max(8px, calc(var(--ui) * 0.6471)); margin-left:5px; }
  .mailList{ list-style:none; margin:0; padding:0; }
  .mailRow{ padding:9px 6px; border-bottom:1px solid var(--panel-light); cursor:pointer; display:flex; flex-direction:column; gap:2px; }
  .mailRow:hover{ background:var(--panel-light); }
  .mailRow .top{ display:flex; justify-content:space-between; font-size:max(8px, calc(var(--ui) * 0.8235)); color:var(--text-dim); }
  .mailRow .subj{ font-size:max(8px, calc(var(--ui) * 0.9412)); }
  .mailRow.unread .subj{ font-weight:700; color:var(--text); }
  .mailRow.unread .top{ color:var(--gold); }
  .mailEmpty{ color:var(--text-dim); padding:16px 4px; }
  .mailDetail .back{ background:var(--panel-light); border:2px solid var(--ink); color:var(--text); font-size:max(8px, calc(var(--ui) * 0.8235)); padding:5px 10px; cursor:pointer; margin-bottom:10px; }
  .mailDetail .from{ color:var(--text-dim); font-size:max(8px, calc(var(--ui) * 0.8235)); }
  .mailDetail .subj{ font-size:max(8px, calc(var(--ui) * 1.1176)); margin:4px 0 10px 0; color:var(--gold); }
  /* pre-wrap so an email body can use blank lines between paragraphs (the
     onboarding email does). Every other body is a single line with no \n in it,
     so none of them render any differently for it. */
  .mailDetail .body{ font-size:max(8px, calc(var(--ui) * 1.0)); line-height:1.4; white-space:pre-wrap; }
  /* A real &lt;button&gt; dressed as a link, so it sits inside a sentence in the
     body text. Inherits the body's font and size deliberately -- it should read
     as part of the memo, not as a control bolted underneath it. */
  .mailInlineLink{ display:inline; background:none; border:none; padding:0; margin:0;
    font-family:inherit; font-size:inherit; line-height:inherit; color:#4da6ff;
    text-decoration:underline; cursor:pointer; white-space:normal; }
  .mailInlineLink:hover{ color:#7cc0ff; }
  .replyBox{ margin-top:16px; padding-top:12px; border-top:2px dashed var(--panel-light); }
  /* ---- the Email window's size ----

     Email used to have no size of its own: it was a plain .panelBox, so it took
     that rule's max-width (about 462px, the thirty-character cap) and grew
     downward to fit whatever message was open. The growth is the bug. The
     window manager centres a window from the size it has AT ADOPTION, and the
     message arrives afterwards -- so a long one pushed the bottom of the window
     off the bottom of the glass and nothing inside ever overflowed, which meant
     no scrollbar either. Measured on 8 September at 1440x900: the window ran
     132px past the play area and SEND REPLY sat 115px outside it, unreachable
     by any means at all.

     A size of its own fixes both halves at once. The window cannot grow, so it
     stays where it was centred; and .panelBody -- which has always had
     overflow-y:auto -- finally has something to scroll, because the message is
     now taller than the box holding it. No JavaScript was needed for either.

     The numbers are the owner's, on 8 September: about five per cent more width
     on each side than the old cap (65% of the play area, now 75%), and the
     window brought up out of the bottom of the screen. Measured after: centred
     with 92px either side and 61px above and below, and SEND REPLY 64px inside
     the play area on the longest message in the game. */
  #mailPanel{ width:min(75%, 640px); height:min(84%, 660px); max-width:none; }
  /* An answered email keeps its dashed divider so the detail view doesn't
     visibly shrink, but says so plainly where the compose box used to be. */
  .replyBox.replied{ color:var(--text-dim); font-size:max(8px, calc(var(--ui) * 0.8824)); letter-spacing:0.5px; }
  .replyBox textarea{ width:100%; min-height:70px; background:var(--panel-light); border:2px solid var(--ink); color:var(--text); font-family:'VT323','Courier New',monospace; font-size:max(8px, calc(var(--ui) * 0.9412)); padding:8px; resize:vertical; margin-bottom:8px; }

  /* ---- attachment viewer (a deck that arrived by email) ----
     The stage keeps the 4:3 shape a deck is drawn at and scales the SVG to fit
     whatever room the panel has, so the same slides read on a phone and on a
     desktop without a second layout. min-height:0 on the body is what lets the
     stage shrink inside the flex column instead of pushing the nav bar off. */
  .deckBox{ max-width:min(920px, 94vw); }
  .deckBody{ display:flex; flex-direction:column; min-height:0; gap:10px; }
  .deckStage{
    flex:1; min-height:0; display:flex; align-items:center; justify-content:center;
    background:var(--ink); border:2px solid var(--ink); overflow:hidden;
  }
  .deckStage svg{ display:block; width:100%; height:100%; max-height:70vh; object-fit:contain; }
  .deckBar{ display:flex; align-items:center; justify-content:space-between; gap:10px; flex-shrink:0; }
  .deckNav{ font-size:11px; padding:8px 14px; }
  .deckNav[disabled]{ opacity:0.4; cursor:default; }
  .deckCount{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim); letter-spacing:1px; }
  /* The approval button only exists on the last slide, and it is the only
     control the deck ever offers. */
  .deckApprove{ font-size:11px; padding:8px 18px; }
  .deckApproved{ font-size:max(8px, calc(var(--ui) * 0.7647)); color:var(--text-dim); }
  /* The paperclip line under an email body. */
  .mailAttach{
    margin-top:14px; padding-top:12px; border-top:2px dashed var(--panel-light);
    display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  }
  .mailAttachBtn{
    display:flex; align-items:center; gap:8px; background:var(--panel-light);
    border:2px solid var(--ink); box-shadow:2px 2px 0 var(--ink); color:var(--text);
    font-family:inherit; font-size:max(8px, calc(var(--ui)*0.8235)); padding:8px 12px;
    cursor:pointer; text-align:left; max-width:100%;
  }
  .mailAttachBtn:hover{ background:var(--panel); }
  /* The filename is the long part and the part worth reading, so it is the one
     allowed to wrap; the size stays on one line beside it. */
  .mailAttachName{ overflow-wrap:anywhere; }
  .mailAttachSize{ color:var(--text-dim); white-space:nowrap; }
  /* THE CHIP A THUMB HAS TO HIT. On a phone this is how a photograph clipped to
     an email is opened again, and it came out about eleven pixels tall against
     the forty-four the phone build holds everything else to (measured in the
     pre-camera audit, 20 September 2026). The type grows with it: a chip a
     thumb can hit carrying a filename nobody can read is half a fix. */
  body.phoneLayout .mailAttachBtn{ min-height:44px; font-size:13px; padding:10px 12px; }
  body.phoneLayout .mailAttach{ gap:8px; }

  /* desktop shell (hidden -- reachable only via the /desktop terminal command) */
  /* THE DESKTOP RUNS EDGE TO EDGE. The owner, 23 September 2026: "I do think we
     can go clear to the edge on the left side. I still like having the game
     going on behind it ... I just want it to grow wider and have the vertical
     scale accordingly, not fill the entire screen."

     So the panel gives up everything that made it a window inside a window --
     its border, its shadow, its title row and its margins -- and what is left
     is the glass, the full width of the game area and 4:3 like a monitor, with
     the office showing above and below it. The overlay centres it for free.

     The title row went with them: it said DESKTOP over a thing that is plainly
     a desktop, and it was the only place the X had to sit. The X moves inside
     the glass's corner, thumb-sized, the way the Leaderboard's does on a
     phone. */
  #desktopOverlay{ padding:0; }
  .desktopBox{ max-width:none; width:100%; border:0; box-shadow:none; background:none; }
  /* The title row is gone, but the ? it carried is not: the card behind it is
     how a player learns what SYNERGY is. So the row is lifted out of the
     layout into the glass's corner with its heading hidden, which keeps the
     button the page wired at load rather than inventing a second one the help
     code would never fill in. */
  .desktopBox .desktopHead{ position:absolute; top:8px; right:56px; z-index:25; padding:0; background:none; border:0; text-align:right; }
  .desktopBox .desktopHead > span{ display:none; }
  .desktopBox .desktopHead .helpBtn{ width:44px; height:44px; margin:0; }
  #desktopBox > .panelClose{ top:8px; right:8px; width:44px; height:44px; font-size:18px; z-index:25; }
  /* container-type:size (not the more common inline-size) because the fix
     below responds to this element's HEIGHT, not its width -- see the
     @container rule near .desktopIcon. */
  .desktopScreen{ container-type:size; container-name:desktopScreen; position:relative; flex:0 0 auto; aspect-ratio:4/3; min-height:0; margin:0; border:2px solid var(--ink); border-left:0; border-right:0; background:radial-gradient(120% 140% at 30% 12%, #322f27 0%, var(--monitor) 55%, #15130e 100%); overflow:hidden; box-shadow:inset 0 0 40px 12px rgba(0,0,0,0.55); }
  .desktopScreen::before{ content:''; position:absolute; inset:0; pointer-events:none; background:repeating-linear-gradient(180deg, rgba(0,0,0,0.16) 0px, rgba(0,0,0,0.16) 1px, transparent 2px, transparent 4px); }
  /* THE SYNERGY LOGO, BEHIND EVERYTHING. Half the glass's width, which is a
     quarter of its area -- the owner's measurement, 23 September 2026 -- and a
     fifth of full strength, which is what keeps a white icon label readable
     over the brightest part of it. Its own element rather than a second
     background on the glass, because a background image cannot be faded on its
     own. A bought background hides it: see applyWallpaper. */
  .deskLogo{ position:absolute; left:50%; top:44%; width:50%; aspect-ratio:260/230; transform:translate(-50%,-50%);
    background:url("/play/skins/synergy-logo.png") center/contain no-repeat; opacity:.2; pointer-events:none; z-index:0; }
  .desktopScreen::after{ content:''; position:absolute; right:10px; bottom:calc(max(22px, var(--ui) * 2.3) + 6px); width:6px; height:6px; border-radius:50%; background:var(--monitor-glow); box-shadow:0 0 5px 2px var(--monitor-glow); }

  /* Safari on iOS zooms the entire page in when a text field smaller than 16px
     takes focus, and does not zoom back out afterwards -- so one tap on the
     terminal leaves the player stranded in a magnified game. Only on touch, so
     the deliberate small type survives on a desktop. */
  @media (pointer: coarse){
    /* 16px is not a preference, it is the exact threshold iOS uses: at 15.99px
       it zooms, at 16px it does not. The floor was 8px here, which meant the
       rule read as a zoom fix while never actually clearing the bar -- measured
       at 12.2px on an iPhone 14, and the page duly zoomed on every tap into the
       sign-in email, the six-digit code, the reply box and the TPS initials. */
    /* .replyBox textarea is listed by name because it has to be. This rule's
       bare `textarea` is one element selector; the reply box's own font-size
       further up is a class AND an element, which outranks it -- so the one
       field this fix was mainly written for was the one field it never
       reached. Measured on an iPhone 12: every other input resolved to 16px
       and the reply box to 12.24px, which is why replying to Natalie left the
       game zoomed in with the onboarding card cropped off the bottom. */
    .terminalInput, .formfield, textarea.formfield, .replyBox textarea, input[type="text"], input[type="email"], input[type="number"], textarea, select{ font-size:max(16px, calc(var(--ui) * 0.9412)); }
  }
  /* min(90px, 15%): the two icon columns below sit at left:3% and left:18%, so
     15% of .desktopScreen's width is exactly the column gap -- at any width
     that keeps 15% >= 90px this is just 90px (unchanged from before), but
     below that (a phone-narrow .cabinet) it shrinks with the column spacing
     instead of overflowing into the next column. Found and fixed during the
     mobile runtime pass (audit item (c)): at 359px cabinet width, "Company
     Handbook" and "Solitaire" (and "Email" and "TPS Reports") overlapped into
     illegible garbled text -- not a squint-and-manage case, a wrong-icon-tap
     case. */
  /* A tenth bigger, and the name closer to the picture: "I also want the icons
     on the desktop to all be about 10% bigger by default and 15% closer to the
     type that names them- the gap between the icon and the type is too big"
     (the owner, 23 September 2026). The box keeps its width -- that is what
     holds a two-word label on one or two tidy lines -- and it is the drawing
     and the type that grow. */
  .desktopIcon{ position:absolute; display:flex; flex-direction:column; align-items:center; gap:calc(var(--ui) * 0.2303); width:min(calc(var(--ui) * 6.0951), 15%); background:none; border:none; cursor:grab; touch-action:none; padding:2px; }
  .desktopIcon .glyph{ position:relative; font-size:max(12px, calc(var(--ui) * 1.8623)); width:max(20px, calc(var(--ui) * 2.6148)); height:max(20px, calc(var(--ui) * 2.6148)); display:flex; align-items:center; justify-content:center; filter:drop-shadow(1px 1px 0 rgba(0,0,0,.5)); }
  /* Down a tenth on 24 September 2026, once the pictures actually filled these
     boxes and he could see them at the right size: "I think we can go back down
     by about 10% in size on the desktop icons overall now, but the spacing is
     looking good between the icons and their titles." The gap under the picture
     is set from the type unit rather than from the box, so it is unchanged. */
  /* THE PICTURE FILLS ITS BOX. Every icon's drawing carries width="29" in its
     own markup, so while the box grew and shrank with the window the drawing
     stayed 29 pixels across -- on a wide screen a 29-pixel picture sat in the
     middle of a 50-pixel box, which is what made the icons look small, the gap
     under them look wide, and Email's number look as though it were floating
     beside the envelope rather than stuck to it. The owner spotted all three
     as separate faults on 24 September 2026; they were one.

     Asked for on 23 September and only half done then: "we need them to scale
     up and down with the window so that they're always a similar size in
     proportion to the window." */
  .desktopIcon .glyph svg{ width:100%; height:100%; }
  /* The unread count on the desktop's Email icon sits on the picture, not on
     the button, which is a wide box with a label under it. */
  /* Up against the corner rather than over it: the bottom-left of the number
     touches the top-right of the picture, which is what the owner asked for on
     23 September -- "not over top of it but just up against the corner". */
  /* THE UNREAD COUNT ON THE DESKTOP ICON. Bigger than the one on the Quickbar
     tile, because the icon it rides on is bigger, and sized from the type unit
     so it grows with the window like everything else on this desk. A quarter
     up on 24 September 2026 at the owner's word.

     Centred by flexbox rather than by line-height: a fixed line-height set the
     digit by its baseline, which left it sitting low in the circle. */
  .desktopIcon .glyph .quickBadge{ top:auto; bottom:100%; left:100%; right:auto;
    height:max(12px, calc(var(--ui) * 1)); min-width:max(12px, calc(var(--ui) * 1));
    padding:0 calc(var(--ui) * 0.16); border-radius:999px;
    font-size:max(8px, calc(var(--ui) * 0.62)); line-height:1;
    display:flex; align-items:center; justify-content:center; }
  /* THE LASSO. Dashed, at the owner's word -- "Let's use dashed lines" -- over
     the faintest wash, which is what tells a box being dragged apart from the
     dashed ring around a folder being renamed. It is drawn INSIDE the icon
     field and never catches a click of its own. */
  .deskMarquee{ position:absolute; display:none; z-index:8000; pointer-events:none;
    border:1px dashed rgba(255,255,255,0.92); background:rgba(255,255,255,0.09); }
  .deskMarquee.show{ display:block; }
  .desktopIcon:active .glyph{ transform:translate(1px,1px); }
  .desktopIcon .lbl{ font-size:max(8px, calc(var(--ui) * 0.745)); color:#fff; text-shadow:1px 1px 0 #000; text-align:center; line-height:1.2; }

  /* ---- the bar along the bottom, and the GO menu (play/synergy-os.js) ----
     The shell renders into #desktopBar and appends #goMenu; everything here is
     the look. Sized in var(--ui) like the icons above, so it shrinks with the
     monitor on a phone instead of eating the screen. The bar sits ON the glass
     rather than below it, because the glass IS the computer -- a strip of chrome
     outside the bezel would read as part of the game's furniture, not SYNERGY's. */
  /* The bar lives on the glass, part of the monitor, and programs open OVER
     it. The owner's call, and it keeps the bar looking like what it is rather
     than a strip of chrome across the game.

     So while a program is open, GO and the tabs are behind it. That is fine:
     the way back is the window's own title bar, which is always at the top of
     the window. Two other arrangements were tried and both were worse -- a bar
     floating above the windows sliced the bottom row off the Calculator, and a
     taskbar across the whole game turned the monitor into a frame around
     somebody else's operating system. */
  .desktopBar{ position:absolute; left:0; right:0; bottom:0; z-index:20; display:flex; align-items:center; gap:calc(var(--ui) * 0.4); padding:calc(var(--ui) * 0.24) calc(var(--ui) * 0.4); min-height:max(22px, calc(var(--ui) * 2.3)); background:linear-gradient(180deg, #34322b 0%, #22211b 100%); border-top:1px solid #4a473d; box-shadow:0 -2px 6px rgba(0,0,0,.45); }
  /* THE QUICKBAR, beside GO, with a rule between them.

     "This just goes next to the GO button. Just an extra place for icons. Put
     a little | between the go bar and the quickbar" -- and, looking at it,
     "can we make the quickbar and its icons about 10% bigger overall?" So the
     bar itself grew from 2.1 to 2.3 of the type unit and the icons are sized
     against the same unit, which is what makes them grow and shrink with the
     window the way he asked ("we need them to scale up and down with the
     window so that they're always a similar size in proportion to the
     window"). */
  .barSep{ flex:0 0 auto; color:#6b675a; font-size:max(12px, calc(var(--ui) * 1.2)); line-height:1; user-select:none; }
  .quickbar{ flex:0 0 auto; display:flex; align-items:center; gap:calc(var(--ui) * 0.3); }
  .quickItem{ position:relative; flex:0 0 auto; width:calc(var(--ui) * 1.7); height:calc(var(--ui) * 1.7); display:flex; align-items:center; justify-content:center; background:none; border:1px solid transparent; border-radius:2px; padding:0; cursor:pointer; }
  .quickItem:hover{ border-color:#5d594a; background:rgba(255,255,255,.05); }
  .quickItem:active{ transform:translate(1px,1px); }
  .quickItem svg{ width:calc(var(--ui) * 1.4); height:calc(var(--ui) * 1.4); }
  /* The Marketplace's bag is drawn small inside its own 24-unit box -- it has
     more empty margin around it than the other icons do -- so at the same
     square it reads as the runt of the row. Fifteen per cent more drawing in
     the same square, which is what the owner asked for: "15% bigger inside the
     square it's in- not making the space for it bigger than the other icons". */
  /* The shirt is drawn smaller inside its own square than the other four, so
     it needs more room to READ the same size. Raised twice, on 23 and 24
     September: "we can still make the corporate store icon 10% bigger than it
     is currently, and make sure it stays centered in the space its in." */
  .quickItem[data-quick="marketplace"] svg{ width:calc(var(--ui) * 1.95); height:calc(var(--ui) * 1.95); margin:0 auto; }
  /* Where a dragged icon would land: a gold rule before the item under the
     pointer, or at the end of the row. */
  .quickItem.quickDropBefore::before{ content:''; position:absolute; left:calc(var(--ui) * -0.22); top:1px; bottom:1px; width:2px; background:var(--gold); }
  .quickbar.quickDropEnd::after{ content:''; flex:0 0 auto; width:2px; align-self:stretch; margin:1px 0; background:var(--gold); }
  .quickbar.quickDragging .quickItem{ cursor:grabbing; }
  /* The unread count, on the Quickbar's Email and on the Game menu's pin of it.
     "Make sure email is showing a notification number here if there is unread
     emails as well." Same number the monitor's badge shows. */
  .quickBadge{ position:absolute; top:-4px; right:-5px; min-width:14px; height:14px; padding:0 3px; border-radius:8px; background:#c23b3b; color:#fff; font-family:'Space Mono',monospace; font-size:max(7px, calc(var(--ui) * 0.52)); font-weight:700; line-height:14px; text-align:center; border:1px solid #201d16; pointer-events:none; }
  .topBarPin{ position:relative; }
  /* The count on a top-bar button: the computer (the phone, on the phone layout --
     same element), Contacts, the Employee File, the Marketplace. The monitor
     keeps its own; this is the second set. Small on
     purpose: it rides the corner of a 22px picture and must not cover it. */
  .titlebar .iconbtn{ position:relative; }
  .titlebar .iconbtn .quickBadge{ top:-7px; right:-8px; min-width:13px; height:13px; line-height:13px; padding:0 2px; font-size:9px; border-radius:7px; }
  .goBtn{ flex:0 0 auto; font-family:inherit; font-weight:700; letter-spacing:.06em; font-size:max(8px, calc(var(--ui) * 0.72)); color:#f4efe0; background:linear-gradient(180deg, #4a4739 0%, #322f27 100%); border:1px solid #5d594a; border-radius:2px; padding:calc(var(--ui) * 0.16) calc(var(--ui) * 0.62); cursor:pointer; text-shadow:1px 1px 0 rgba(0,0,0,.6); }
  .goBtn:hover{ background:linear-gradient(180deg, #5a5645 0%, #3d3a30 100%); }
  .goBtn:active{ transform:translate(1px,1px); }
  .barTabs{ flex:1 1 auto; min-width:0; display:flex; gap:calc(var(--ui) * 0.24); overflow:hidden; }
  .desktopClock{ flex:0 0 auto; font-size:max(7px, calc(var(--ui) * 0.66)); color:#cfc8b8; letter-spacing:.03em; text-shadow:1px 1px 0 rgba(0,0,0,.6); white-space:nowrap; }
  /* Disposal, at the right-hand end of the bar. Half the size of a desktop
     icon's glyph, on the owner's instruction of 8 September -- a bin in a bar
     is a control, not a shortcut, and at full size it wore the bar rather than
     sitting in it. No label: the bar has no room for one and the shape has
     meant this since 1984. */
  /* THE TWO CONTROLS AT THE RIGHT-HAND END: the SYNERGY Assistant and the
     sound button, in that order, with the clock to their left. The owner's
     arrangement of 23 September 2026. The bin used to be here and is an icon
     on the glass again. */
  .barTool{ flex:0 0 auto; display:flex; align-items:center; justify-content:center; width:calc(var(--ui) * 1.7); height:calc(var(--ui) * 1.7); margin-left:calc(var(--ui) * 0.21); padding:0; background:none; border:1px solid transparent; border-radius:2px; cursor:pointer; opacity:.94;
    font-size:max(11px, calc(var(--ui) * 1.05)); line-height:1; color:#e9e4d3; }
  /* The three of them sit closer together than the bar's own gap: "I also want
     those icons and the clock to be about 15% closer to each other- still right
     aligned with the sound button staying where it is currently" (the owner, 23
     September 2026). The bar's gap is 0.4 of the type unit and each tool added
     0.32 of its own, which is 0.72 between one and the next; 0.21 brings that
     to 0.61, a shade under fifteen per cent closer. Only the space to the LEFT
     closes up, so the sound button does not move. */
  .barTool svg{ width:calc(var(--ui) * 1.4); height:calc(var(--ui) * 1.4); filter:drop-shadow(1px 1px 0 rgba(0,0,0,.5)); }
  .barTool:hover{ opacity:1; border-color:#5d594a; background:rgba(255,255,255,.05); }
  .barTool:active{ transform:translate(1px,1px); }

  /* GO opens upward from the bar, because the bar is at the bottom and a menu
     that opened downward would leave the screen. Capped at 70% of the glass
     with its own scroll: the list only grows from here -- Documents, Splotch,
     DocuMax, and whatever comes after -- and the failure to avoid is a menu
     taller than the monitor with its last entries unreachable. */
  /* RE-ARRANGING GO. A row being dragged fades and the gold rule shows where it
     would land, the same language the Quickbar uses. */
  .goMenu.goDragging .goItem, .goMenu.goDragging .goSection{ cursor:grabbing; }
  .goMenu .goDragged{ opacity:.45; }
  .goMenu .goDropBefore{ box-shadow:inset 0 2px 0 0 var(--gold); }
  .goMenu.goDropEnd > .goItem:last-of-type{ box-shadow:inset 0 -2px 0 0 var(--gold); }
  .goMenu{ position:absolute; left:calc(var(--glassL, 0px) + var(--ui) * 0.4); bottom:calc(var(--glassB, 0px) + max(22px, var(--ui) * 2.34)); max-width:calc(var(--glassW, 100%) - var(--ui) * 0.8); z-index:9000; display:none; flex-direction:column; min-width:max(112px, calc(var(--ui) * 13)); max-height:70%; overflow-y:auto; padding:calc(var(--ui) * 0.3) 0; background:linear-gradient(180deg, #2e2c25 0%, #24231d 100%); border:1px solid #5d594a; border-radius:2px; box-shadow:0 4px 14px rgba(0,0,0,.6); }
  /* Above every window. A window is its own overlay and stacks above the
     desktop panel entirely, so a menu rendered inside the glass can never
     win however high its z-index -- which is why this one is a sibling of
     the overlays, borrowing the glass's geometry like they do. */
  .goMenu.show{ display:flex; }
  .goBrand{ font-size:max(7px, calc(var(--ui) * 0.6)); font-weight:700; letter-spacing:.1em; color:var(--monitor-glow); padding:calc(var(--ui) * 0.2) calc(var(--ui) * 0.62) calc(var(--ui) * 0.34); border-bottom:1px solid #46433a; margin-bottom:calc(var(--ui) * 0.24); }
  .goSection{ font-size:max(6px, calc(var(--ui) * 0.52)); letter-spacing:.09em; text-transform:uppercase; color:#8a8375; padding:calc(var(--ui) * 0.34) calc(var(--ui) * 0.62) calc(var(--ui) * 0.14); }
  /* ---- GO's categories ----
     A heading is a BUTTON now, not a label: GO opens showing four of them and
     nothing else, and a click opens one row of programs. It is the same
     typography it always had, so the menu reads the same when a category is
     open -- what is new is that it is reachable, which means it has to look
     reachable on hover and has to say which way it is pointing. */
  button.goSection{ display:flex; align-items:center; justify-content:space-between; gap:calc(var(--ui) * 0.4); width:100%; background:none; border:0; font-family:inherit; text-align:left; cursor:pointer; padding-top:calc(var(--ui) * 0.42); padding-bottom:calc(var(--ui) * 0.42); }
  button.goSection:hover{ background:rgba(255,255,255,.06); color:#cfc6ae; }
  .goCatOpen{ color:var(--gold, #e0b23a); }
  /* Turned rather than swapped for another character, so the heading never
     changes width as it opens and the row beneath it does not jump.

     DOWN when shut, UP when open -- the owner's call, and the one every
     accordion outside a file tree uses. A right-pointing arrow says "there is
     more over here", which is what a submenu does; down says "this opens
     downward", which is what this actually does. */
  .goCaret{ flex:0 0 auto; font-size:max(8px, calc(var(--ui) * 0.7)); line-height:1; transition:transform .12s ease; transform:rotate(90deg); }
  .goCatOpen .goCaret{ transform:rotate(-90deg); }
  /* Shut is genuinely absent, not merely invisible: a hidden row that still
     held its height would leave a gap under every closed heading, and the
     whole point of the change is that four closed categories take four rows. */
  .goCatBody{ display:none; }
  .goCatBodyOpen{ display:block; }
  .goCatBody .goItem{ padding-left:calc(var(--ui) * 1.1); }
  .goItem{ display:flex; align-items:center; gap:calc(var(--ui) * 0.4); width:100%; font-family:inherit; font-size:max(7px, calc(var(--ui) * 0.66)); color:#f4efe0; background:none; border:none; text-align:left; padding:calc(var(--ui) * 0.2) calc(var(--ui) * 0.62); cursor:pointer; }
  .goItem:hover{ background:#3d3a30; }
  .goItem .goGlyph{ flex:0 0 auto; width:max(12px, calc(var(--ui) * 1.2)); height:max(12px, calc(var(--ui) * 1.2)); display:flex; align-items:center; justify-content:center; font-size:max(10px, calc(var(--ui) * 1.05)); }
  .goItem .goGlyph svg{ width:100%; height:100%; }
  .goItem .goLbl{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .goLogOff{ margin-top:calc(var(--ui) * 0.24); border-top:1px solid #46433a; padding-top:calc(var(--ui) * 0.34); }

  /* ---- selection, context menus and renaming (Phase 3b) ---- */
  /* A selected icon, not a hovered one: this has to survive the pointer
     leaving, because the whole point is to click an icon and then do something
     else to it. */
  .desktopIcon.selected .glyph{ background:rgba(143,224,160,.16); outline:1px solid var(--monitor-glow); outline-offset:1px; border-radius:2px; }
  .desktopIcon.selected .lbl{ background:var(--monitor-glow); color:#12160f; text-shadow:none; border-radius:1px; padding:0 2px; }
  /* A folder reads as a folder at a glance -- the manila the office runs on --
     and an icon you are about to drop something INTO says so before you let go,
     because otherwise the player is guessing whether they are over it or near it. */
  .desktopIcon.desktopFolder .glyph{ filter:drop-shadow(1px 1px 0 rgba(0,0,0,.5)); }
  .desktopIcon.dropTarget .glyph{ outline:2px solid var(--monitor-glow); outline-offset:2px; border-radius:3px; background:rgba(143,224,160,.14); }

  /* The label becomes a text box in place. Sized to the label so the icon does
     not jump about while it is being renamed. */
  .iconRename{ font-family:inherit; font-size:max(8px, calc(var(--ui) * 0.6773)); width:100%; min-width:0; text-align:center; color:#12160f; background:#f4efe0; border:1px solid var(--monitor-glow); border-radius:1px; padding:0; }

  /* Context menus reuse the GO menu's look -- same machine, same furniture --
     but are positioned at the pointer rather than anchored to the bar. */
  .ctxMenu{ bottom:auto; max-height:none; min-width:max(96px, calc(var(--ui) * 10)); }
  .ctxSep{ height:1px; background:#46433a; margin:calc(var(--ui) * 0.2) 0; }
  /* The room's right-click menu (Put Away): as wide as its words, with the
     same padding on the right as on the left. */
  .ctxMenu.roomCtxMenu{ min-width:0; width:max-content; }
  .ctxNote{ margin-left:auto; padding-left:calc(var(--ui) * 0.5); font-size:max(6px, calc(var(--ui) * 0.5)); color:#8a8375; font-style:italic; }

  /* A folder opens as a PANEL on the desktop, not as a window: it is part of
     the desktop rather than a program, so it sits above the glass and below
     every window (windows are 9000), and it never gets a tab. */
/* ==== SPLOTCH: copied from tools/splotch/src/style.css ==== */
/* ============================================================================
   SPLOTCH — THE PROGRAM'S OWN LOOK
   ----------------------------------------------------------------------------
   Bob's world: the game's panel colours, its gold, VT323 and Press Start 2P,
   hard offset shadows, two-pixel ink borders. The FEEL of a classic paint
   program -- a toolbox down the left, a palette along the bottom, a sheet in
   the middle -- with none of any real one's icons or exact layout copied.

   Every rule is scoped under .sp so that when this file is folded into
   game.css it cannot reach out and restyle the office.
   ========================================================================= */

.sp, .sp *{ box-sizing:border-box; }

.sp{
  --panel:#23241d; --panel-light:#33352a; --panel-dark:#1a1b15;
  --text:#f4efe0; --text-dim:#b9b39a; --ink:#201d16;
  --gold:#e0b23a; --good:#5c9a5f; --danger:#c23b3b; --warn:#d9a53d;
  --line:#4a4c3c; --paper:#f4efe0;
  --ui:15px;
  font-family:'VT323','Courier New',monospace;
  color:var(--text);
}
.sp button{ font-family:inherit; cursor:pointer; touch-action:manipulation; color:inherit; }
.sp :focus-visible{ outline:3px solid var(--gold); outline-offset:2px; }

/* ---------------------------------------------------------------- the window
   A stand-in for the SYNERGY window this program will live in. When Splotch
   moves into the game the window manager supplies all of this and the shell
   is deleted; nothing inside .spBody knows the difference. */
.spDesk{
  /* height, not min-height. The window is stretched to this, and a stretched
     box only has a height to give its children if the box it is stretching to
     has one of its own -- with min-height the sheet grew the window, the
     window grew the page, and zooming in scrolled the whole browser instead of
     the work surface. */
  height:100vh; min-height:100vh; margin:0; padding:18px;
  background:#111008;
  background-image: radial-gradient(ellipse at 50% 0%, #1c1a12 0%, #0d0c08 70%);
  /* STRETCHED, not top-aligned. The window fills the browser window the way a
     maximized program does, so making the browser bigger makes the SHEET
     bigger -- which is the standalone stand-in for the game's maximize
     button. Top-aligned, the sheet was stuck at 640px on any monitor. */
  display:flex; align-items:stretch; justify-content:center;
}
.spWindow{ width:100%; max-width:920px; background:var(--panel); border:4px solid var(--ink); box-shadow:0 0 0 2px #000, 10px 10px 0 rgba(0,0,0,0.5);
           display:flex; flex-direction:column; }
.spTitlebar{ display:flex; align-items:center; gap:12px; padding:8px 12px; background:linear-gradient(180deg,#3a3c30,#282a20); border-bottom:3px solid var(--ink); }
.spTitlebar .spMark{ width:26px; height:26px; flex:0 0 26px; display:grid; place-items:center; background:var(--gold); color:var(--ink); font-family:'Press Start 2P','VT323',monospace; font-size:10px; border:2px solid var(--ink); }
.spTitlebar h1{ margin:0; font-family:'Press Start 2P','VT323',monospace; font-size:12px; letter-spacing:1px; color:var(--text); }
.spTitlebar .spSubtitle{ font-size:var(--ui); color:var(--text-dim); margin-left:2px; }
.spTitlebar .spSpacer{ flex:1; }
.spWinBtns{ display:flex; gap:5px; }
.spWinBtns span{ width:15px; height:15px; border:2px solid var(--ink); background:#5a5c4a; display:block; }
.spBody{ padding:0; flex:1; min-height:0; display:flex; }

/* --------------------------------------------------------------- the program
   menu bar / toolbox + sheet / toolbox + palette / status.

   The sheet's row is the one that gives: everything else is its own height and
   the drawing area takes what is left, so a taller window is a taller sheet. */
.spApp{
  display:grid;
  grid-template-columns: 96px 1fr;
  grid-template-rows: auto 1fr auto auto;
  grid-template-areas: "menu menu" "tools sheet" "tools palette" "status status";
  flex:1; min-height:0;
  background:var(--panel-dark);
}

/* menu bar */
.spMenuBar{ grid-area:menu; display:flex; gap:2px; padding:3px 6px; background:var(--panel-light); border-bottom:2px solid var(--ink); position:relative; }
.spMenu{ background:none; border:2px solid transparent; padding:2px 10px; font-size:calc(var(--ui) + 3px); }
.spMenu[aria-expanded="true"], .spMenu:hover{ background:var(--panel); border-color:var(--ink); }
.spDrop{ position:absolute; top:100%; left:0; min-width:230px; background:var(--panel); border:3px solid var(--ink); box-shadow:5px 5px 0 rgba(0,0,0,.5); padding:4px; z-index:20; display:flex; flex-direction:column; }
.spMenuItem{ display:flex; justify-content:space-between; gap:14px; text-align:left; background:none; border:2px solid transparent; padding:4px 10px; font-size:calc(var(--ui) + 3px); }
.spMenuItem:hover:not(:disabled){ background:var(--panel-light); border-color:var(--ink); }
.spMenuItem:disabled{ color:var(--text-dim); opacity:.7; cursor:default; }
.spMenuItem .spNote{ font-size:var(--ui); color:var(--gold); }
.spSep{ height:2px; background:var(--line); margin:4px 2px; }

/* toolbox */
.spTools{ grid-area:tools; display:grid; grid-template-columns:1fr 1fr; gap:4px; align-content:start; padding:8px 6px; border-right:2px solid var(--ink); background:var(--panel); }
.spTool{ aspect-ratio:1; display:grid; place-items:center; background:var(--panel-light); border:2px solid var(--ink); box-shadow:2px 2px 0 rgba(0,0,0,.45); padding:0; }
.spTool svg{ width:22px; height:22px; fill:none; stroke:var(--text); stroke-width:2; stroke-linecap:square; stroke-linejoin:miter; }
.spTool[aria-pressed="true"]{ background:var(--gold); box-shadow:inset 2px 2px 0 rgba(0,0,0,.35); }
.spTool[aria-pressed="true"] svg{ stroke:var(--ink); }
.spTool:hover{ filter:brightness(1.12); }
/* The two zoom buttons, at the top of the toolbox, in the toolbox's own
   language: same square button, same stroke drawing, no rule under them. */
.spZoomBtns{ grid-column:1 / -1; display:flex; gap:4px; }
.spZoomBtn{ flex:1; aspect-ratio:auto; height:30px; }
.spZoomBtn svg{ width:19px; height:19px; }
.spZoomBtn:disabled{ opacity:.4; cursor:default; }
.spZoomBtn:disabled:hover{ filter:none; }
.spSizes{ grid-column:1 / -1; display:flex; gap:4px; justify-content:space-between; margin-top:6px; }
.spSizes[hidden]{ display:none; }
.spSize{ flex:1; aspect-ratio:1; display:grid; place-items:center; background:var(--panel-light); border:2px solid var(--ink); padding:0; }
.spSize i{ display:block; border-radius:50%; background:var(--text); }
.spSize[aria-pressed="true"]{ background:var(--gold); } .spSize[aria-pressed="true"] i{ background:var(--ink); }
.spFillTog{ grid-column:1 / -1; margin-top:6px; background:var(--panel-light); border:2px solid var(--ink); padding:3px 0; font-size:var(--ui); }
.spFillTog[aria-pressed="true"]{ background:var(--gold); color:var(--ink); }
.spFillTog[hidden]{ display:none; }
/* Three stamps in a 96px column: stacked, or the third one slides under the sheet and cannot be clicked. */
.spStamps{ flex-direction:column; }
.spStamps .spStamp{ width:100%; }

/* the sheet
   ---------------------------------------------------------------------------
   Three boxes. The STAGE is the cell the layout hands over, whatever shape it
   is, painted in the window's own colour. The WORKING AREA inside it is
   measured out 4 by 3 in JavaScript -- only the code can measure the cell --
   so that a 4 by 3 sheet can fill it exactly at 100%, which is what "when you
   first open it we should only see white canvas" requires. Whatever is left
   over becomes an even margin around it: a mount, not a gap.

   The SHEET's size is set in JavaScript too. Everything here is the look plus
   the scrolling.

   The one-track grid inside the working area is the trick that lets a
   zoomed-in sheet scroll and a zoomed-out one sit in the middle.
   `minmax(min-content, 1fr)` means "at least as big as the sheet, otherwise as
   big as the working area" -- so when the sheet is the larger of the two, the
   area scrolls to it and its top-left corner is still reachable, which plain
   centring does not allow. */
.spStage{ grid-area:sheet; display:grid; place-items:center; padding:10px; background:var(--panel-dark); position:relative; min-height:300px; overflow:hidden; }
.spWork{ display:grid; grid-template-columns:minmax(min-content, 1fr); grid-template-rows:minmax(min-content, 1fr); overflow:auto; border:2px solid var(--ink); box-shadow:4px 4px 0 rgba(0,0,0,.45); background:#5b5749; background-image:repeating-linear-gradient(45deg, rgba(0,0,0,.06) 0 6px, transparent 6px 12px); }
.spPad{ display:grid; place-items:center; position:relative; }
/* Scrollbars you can actually see. The wheel zooms now, so these are how a
   drawing bigger than its window gets moved around -- overlay scrollbars that
   only appear once you scroll would be a way out that nobody could find. Drawn
   in the program's own parts: an ink border and a lighter slider.

   NOT scrollbar-color first. Setting the standard property makes Chrome ignore
   every ::-webkit-scrollbar rule below it and fall back to the operating
   system's overlay scrollbars -- which on a Mac are invisible until you
   scroll. It is kept for the engines that have no other way. */
@supports not selector(::-webkit-scrollbar){
  .spWork{ scrollbar-color:#5a5c4a var(--panel-dark); }
}
.spWork::-webkit-scrollbar{ width:15px; height:15px; }
.spWork::-webkit-scrollbar-track{ background:var(--panel-dark); border:2px solid var(--ink); }
.spWork::-webkit-scrollbar-thumb{ background:#5a5c4a; border:2px solid var(--ink); }
.spWork::-webkit-scrollbar-thumb:hover{ background:var(--gold); }
.spWork::-webkit-scrollbar-corner{ background:var(--panel-dark); }
/* Space held down: the pointer is a hand and a drag moves the drawing. */
.spWork.spGrab .spSheet{ cursor:grab; }
.spSheet{ image-rendering:pixelated; image-rendering:crisp-edges; background:var(--paper); box-shadow:0 0 0 2px var(--ink); cursor:crosshair; touch-action:none; display:block; }
.spTextIn{ position:absolute; font-family:'Press Start 2P','VT323',monospace; font-size:12px; background:rgba(244,239,224,.95); color:var(--ink); border:2px dashed var(--ink); padding:2px 4px; min-width:80px; z-index:5; }

/* palette */
.spPalette{ grid-area:palette; display:flex; align-items:center; gap:8px; padding:8px 10px; background:var(--panel); border-top:2px solid var(--ink); }
.spCurrent{ width:40px; height:40px; border:2px solid var(--ink); box-shadow:2px 2px 0 rgba(0,0,0,.45); flex:0 0 40px; }
.spSwatches{ display:grid; grid-template-columns:repeat(8, 22px); grid-auto-rows:22px; gap:3px; }
.spSwatch{ width:22px; height:22px; border:2px solid var(--ink); padding:0; }
.spSwatch[aria-pressed="true"]{ outline:2px solid var(--gold); outline-offset:1px; }

/* status */
.spStatus{ grid-area:status; display:flex; gap:18px; padding:4px 10px; font-size:var(--ui); color:var(--text-dim); background:var(--panel-light); border-top:2px solid var(--ink); }
.spStatus .spSpacer{ flex:1; }
.spStatus .spFileName{ color:var(--text); }
/* The zoom reading, in the left-hand group with the tool and the coordinates:
   everything about the VIEW is on the left, everything about the FILE on the
   right, and a number that changes there shifts nothing that matters. */
.spStatus .spZoom{ white-space:nowrap; }

/* dialogs -- in-page, never window.confirm; the game never uses browser dialogs */
.spScrim{ position:fixed; inset:0; background:rgba(0,0,0,.55); display:grid; place-items:center; z-index:900; }
.spDialog, .spConfirm{ background:var(--panel); border:3px solid var(--ink); box-shadow:8px 8px 0 rgba(0,0,0,.5); padding:14px 16px; min-width:min(92vw, 380px); max-width:92vw; font-size:calc(var(--ui) + 3px); }
.spDialog h2, .spConfirm h2{ margin:0 0 10px; font-family:'Press Start 2P','VT323',monospace; font-size:11px; letter-spacing:.5px; color:var(--gold); }
.spDialog input{ width:100%; font:inherit; font-size:calc(var(--ui) + 4px); padding:5px 8px; background:var(--paper); color:var(--ink); border:2px solid var(--ink); }
.spDialog .spHint{ font-size:var(--ui); color:var(--text-dim); margin:6px 0 0; min-height:1.2em; }
.spDialog .spNote{ font-size:var(--ui); color:var(--gold); margin:0 0 8px; }
.spBtns{ display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }
.spBtn{ background:var(--panel-light); border:2px solid var(--ink); box-shadow:2px 2px 0 rgba(0,0,0,.45); padding:4px 14px; font-size:calc(var(--ui) + 2px); }
.spBtn.spPrimary{ background:var(--gold); color:var(--ink); }
.spBtn.spDanger{ background:var(--danger); color:var(--text); }
.spBtn:disabled{ opacity:.5; cursor:default; }
.spFiles{ display:flex; flex-direction:column; gap:3px; max-height:46vh; overflow:auto; border:2px solid var(--ink); background:var(--panel-dark); padding:4px; }
.spFile{ display:flex; align-items:center; gap:10px; padding:4px 8px; border:2px solid transparent; cursor:default; }
.spFile[aria-selected="true"]{ background:var(--panel-light); border-color:var(--ink); }
.spFile .spFName{ flex:1; }
.spFile .spFWhen{ color:var(--text-dim); font-size:var(--ui); }
.spDelete{ background:none; border:2px solid var(--ink); width:24px; height:22px; font-size:12px; padding:0; }
.spEmpty{ color:var(--text-dim); padding:10px; text-align:center; }

.spToast{ position:fixed; left:50%; bottom:26px; transform:translateX(-50%); background:var(--panel); color:var(--text); border:3px solid var(--ink); box-shadow:5px 5px 0 rgba(0,0,0,0.5); padding:11px 17px; font-size:calc(var(--ui) + 3px); z-index:1000; max-width:88vw; text-align:center; }

/* ------------------------------------------------------------------ phones
   The sheet fills the width, the toolbox becomes a strip, and the page must
   not scroll while a finger is drawing (touch-action:none on the sheet). */
@media (max-width:600px){
  /* Content-sized, not stretched to the viewport: forced to full height the
     status line was pushed off the bottom of the phone. That is also why the
     work surface loses its 300px floor here -- with no height to fit into,
     the sheet is sized by the WIDTH of the phone and the page scrolls, which
     is what it did before zoom existed. */
  .spDesk{ padding:0; align-items:flex-start; height:auto; }
  .spWindow{ border-width:0; box-shadow:none; }
  .spTitlebar .spSubtitle{ display:none; }
  .spApp{ grid-template-columns:1fr; grid-template-rows:auto auto 1fr auto auto; grid-template-areas:"menu" "tools" "sheet" "palette" "status"; min-height:0; }

  /* The toolbox becomes one strip across the top: every tool, then whatever
     options the chosen tool has, on the same line. Left as a grid it gave
     three brush sizes a third of the screen each. */
  .spTools{ display:flex; flex-wrap:wrap; align-items:center; gap:4px; padding:6px; border-right:0; border-bottom:2px solid var(--ink); }
  .spTool{ flex:0 0 30px; width:30px; }   /* ten across one row on a 375px screen */
  .spTool svg{ width:19px; height:19px; }
  .spSizes, .spStamps{ display:flex; flex-direction:row; justify-content:flex-start; flex:1 1 100%; gap:4px; margin:0; }
  /* On the strip the zoom buttons sit inline with the tools, not on a rule of
     their own -- there is no column for a rule to run across. */
  .spZoomBtns{ flex:0 0 auto; margin:0; padding:0; border-top:0; }
  .spZoomBtn{ flex:0 0 34px; width:34px; height:30px; }
  .spSize{ flex:0 0 30px; width:30px; }
  .spStamps .spStamp{ width:auto; flex:1; }
  .spFillTog{ flex:0 0 auto; margin:0; padding:3px 12px; }

  .spStage{ padding:6px; min-height:0; }

  /* Sixteen colours in two rows of eight, wide enough for a thumb. Sixteen in
     ONE row made every swatch a sliver. */
  .spPalette{ padding:6px 8px; }
  .spCurrent{ width:34px; height:34px; flex:0 0 34px; }
  .spSwatches{ flex:1; grid-template-columns:repeat(8, 1fr); gap:4px; }
  .spSwatch{ width:auto; height:26px; }

  .spStatus{ flex-wrap:wrap; gap:4px 14px; }
  .spDialog, .spConfirm{ min-width:0; width:94vw; }
}
/* ==== end SPLOTCH ==== */


  /* SPLOTCH IN A WINDOW.
     The stylesheet above is the program's own, copied verbatim from
     tools/splotch/src/style.css and checked for drift by its build -- so it
     must not be edited here. These are the few rules that adapt a page to a
     window: the standalone version sizes its sheet against the browser
     viewport and stands 520px tall whatever happens, neither of which is true
     inside a window somebody can resize. */
  /* A HEIGHT, not just a width, and for the same reason DocuMax has one: the
     drawing area is sized to the room it is given, so the window has to know
     how tall it is before the sheet can be fitted into it. Left to grow with
     its contents the two would chase each other -- a bigger sheet making a
     taller window making a bigger sheet. Maximizing replaces this outright
     with the full screen, which is what makes the sheet grow with it. */
  /* SPLOTCH'S WINDOW, sized against the room the game actually has.
     Measured on 7 September 2026, because every number here was guessed the
     first time and every one of them was wrong. The game area a window lives
     in is about 700 x 720 at a 1440 x 900 browser and 913 x 880 at 1920 x
     1080 -- it is a portrait cabinet, not the whole screen.

     max-width:none is the important line. .panelBox caps every panel at about
     30 characters of the game's own text size, which is right for a dialog
     full of sentences and wrong for a drawing: it was holding this window at
     462px however much room was going spare, which is why the drawing area
     came out SMALLER than the flat 420px it used to be.

     88% and 84%, not the whole thing: a default window that already filled
     the game area would leave the maximize button with nothing to do. At 1440
     x 900 this opens with a 499px sheet and maximizes to 583px, so pressing
     the square is worth pressing. */
  .splotchBox{ width:min(88%, 900px); height:min(84%, 760px); max-width:none; }
  .splotchBody{ padding:0; display:flex; min-height:0; }
  .splotchBody .spApp{ min-height:0; flex:1; background:none; }
  .splotchBody .spStage{ min-height:0; padding:8px; }
  /* At the narrow end the toolbox goes to a strip, the same as on a phone --
     the program already knows how to do this, it just measures the page rather
     than the window it is in. */
  .cabinet.glassNarrow .splotchBody .spApp{ grid-template-columns:1fr; grid-template-rows:auto auto 1fr auto auto; grid-template-areas:"menu" "tools" "sheet" "palette" "status"; }
  .cabinet.glassNarrow .splotchBody .spTools{ display:flex; flex-wrap:wrap; align-items:center; gap:4px; border-right:0; border-bottom:2px solid var(--ink); }
  .cabinet.glassNarrow .splotchBody .spTool{ flex:0 0 30px; width:30px; }
  .cabinet.glassNarrow .splotchBody .spSizes, .cabinet.glassNarrow .splotchBody .spStamps{ flex-direction:row; justify-content:flex-start; flex:1 1 100%; }
  .cabinet.glassNarrow .splotchBody .spSwatches{ grid-template-columns:repeat(8, 1fr); }


/* ==== DOCUMAX: copied from tools/documax/src/style.css ==== */
/* ============================================================================
   DOCUMAX — THE PROGRAM'S OWN LOOK
   ----------------------------------------------------------------------------
   What Corporate would have shipped if they had licensed the cheapest thing
   available. A cream page on a grey work surface, one short row of buttons
   with letters on them, a status bar that counts. No ribbon, no icons, no
   colour anywhere except the gold that means "you have hit a limit".

   Everything is scoped under .dx so that when this file is folded into
   game.css it cannot reach out and restyle the office.
   ========================================================================= */

.dx, .dx *{ box-sizing:border-box; }

.dx{
  --panel:#23241d; --panel-light:#33352a; --panel-dark:#1a1b15;
  --text:#f4efe0; --text-dim:#b9b39a; --ink:#201d16;
  --gold:#e0b23a; --good:#5c9a5f; --danger:#c23b3b;
  --line:#4a4c3c; --paper:#f4efe0;
  --ui:15px;
  font-family:'VT323','Courier New',monospace;
  color:var(--text);
  /* A phone must never resize this program's type on its own. The paper is
     measured off the text that is on it, so a browser quietly enlarging the
     writing would change where the pages end. */
  -webkit-text-size-adjust:100%; text-size-adjust:100%;
}
.dx button{ font-family:inherit; cursor:pointer; touch-action:manipulation; color:inherit; }
.dx :focus-visible{ outline:3px solid var(--gold); outline-offset:2px; }
/* Except the page: a document you are typing in is always focused, and a gold
   ring permanently around it reads as a selection. The caret is the indicator. */
.dxPage:focus, .dxPage:focus-visible{ outline:none; }

/* ---------------------------------------------------------------- the window
   A stand-in for the SYNERGY window this program will live in; thrown away
   when DocuMax moves into the game. */
/* Fixed to the viewport rather than tall inside it, so the desk is exactly
   the height of the screen whatever margin the page around it has -- and the
   program inside can be a column that ends at the bottom of the window, with
   the document scrolling and the chrome staying still. */
.dxDesk{
  position:fixed; inset:0; overflow:auto; padding:18px;
  background:#111008;
  background-image: radial-gradient(ellipse at 50% 0%, #1c1a12 0%, #0d0c08 70%);
  display:flex; align-items:stretch; justify-content:center;
}
.dxWindow{ width:100%; max-width:880px; max-height:100%; min-height:min(340px, 100%); display:flex; flex-direction:column; background:var(--panel); border:4px solid var(--ink); box-shadow:0 0 0 2px #000, 10px 10px 0 rgba(0,0,0,0.5); }
.dxTitlebar{ display:flex; align-items:center; gap:12px; padding:8px 12px; background:linear-gradient(180deg,#3a3c30,#282a20); border-bottom:3px solid var(--ink); }
.dxTitlebar .dxMark{ width:26px; height:26px; flex:0 0 26px; display:grid; place-items:center; background:var(--gold); color:var(--ink); font-family:'Press Start 2P','VT323',monospace; font-size:10px; border:2px solid var(--ink); }
.dxTitlebar h1{ margin:0; font-family:'Press Start 2P','VT323',monospace; font-size:12px; letter-spacing:1px; color:var(--text); }
.dxTitlebar .dxSubtitle{ font-size:var(--ui); color:var(--text-dim); margin-left:2px; }
.dxTitlebar .dxSpacer{ flex:1; }
.dxWinBtns{ display:flex; gap:5px; }
.dxWinBtns span{ width:15px; height:15px; border:2px solid var(--ink); background:#5a5c4a; display:block; }
.dxTitlebar{ flex:0 0 auto; }
.dxBody{ padding:0; flex:1 1 auto; min-height:0; display:flex; }

/* --------------------------------------------------------------- the program
   The chrome does not move. The menu bar, the toolbar and the status line sit
   still and the DOCUMENT scrolls between them -- which is how every word
   processor since the first one has behaved, and is the difference between a
   program and a very long web page. That means the app is a column of fixed
   height with one part that scrolls, so whatever container it is mounted in
   -- this file's stand-in window now, a SYNERGY window later -- has to give it
   a height. */
.dxApp{ display:flex; flex-direction:column; flex:1 1 auto; min-height:0; max-height:100%; background:var(--panel-dark); overflow:hidden; }

.dxMenuBar{ display:flex; gap:2px; padding:3px 6px; background:var(--panel-light); border-bottom:2px solid var(--ink); position:relative; flex:0 0 auto; z-index:30; }
.dxMenu{ background:none; border:2px solid transparent; padding:2px 10px; font-size:calc(var(--ui) + 3px); }
.dxMenu[aria-expanded="true"], .dxMenu:hover{ background:var(--panel); border-color:var(--ink); }
.dxDrop{ position:absolute; top:100%; left:0; min-width:230px; background:var(--panel); border:3px solid var(--ink); box-shadow:5px 5px 0 rgba(0,0,0,.5); padding:4px; z-index:20; display:flex; flex-direction:column; }
/* A list of values is a short list. The menus are wide because their items
   are sentences; "Single", "1.5" and "12" are not. */
.dxDrop.dxDropPick{ min-width:88px; }
.dxMenuItem{ display:flex; justify-content:space-between; gap:14px; text-align:left; background:none; border:2px solid transparent; padding:4px 10px; font-size:calc(var(--ui) + 3px); }
.dxMenuItem:hover:not(:disabled){ background:var(--panel-light); border-color:var(--ink); }
.dxMenuItem:disabled{ color:var(--text-dim); opacity:.7; cursor:default; }
.dxMenuItem .dxNote{ font-size:var(--ui); color:var(--gold); }
.dxMenuItem .dxKey{ font-size:var(--ui); color:var(--text-dim); }
.dxSep{ height:2px; background:var(--line); margin:4px 2px; }

/* the toolbar: letters, not pictures */
.dxToolbar{ position:relative; display:flex; align-items:center; gap:4px; padding:5px 8px; background:var(--panel); border-bottom:2px solid var(--ink); flex-wrap:wrap; flex:0 0 auto; z-index:10; }
.dxTool{ min-width:28px; height:28px; padding:0 6px; background:var(--panel-light); border:2px solid var(--ink); box-shadow:2px 2px 0 rgba(0,0,0,.45); font-size:calc(var(--ui) + 2px); }
.dxTool[aria-pressed="true"]{ background:var(--gold); color:var(--ink); box-shadow:inset 2px 2px 0 rgba(0,0,0,.3); }
.dxTool:hover:not(:disabled){ filter:brightness(1.12); }
.dxTool:disabled{ opacity:.42; cursor:default; box-shadow:none; }
.dxTool.dxB{ font-weight:700; } .dxTool.dxI{ font-style:italic; } .dxTool.dxU{ text-decoration:underline; } .dxTool.dxK{ text-decoration:line-through; }
.dxToolSep{ width:2px; height:22px; background:var(--line); margin:0 3px; }
/* Size and spacing sit in a list rather than on a button, the way a word
   processor has always put them -- but it is the program's own list, in the
   same box as everything else, because the browser's own would take the
   focus and lose the words you had selected. */
.dxPickBtn{ display:inline-flex; align-items:center; gap:4px; min-width:44px; padding:0 5px; }
.dxPickBtn[aria-expanded="true"]{ background:var(--gold); color:var(--ink); }
.dxPickArrow{ font-size:calc(var(--ui) - 3px); line-height:1; flex:0 0 auto; }
/* The font box shows the face you are standing in, drawn in that face --
   which is how every word processor has named a font since they had names. */
/* A fixed width, not a fitted one: a box that grows with the name would
   reflow the whole toolbar every time the typeface changed. */
.dxPickBtn[data-pick="font"]{ width:120px; min-width:120px; max-width:120px; }
/* Courier Prime is the longest name in the widest face; it is set a shade
   smaller in the box so the box never has to change size. */
.dxPickBtn[data-pick="font"][data-face="b"] .dxPickVal{ font-size:.68em; }
.dxPickBtn[data-pick="font"] .dxPickVal{ flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
.dxDrop.dxDropFont{ min-width:190px; }
.dxDropFont .dxMenuItem{ font-size:calc(var(--ui) + 4px); }
.dxDropFont .dxMenuItem .dxNote{ font-size:calc(var(--ui) - 1px); font-family:'VT323','Courier New',monospace; }
/* The list, and the box, in the actual faces. */
.dxFace-a, .dxPickBtn[data-face="a"] .dxPickVal{ font-family:'VT323','Courier New',monospace; }
.dxFace-b, .dxPickBtn[data-face="b"] .dxPickVal{ font-family:'Courier Prime','Courier New',monospace; font-size:.82em; }
.dxFace-g, .dxPickBtn[data-face="g"] .dxPickVal{ font-family:'Arimo','Helvetica Neue',Helvetica,Arial,sans-serif; font-size:.82em; }
.dxFace-h, .dxPickBtn[data-face="h"] .dxPickVal{ font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif; font-size:.8em; }
.dxFace-d, .dxPickBtn[data-face="d"] .dxPickVal{ font-family:'EB Garamond',Georgia,serif; font-size:.94em; }
.dxFace-e, .dxPickBtn[data-face="e"] .dxPickVal{ font-family:'Comic Neue','Comic Sans MS',cursive; font-size:.86em; }
.dxFace-f, .dxPickBtn[data-face="f"] .dxPickVal{ font-family:'Patrick Hand','Bradley Hand',cursive; font-size:.92em; }

/* ---------------------------------------------------------------- glyphs
   A button with a letter on it tells you what it does only once you have been
   told. These are the shapes every word processor has used for the list,
   alignment and indent buttons since the nineties -- a marker and some lines
   standing in for writing -- drawn out of plain rectangles in the current
   text colour, so they invert with the button when it is pressed and cost
   nothing to load. */
.dxIco{ display:inline-flex; flex-direction:column; justify-content:center; gap:2px; width:16px; height:15px; vertical-align:middle; position:relative; }
.dxIco i{ display:block; position:relative; height:3px; }
.dxIco i::after{ content:''; position:absolute; right:0; top:.5px; height:2px; background:currentColor; }
/* bulleted and numbered lists: a marker, then the writing beside it */
.dxIcoUl i::after, .dxIcoOl i::after{ left:7px; }
.dxIcoUl i::before{ content:''; position:absolute; left:0; top:0; width:3px; height:3px; background:currentColor; }
.dxIcoOl i::before{ position:absolute; left:0; top:-2px; font-family:'Courier Prime','Courier New',monospace; font-size:7px; line-height:1; font-weight:700; }
.dxIcoOl i:nth-child(1)::before{ content:'1'; }
.dxIcoOl i:nth-child(2)::before{ content:'2'; }
.dxIcoOl i:nth-child(3)::before{ content:'3'; }
/* alignment: four lines of writing, ragged on the side they are ragged on */
.dxIcoAl{ gap:2px; }
.dxIcoAl i{ height:2px; background:currentColor; width:100%; }
.dxIcoAl i::after{ display:none; }
.dxIcoAl.dxAlL i:nth-child(even){ width:62%; }
.dxIcoAl.dxAlR i:nth-child(even){ width:62%; margin-left:auto; }
.dxIcoAl.dxAlC i:nth-child(even){ width:62%; margin-left:auto; margin-right:auto; }
/* indent: the middle lines stepped over, with an arrow showing which way */
.dxIcoIn i{ height:2px; background:currentColor; width:100%; }
.dxIcoIn i::after{ display:none; }
.dxIcoIn i:nth-child(2), .dxIcoIn i:nth-child(3){ width:58%; margin-left:auto; }
.dxIcoIn::before{
  content:''; position:absolute; left:0; top:50%; margin-top:-3px;
  border-top:3px solid transparent; border-bottom:3px solid transparent;
}
.dxIcoIn.dxInR::before{ border-left:5px solid currentColor; }
.dxIcoIn.dxInL::before{ border-right:5px solid currentColor; left:1px; }

/* --------------------------------------------------------------- find bar
   One row under the toolbar, in the same panel colours: Corporate would not
   have paid for a floating palette. It is hidden until asked for, so the
   writing surface is not one line shorter for the ninety-nine per cent of the
   time nobody is looking for anything. */
.dxFind{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; padding:5px 8px; background:var(--panel-light); border-bottom:2px solid var(--ink); flex:0 0 auto; }
.dxFind[hidden]{ display:none; }
.dxFindLbl{ font-size:var(--ui); color:var(--text-dim); }
/* Sixteen at every width, not only on a narrow one. A phone turned sideways is
   over six hundred pixels wide and still a phone: below sixteen it zooms the
   program in the moment the box is tapped, and it does not zoom back out. */
.dxFind input{ font:inherit; font-size:16px; width:150px; min-width:0; flex:0 1 150px; padding:3px 6px; background:var(--paper); color:var(--ink); border:2px solid var(--ink); }
.dxFindBtn{ padding:2px 9px; font-size:var(--ui); }
.dxFindCount{ font-size:var(--ui); color:var(--gold); min-width:66px; }
.dxFind .dxSpacer{ flex:1 1 auto; }

/* ------------------------------------------------------------- the paper
   A sheet of US Letter, eight and a half by eleven, with an inch of margin all
   round, sitting on a dark desk. Every measurement below comes from ONE number
   -- how many pixels DocuMax is calling an inch -- which the program works out
   from the width it has been given and writes in as --dxpw and friends. That
   is why the sheet keeps its proportions on a phone, and why turning the paper
   sideways is a change to one variable rather than a second stylesheet.

   The sheets themselves are drawn in a layer BEHIND the text. The typing
   surface is one continuous transparent column on top of them, which is the
   only arrangement in which the caret can never get stuck at the bottom of a
   page: as far as the browser is concerned there is only ever one document. */
.dxSurface{ flex:1 1 auto; min-height:0; overflow:auto; padding:18px 14px 30px; background:#3b3931; background-image:repeating-linear-gradient(45deg, rgba(0,0,0,.07) 0 6px, transparent 6px 12px); }
.dxStack{ position:relative; width:var(--dxpw, 620px); margin:0 auto; }
.dxPapers{ position:absolute; inset:0; pointer-events:none; }
.dxPaper{ position:absolute; left:0; width:100%; height:var(--dxph, 800px); background:var(--paper); box-shadow:0 0 0 2px var(--ink), 6px 6px 0 rgba(0,0,0,.45); }
/* The number of the sheet below, small, in the strip of desk between them. */
.dxPaperNo{ position:absolute; left:1px; font-family:'Press Start 2P','VT323',monospace; font-size:9px; line-height:1; color:rgba(244,239,224,.62); }

.dxPage{
  position:relative; z-index:1;
  width:100%;
  background:transparent; color:var(--ink);
  padding:var(--dxpm, 56px);
  font-family:'Courier Prime','Courier New',monospace; line-height:1.18;
  outline:none; overflow-wrap:break-word;
  /* Real spaces, kept: two spaces after a full stop stay two spaces. */
  white-space:pre-wrap;
  tab-size:8; -moz-tab-size:8;
  /* SIXTEEN PIXELS, AND IT IS NOT A TASTE.
     This div is the thing a phone puts the caret in, and an iPhone decides how
     far to zoom the WHOLE screen in the moment it is tapped by dividing
     sixteen by the type size of exactly this element -- nothing else, not the
     line the caret is on, not what the writing looks like after any scaling.
     At the twelve pixels the paper used to give it, tapping into a document
     magnified the entire program to 133% and left it there: the writer could
     no longer see the edges of the page, and panning about showed the desktop
     behind the window. That is the whole of the bug the owner hit on his
     phone.
     So the ELEMENT is sixteen and every line of writing inside it is given the
     paper's own size back on the line below. Nothing on screen moves; the
     number the phone reads is the only thing that changes. */
  font-size:16px;
}
.dxPage > *, .dxPage ul, .dxPage ol{ font-size:var(--dxfs, 14px); }
/* The spacers that make a page end.
   A full-width float carrying a shape that is empty for the height of one
   page and solid for the height of the gap plus the two margins around it.
   Line boxes step over the solid part and land at the top of the next sheet,
   which means a paragraph splits across pages LINE BY LINE the way a word
   processor splits it -- rather than being thrown whole onto the next page.
   It floats RIGHT rather than left, which matters more than it sounds: a left
   float pushes a list's bullet out to the far side of the paper, because the
   number is placed against the float's box while the words are placed against
   its shape. Floated the other way, the bullets stay where bullets go.
   There is no text in these, the model never sees them, and they are rebuilt
   from scratch on every layout, so nothing can be lost inside one. */
.dxGap{ float:right; width:100%; clear:both; pointer-events:none; -webkit-user-select:none; user-select:none; }

.dxPage p{ margin:0 0 .35em; }
/* A heading is given room above it, the way a heading always is -- except the
   first line of the document, which sits on the top margin where it belongs. */
.dxPage > :first-child{ margin-top:0; }
/* The heading multipliers are against --dxfs, which changed when the standard
   face did, so they were multiplied through to keep headings the size they
   have always been. */
.dxPage h2{ margin:.9em 0 .58em; font-family:'Press Start 2P','VT323',monospace; font-size:calc(var(--dxfs, 14px) * 1.218); line-height:1.5; letter-spacing:.5px; }
.dxPage h3{ margin:.9em 0 .51em; font-family:'Press Start 2P','VT323',monospace; font-size:calc(var(--dxfs, 14px) * .897); line-height:1.6; letter-spacing:.5px; }
.dxPage ul, .dxPage ol{ margin:0 0 .35em; padding-left:calc(var(--dxfs, 14px) * 2.18); }
.dxPage ul ul, .dxPage ol ol, .dxPage ul ol, .dxPage ol ul{ margin-bottom:0; }
.dxPage ul{ list-style:disc; } .dxPage ul ul{ list-style:circle; } .dxPage ul ul ul{ list-style:square; }
.dxPage ol{ list-style:decimal; } .dxPage ol ol{ list-style:lower-alpha; } .dxPage ol ol ol{ list-style:lower-roman; }
.dxPage li{ margin:0 0 .1em; }
/* ------------------------------------------------------------ the faces
   Five of them, all inlined in the file itself so they draw with no network.
   Each carries an optical scale as well as a family: set at the same pixel
   size, VT323's letters are much smaller on the em than Garamond's, so
   without this, changing the typeface would silently change how big the
   writing looks. The scale is a VARIABLE rather than a size, so that a point
   size chosen on top of a face still composes -- eighteen point Garamond is
   eighteen point.

   **The scale of ONE is Courier Prime**, because Courier Prime is what a new
   document is written in, and --dxfs therefore means the size of the writing
   you actually get. When the standard face changed the whole table was
   divided through by the old Courier figure and --dxfs was multiplied by it,
   so every face still draws at exactly the pixel size it drew at before:
   nothing anybody has already written moved. */
.dxPage{ --dxface:1; }
.dxPage .face-a{ --dxface:1.282; font-family:'VT323','Courier New',monospace; font-size:calc(var(--dxfs, 14px) * 1.282); }
.dxPage .face-b{ --dxface:1;     font-family:'Courier Prime','Courier New',monospace; font-size:calc(var(--dxfs, 14px) * 1); }
/* Arimo. The scale was measured rather than judged: at .9 its lowercase is
   6.2px against the page's 13px, which is the middle of the range the other
   five sit in (5.9 to 6.7), so switching to it does not change how big the
   writing looks. Being a narrow, neutral grotesque it fits far more on a line
   than the typewriter faces do -- that is what a proportional face does, and
   the page count follows the writing rather than the other way round. */
.dxPage .face-g{ --dxface:.9;   font-family:'Arimo','Helvetica Neue',Helvetica,Arial,sans-serif; font-size:calc(var(--dxfs, 14px) * .9); }
/* Open Sans, scaled the same way and to the same lowercase height as Arimo,
   which it sits beside. The two are not a duplication: Open Sans is a good
   deal wider, so the same paragraph in it takes noticeably more room. */
.dxPage .face-h{ --dxface:.89;   font-family:'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif; font-size:calc(var(--dxfs, 14px) * .89); }
.dxPage .face-d{ --dxface:1.128; font-family:'EB Garamond',Georgia,'Times New Roman',serif; font-size:calc(var(--dxfs, 14px) * 1.128); }
.dxPage .face-e{ --dxface:1.026; font-family:'Comic Neue','Comic Sans MS',cursive; font-size:calc(var(--dxfs, 14px) * 1.026); }
.dxPage .face-f{ --dxface:1.103; font-family:'Patrick Hand','Bradley Hand',cursive; font-size:calc(var(--dxfs, 14px) * 1.103); }
/* Bold in a typewriter face is barely a difference, so it is helped along. */
.dxPage b, .dxPage strong{ font-weight:700; text-shadow:0.4px 0 0 currentColor; }
.dxPage .al-c{ text-align:center; }
.dxPage .al-r{ text-align:right; }
.dxPage .al-j{ text-align:justify; }
/* Line spacing, as a word processor names it: single is the page's own
   leading, and the other two are multiples of it. */
.dxPage .ls-15{ line-height:1.77; }
.dxPage .ls-2{ line-height:2.36; }
/* Indent: half an inch a step, measured in the same inch as the margins. */
.dxPage .in-1{ margin-left:calc(var(--dxpm, 56px) * .5); }
.dxPage .in-2{ margin-left:calc(var(--dxpm, 56px) * 1); }
.dxPage .in-3{ margin-left:calc(var(--dxpm, 56px) * 1.5); }
.dxPage .in-4{ margin-left:calc(var(--dxpm, 56px) * 2); }
.dxPage .in-5{ margin-left:calc(var(--dxpm, 56px) * 2.5); }
/* Point sizes, absolute rather than relative, so that a size inside a size
   inside a typeface is still the size it says on the button. */
.dxPage .sz-10{ font-size:calc(var(--dxfs, 14px) * .84 * var(--dxface, 1)); }
.dxPage .sz-12{ font-size:calc(var(--dxfs, 14px) * 1 * var(--dxface, 1)); }
.dxPage .sz-14{ font-size:calc(var(--dxfs, 14px) * 1.17 * var(--dxface, 1)); }
.dxPage .sz-18{ font-size:calc(var(--dxfs, 14px) * 1.5 * var(--dxface, 1)); }
.dxPage .sz-24{ font-size:calc(var(--dxfs, 14px) * 2 * var(--dxface, 1)); }

/* status */
.dxStatus{ display:flex; align-items:center; gap:18px; padding:4px 10px; font-size:var(--ui); color:var(--text-dim); background:var(--panel-light); border-top:2px solid var(--ink); }
.dxStatus .dxSpacer{ flex:1; }
.dxStatus .dxFileName{ color:var(--text); }
.dxStatus .dxPaperSize{ color:var(--text-dim); }
/* Every item is one unbroken run. Without this a squeezed status bar breaks
   "Letter Portrait" over two lines inside its own box, which reads as the bar
   being broken rather than being narrow. The exception is the sentence
   Corporate prints when typing stops: that is a sentence, and it may wrap. */
.dxStatus > span{ white-space:nowrap; }
/* An item with nothing in it -- the selection count with nothing selected, the
   limit notice on a document nowhere near it -- takes no room and leaves no
   gap. Without this the bar is full of holes where a message might one day be.
   The spacer is exempt: being empty is its whole job. */
.dxStatus > span:empty:not(.dxSpacer){ display:none; }
.dxStatus .dxLimit{ color:var(--gold); white-space:normal; }

/* dialogs -- in-page, never window.confirm */
.dxScrim{ position:fixed; inset:0; background:rgba(0,0,0,.55); display:grid; place-items:center; z-index:900; }
.dxDialog, .dxConfirm{ background:var(--panel); border:3px solid var(--ink); box-shadow:8px 8px 0 rgba(0,0,0,.5); padding:14px 16px; min-width:min(92vw, 380px); max-width:92vw; font-size:calc(var(--ui) + 3px); }
.dxDialog h2, .dxConfirm h2{ margin:0 0 10px; font-family:'Press Start 2P','VT323',monospace; font-size:11px; letter-spacing:.5px; color:var(--gold); }
.dxDialog input{ width:100%; font:inherit; font-size:calc(var(--ui) + 4px); padding:5px 8px; background:var(--paper); color:var(--ink); border:2px solid var(--ink); }
.dxDialog .dxHint{ font-size:var(--ui); color:var(--text-dim); margin:6px 0 0; min-height:1.2em; }
.dxDialog .dxNote{ font-size:var(--ui); color:var(--gold); margin:0 0 8px; }
.dxBtns{ display:flex; justify-content:flex-end; gap:8px; margin-top:12px; }
.dxChoice{ display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; }
.dxBtn{ background:var(--panel-light); border:2px solid var(--ink); box-shadow:2px 2px 0 rgba(0,0,0,.45); padding:4px 14px; font-size:calc(var(--ui) + 2px); }
.dxBtn.dxPrimary{ background:var(--gold); color:var(--ink); }
.dxBtn.dxDanger{ background:var(--danger); color:var(--text); }
.dxBtn:disabled{ opacity:.5; cursor:default; }
.dxFiles{ display:flex; flex-direction:column; gap:3px; max-height:46vh; overflow:auto; border:2px solid var(--ink); background:var(--panel-dark); padding:4px; }
.dxFile{ display:flex; align-items:center; gap:10px; padding:4px 8px; border:2px solid transparent; cursor:default; }
.dxFile[aria-selected="true"]{ background:var(--panel-light); border-color:var(--ink); }
.dxFile .dxFName{ flex:1; }
.dxFile .dxFWhen{ color:var(--text-dim); font-size:var(--ui); }
.dxDelete{ background:none; border:2px solid var(--ink); width:24px; height:22px; font-size:12px; padding:0; }
.dxEmpty{ color:var(--text-dim); padding:10px; text-align:center; }

.dxToast{ position:fixed; left:50%; bottom:26px; transform:translateX(-50%); background:var(--panel); color:var(--text); border:3px solid var(--ink); box-shadow:5px 5px 0 rgba(0,0,0,0.5); padding:11px 17px; font-size:calc(var(--ui) + 3px); z-index:1000; max-width:88vw; text-align:center; }

/* ------------------------------------------------------------------ phones
   The page fills the width and the toolbar stays above the keyboard, which is
   what sticky positioning is for: on a phone the toolbar is the only way to
   reach bold, and it must not scroll away under the text being typed. */
@media (max-width:600px){
  .dxDesk{ padding:0; }
  .dxWindow{ border-width:0; box-shadow:none; }
  .dxTitlebar .dxSubtitle{ display:none; }
  .dxApp{ min-height:0; }
  .dxToolbar{ gap:3px; padding:4px 6px; }
  /* Taller than it needs to be to READ, because it has to be hit with a
     thumb. Two rows of 32 is worth more than three rows of 26. */
  .dxTool{ min-width:24px; height:32px; padding:0 3px; font-size:var(--ui); }
  .dxPickBtn{ min-width:38px; gap:2px; }
  .dxPickBtn[data-pick="font"]{ width:88px; min-width:88px; max-width:88px; }
  /* The font list is tapped, so its rows are thumb-sized rather than tidy. */
  .dxDrop.dxDropFont{ min-width:0; }
  .dxDropFont .dxMenuItem{ padding:7px 10px; }
  .dxDropPick .dxMenuItem{ padding:7px 10px; }
  /* Two rows, not three. The groups are still marked, just hairline. */
  .dxToolSep{ width:1px; margin:0; height:26px; }
  .dxSurface{ padding:8px 5px 20px; }
  .dxPickBtn{ min-width:40px; }
  .dxFind{ gap:4px 5px; padding:5px 6px; }
  /* The boxes fill the row and are tall enough to hit. Their type size is set
     once, further up, at every width -- see the note there: below sixteen an
     iPhone zooms the whole program in the moment a box is tapped, and a phone
     turned sideways never sees this media query at all. */
  .dxFind input{ flex:1 1 90px; width:auto; height:32px; }
  .dxFindBtn{ height:32px; padding:0 10px; }
  .dxFind .dxSpacer{ display:none; }
  /* Two rows, not three: the labels are in the boxes themselves. */
  .dxFindLbl{ display:none; }
  .dxFindCount{ min-width:0; }
  /* THE STATUS BAR ON A PHONE.
     It may wrap -- there is no width at which all seven items fit -- but every
     line it takes is a line of paper the writer does not get, in a window that
     is only about 340px tall to begin with. So the items are ranked, and the
     ones that are not telling the writer anything step aside.
     First out is the product name: no real word processor prints its own
     brand in the status bar, and the title bar above already says DOCUMAX.
     The spacer goes with it, so what is left packs to the left in one run
     instead of being flung to both edges and stranding an item on line two. */
  .dxStatus{ flex-wrap:wrap; gap:4px 12px; }
  .dxStatus .dxTagline{ display:none; }
  .dxStatus .dxSpacer{ display:none; }
  .dxDialog, .dxConfirm{ min-width:0; width:94vw; }
  /* A question you answer with a thumb. Save / Discard / Cancel are the three
     buttons a writer presses in a hurry, on the way out of the program, and
     twenty-eight pixels is a mouse's target. The find bar's buttons are left
     at their own height on purpose: it is a row, not a question, and 44 there
     would cost a line of paper. */
  .dxBtns .dxBtn, .dxChoice .dxBtn{ min-height:44px; }
  .dxBtns{ flex-wrap:wrap; }
}
/* Below this the bar has room for about three items, and these are the two
   that normally sit there saying the same thing all day. They come back the
   moment they are not saying the same thing all day -- see dxAlt in ui.js:
   paper that has been turned sideways stays on screen, and so does the count
   once a document is inside the last tenth of the ceiling, which is the only
   point at which it is a warning rather than a decoration. What is left is
   what actually moves as somebody writes: words, page, and the file name with
   its unsaved star. */
@media (max-width:430px){
  .dxStatus .dxPaperSize:not(.dxAlt){ display:none; }
}
@media (max-width:360px){
  .dxStatus .dxChars:not(.dxAlt){ display:none; }
}
/* ==== end DOCUMAX ==== */

  /* DOCUMAX IN A WINDOW.
     As with Splotch: everything above is the program's own stylesheet, copied
     verbatim and drift-checked by tools/documax/build.mjs --game, so it must
     not be edited here. These are the rules that turn a page into a window.

     The standalone version owns the whole browser and sizes its writing
     surface against the viewport. In a window it has to fill whatever the
     player has dragged the frame to instead, which is what min-height:0 on
     each nesting level buys -- without it a flex child refuses to shrink below
     its content and the paper pushes the bottom of the window off the glass. */
  /* max-width:none is load-bearing, not tidiness. .panelBox caps every panel
     at about thirty characters of the game's own text size -- right for a
     dialog full of sentences, wrong for a program with a page in it. Without
     this the window computed to 462px however much room the game area had
     going spare, so the width and the height above were both fiction: they
     were written, never took effect, and nobody noticed because a word
     processor degrades quietly. It just gave you a narrower page.

     Found by the session wiring DeckGen in, from the same trap Splotch was
     dug out of an hour earlier -- see .splotchBox. Measured after: 616px at a
     1440x900 browser against 462 before.

     88% and 84% rather than the 96% and 88% that were sitting here, matching
     Splotch for the reason Splotch has them: a default window that already
     fills the game area leaves the maximize button with nothing to do. */
  .documaxBox{ width:min(88%, 720px); height:min(84%, 620px); max-width:none; }
  .documaxBody{ padding:0; display:flex; min-height:0; }
  .documaxBody .dx{ min-height:0; flex:1; background:none; }
  .documaxBody .dxSurface{ min-height:0; }
  /* The program draws its own title bar for the standalone file. In here the
     window manager has already drawn one, and two stacked title bars is the
     clearest possible way to say nobody looked at this. */
  .documaxBody .dxTitlebar{ display:none; }
  /* At the narrow end the toolbar wraps rather than scrolling sideways: a
     hidden formatting button is a feature the player never finds. */
  .cabinet.glassNarrow .documaxBody .dxToolbar{ flex-wrap:wrap; row-gap:3px; }
  /* WHILE THE KEYBOARD IS UP, the paper is what matters. Three or four wrapped
     rows of buttons plus the keyboard left about 28px of writing surface on a
     320px phone; the row goes back to one that scrolls sideways for as long as
     somebody is typing, and wraps again the moment the keyboard is put away.
     Nothing is hidden for good, which is the reason it wraps in the first place. */
  body.phoneLayout.keyboardUp .cabinet.glassNarrow .documaxBody .dxToolbar{
    flex-wrap:nowrap; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch;
  }
  /* And every button on it is a thumb's width: they measured 24 pixels on a
     phone, which is a mouse pointer's target, not a finger's. */
  .cabinet.glassNarrow .documaxBody .dxTool{ min-width:40px; height:40px; }
  .cabinet.glassNarrow .documaxBody .dxToolSep{ display:none; }

/* ==== DECKGEN ==== */
/* max-width:none is load-bearing, not tidiness. .panelBox caps every panel in
   the game at about thirty characters of the game's own text size (game.css
   :910, max-width:calc(var(--ui) * 30.5853)), which is right for a dialog full
   of sentences and wrong for anything holding a canvas -- it pinned Splotch's
   window to 462px however much room the cabinet had spare, and made the drawing
   area smaller than the fixed size it replaced. A 4:3 slide hits this
   identically. Same numbers as .splotchBox, which was measured against the
   cabinet rather than guessed.

   Do not design around a fixed slide size. The game area a window lives in is
   roughly 700 x 720 at a 1440 x 900 browser and 913 x 880 at 1920 x 1080 -- a
   portrait cabinet, not the screen. Showing a 768px-wide 4:3 slide at 1:1
   needs about 886px of window once a rail and padding are counted, so a
   laptop cannot reach 1:1 and must not be made to look broken for failing to.
   DeckGen lays itself out against its own container precisely so this is a
   non-issue -- do not reintroduce a fixed width here. */
  .deckgenBox{ width:min(88%, 900px); height:min(84%, 760px); max-width:none; }
  .deckgenBody{ padding:0; display:flex; min-height:0; }
  .deckgenBody > .dg{ flex:1; min-height:0; }
/* ==== end DECKGEN ==== */

/* ==== CONTACTS ====
   play/contacts.css draws the program; this is only the window it lives in,
   sized like the owner's mockups (about 560 x 640 on a desktop) and allowed to
   shrink. Its heading row is the same as every other program's. */
  .contactsBox{ width:min(80%, 560px); height:min(84%, 640px); max-width:none; }
  .contactsBody{ padding:0; display:flex; min-height:0; flex:1 1 auto; }
  .contactsBody > .ct{ flex:1; min-height:0; }
/* ==== end CONTACTS ==== */

/* ==== CO-WORKER EMAIL ====
   The pieces co-worker mail adds to Email, in Email's own look. */
  /* Compose, the left end of the folder tabs: set in the tabs' own type (the
     owner's call -- bold capitals read as shouting), and it does not stretch
     like a folder does. */
  .mailComposeBtn{ flex:0 0 auto; background:var(--panel-light); border:none; border-right:3px solid var(--ink); color:var(--text-dim);
    font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647)); padding:10px 10px; cursor:pointer; white-space:nowrap; }
  .mailComposeBtn:hover{ background:#4a4c3f; color:var(--gold); }
  .mailComposeBtn[hidden]{ display:none; }
  .cwHeadRow{ display:flex; gap:8px; margin-bottom:10px; }
  .cwComposeRow .cwSpacer{ flex:1 1 auto; }
  .cwComposeRow .replyClip.cwAttach{ font-size:max(8px, calc(var(--ui) * 0.8235)); padding:6px 10px; }
  .cwComposeRow .back.cwDiscard{ margin:0; }
  .cwNobody{ padding:16px 4px; }
  .cwNobody p{ margin:0 0 12px; font-size:max(8px, calc(var(--ui) * 1.0)); }
  .cwNobody .startbtn{ font-size:11px; padding:8px 14px; }
  .cwReplyRow{ padding-top:12px; }
  /* Co-worker email's toasts: inside the Email window, along its bottom edge
     (game.js cwToast). Wrapped rather than one long line, so a phone shows
     all of "You must wait 4:12 before emailing this co-worker again." */
  /* Centred by margins, not a transform: the toast's own fade animates transform. */
  .cwPanelToast{ position:absolute; left:12px; right:12px; margin:0 auto; z-index:7; pointer-events:none;
    width:fit-content; max-width:calc(100% - 24px); white-space:normal; text-align:center; line-height:1.25; }
  .cwCompose .cwField{ display:flex; align-items:center; gap:8px; margin:0 0 8px; }
  .cwCompose .cwFieldLbl{ flex:0 0 4.5em; color:var(--text-dim); font-size:max(8px, calc(var(--ui) * 0.8235)); }
  .cwCompose select, .cwCompose input[type="text"]{ flex:1 1 auto; min-width:0; background:var(--panel-light); border:2px solid var(--ink); color:var(--text);
    font-family:'VT323','Courier New',monospace; font-size:max(8px, calc(var(--ui) * 0.9412)); padding:4px 6px; }
  .cwWrite{ margin-top:12px; }
  .cwRefusal{ margin-top:6px; }
  .cwRefusal:empty{ display:none; }
  .cwBanner{ display:flex; gap:10px; align-items:center; background:#3a3222; border:2px solid var(--ink); border-left:6px solid var(--warn);
    color:#f3dfb2; font-size:max(8px, calc(var(--ui) * 0.9412)); padding:8px 10px; margin:6px 0 0; }
  .cwBanner svg{ flex:0 0 auto; }
  .mailDetail .back.cwReport{ display:inline-flex; align-items:center; gap:5px; }
  .cwShredded{ flex-direction:column; align-items:flex-start; }
  .mailAttachBtn.cwGone{ cursor:default; opacity:.7; }
  .cwShredLine{ color:var(--text-dim); font-size:max(8px, calc(var(--ui) * 0.8235)); margin-top:4px; }
  .cwDim{ color:var(--text-dim); }
  /* REPORT RECEIVED, over the open message. */
  .cwCardLayer{ position:absolute; inset:0; z-index:6; display:flex; align-items:center; justify-content:center; padding:12px; }
  .cwCardScrim{ position:absolute; inset:0; background:rgba(10,9,6,.55); }
  /* The same card, over the whole game rather than inside Email: "Out of room",
     asked by a program whose save the drawer refused. Above every window (they
     count up from 30) and below the folder's own Shred prompt (.docScrim). */
  .roomCardLayer{ z-index:9400; }
  .cwCard{ position:relative; width:min(420px, 100%); background:var(--panel); border:3px solid var(--ink); box-shadow:6px 6px 0 rgba(0,0,0,.5); }
  .cwCard .panelBody p{ margin:0 0 10px; font-size:max(8px, calc(var(--ui) * 1.0)); line-height:1.3; }
  .cwStack{ display:flex; flex-direction:column; align-items:stretch; gap:10px; margin-top:12px; }
  .cwStack .startbtn{ font-size:max(8px, calc(var(--ui) * 0.62)); padding:10px 12px; text-align:center; }
  .cwStack .startbtn.cwRed{ background:#b94a3f; color:#fff3e8; }
  .cwLinkBtn{ align-self:center; background:none; border:none; color:var(--text-dim); text-decoration:underline; font-family:inherit; font-size:max(8px, calc(var(--ui) * 1.0)); padding:2px; cursor:pointer; }
  /* The ? and its card -- one help card for the whole game (play/help-cards.js):
     "What are co-workers?" in Email's COMPOSE and Contacts, and the ? in the
     title band of the Employee File, the Marketplace, Support, the desktop,
     Backslash and the Assistant. The ? is quiet: the dim text colour, no
     fill, gold only when pointed at. */
  .cwNobodyRow{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
  .helpBtn{ flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; padding:0; margin:0;
    background:none; border:none; color:var(--text-dim); cursor:pointer; border-radius:50%; }
  .helpBtn svg{ width:20px; height:20px; display:block; }
  .helpBtn:hover, .helpBtn:focus-visible{ color:var(--gold); }
  .ctTb .helpBtn{ align-self:center; width:26px; height:26px; margin-left:-2px; }
  /* In a title band: right after the title, a little smaller than the band's
     own type so it reads as an aside, and pulled in so the band keeps its
     height. The title band's gap does the spacing. */
  .supportHead .helpBtn{ width:24px; height:24px; margin:-6px 0; vertical-align:middle; }
  /* Every ? is a thumb target on a phone, wherever it sits. */
  body.phoneLayout .helpBtn,
  body.phoneLayout .supportHead .helpBtn{ width:44px; height:44px; margin:0; }
  /* The Support amount's up and down arrows are stacked inside a 22px column,
     so each one is about 21px tall. They cannot grow taller without the pair
     outgrowing the button beside them, so on a phone they grow WIDER instead --
     a 44px-wide target 21 tall is one a thumb can find. */
  body.phoneLayout .stepperArrows{ flex:0 0 44px; }
  .supportHead .helpBtn svg{ width:17px; height:17px; }
  /* A thumb needs more than 24px to land on; the icon stays the same size. */
  @media (pointer: coarse), (max-width: 430px){
    .supportHead .helpBtn{ width:36px; height:36px; margin:-12px -4px; }
  }
  .solHead .headTitle{ display:inline-flex; align-items:center; gap:0.6em; min-width:0; }
  /* Never during a tour: a ? inside a window a tour card is pointing at would
     be one more thing to press that the card did not ask for. */
  body.helpOff .helpBtn{ display:none !important; }
  .helpLayer{ position:absolute; inset:0; z-index:20; display:flex; align-items:center; justify-content:center; padding:12px; }
  .helpScrim{ position:absolute; inset:0; background:rgba(10,9,6,.55); }
  .helpCard{ position:relative; display:flex; flex-direction:column; width:min(420px, 100%); max-height:100%;
    background:var(--panel); border:3px solid var(--ink); box-shadow:6px 6px 0 rgba(0,0,0,.5);
    font-family:'VT323','Courier New',monospace; color:var(--text); text-align:left; }
  .helpCard .helpHead{ flex:0 0 auto; line-height:1.6; }
  /* The six window cards are drawn over the whole game (play/help-cards.js,
     containerFor), above every window however many times one was raised. */
  .helpLayer.helpOverGame{ z-index:100000; }
  .helpBody{ flex:1 1 auto; min-height:0; overflow-y:auto; padding:14px 16px 4px; overscroll-behavior:contain; -webkit-overflow-scrolling:touch; }
  .helpBody p{ margin:0 0 10px; font-size:max(15px, calc(var(--ui) * 1.0)); line-height:1.3; }
  .helpBody .mailInlineLink{ font-family:inherit; }
  .helpBody .helpLabel{ font-weight:700; text-transform:uppercase; letter-spacing:0.5px; color:var(--gold); }
  .helpBody .helpSection{ margin-bottom:12px; }
  .helpFoot{ flex:0 0 auto; display:flex; justify-content:center; padding:8px 16px 14px; }
  /* Two classes deep, so Contacts' own ".ct button{ font-family:inherit }" does
     not turn the button's type into the card's. */
  .helpFoot .startbtn.helpOk{ font-family:'Press Start 2P',monospace; font-size:max(9px, calc(var(--ui) * 0.62)); padding:10px 22px; }
  /* A co-worker's file on the attachment viewer. */
  .cwDoc{ align-self:stretch; width:100%; overflow-y:auto; background:#fbf8f0; color:#201d16; padding:18px 22px; font-family:Georgia,'Times New Roman',serif; font-size:15px; line-height:1.5; }
  .cwDoc h2, .cwDoc h3{ margin:0 0 8px; }
  .cwDoc p{ margin:0 0 8px; }
  .cwDrawing{ display:block; width:100%; height:100%; max-height:70vh; object-fit:contain; image-rendering:pixelated; background:#f4efe0; }
  /* A photo is a photograph rather than pixel art, so it is smoothed rather
     than pixelated, and sits on the dark the office was photographed against. */
  .cwPhoto{ display:block; width:100%; height:100%; max-height:70vh; object-fit:contain; background:#14130e; }
  /* WHILE THE CAMERA IS UP the room is the only thing on screen. Every panel,
     window and the computer's own desktop go out of sight -- visibility, not
     display, so nothing is torn down and rebuilt and nothing loses its place.
     The camera's own layer is not an .overlay, and neither is the toast that
     says Bob has left the room, so both still show. */
  .cabinet.cameraShooting > .overlay.show,
  .cabinet.cameraShooting .overlay.show{ visibility:hidden; }
  /* AND ON A PHONE, the phone's own furniture goes with them: the dashboard,
     GET COFFEE and the BOB / SYNERGY / FILE tabs. On a computer the camera
     draws a grey body around the office and the game underneath is simply
     covered; here the phone IS the body, and the strip those three occupy is
     where the camera's deck goes (cameraHost().deckArea()).

     visibility, not display, and that is the whole reason this rule can work:
     their space is the measurement. Taking them out of the layout would let
     fitPhoneStage grow the room into the space they left -- it measures exactly
     these three -- and the deck would have nowhere to be, on a screen that had
     silently changed shape underneath the picture being composed.

     The top bar stays exactly as it is. Support, the sound button and the way
     back to the title menu are reachable through the camera the way they are
     through everything else. */
  body.phoneLayout .cabinet.cameraShooting .hud,
  body.phoneLayout .cabinet.cameraShooting #coffeeBtn,
  body.phoneLayout .cabinet.cameraShooting .phoneTabs{ visibility:hidden; }
  /* ZOOMING FOR REAL. On a phone the camera magnifies the room itself rather
     than drawing a box on it -- the owner, 21 September 2026, after watching
     his daughter pinch the screen: "if I zoom in 5x that's what I'm seeing on
     the work area." The camera scales #stage (see phoneZoom in camera.js), and
     a scaled room is bigger than the hole it lives in, so three things hold the
     spill:

     the cabinet clips it, so nothing leaves the screen;
     the top bar is lifted above the room, so the spill goes UNDER it and the
     bar stays lit and reachable -- Support, sound and the way out;
     and the camera's own deck, which is drawn above everything, covers the
     bottom.

     overflow rather than clip on the cabinet: clip has no support in older
     Safari, and a phone that fell back to visible would have the room sliding
     over the whole page. */
  body.phoneLayout .cabinet.cameraShooting{ overflow:hidden; }
  /* Lifted above the room AND given the ground it stands on. The bar has no
     background of its own -- it has never needed one, because the cabinet
     behind it is the same colour -- and a magnified room slid straight up
     behind the lettering. This is that same colour, said out loud, and only
     while the camera is up. */
  body.phoneLayout .cabinet.cameraShooting .titlebar{ position:relative; z-index:2; background:#111008; }
  /* The room does the moving, so it must not fight the browser for the gesture:
     a pinch is the camera's, not the page's. */
  body.phoneLayout .cabinet.cameraShooting #stage{ touch-action:none; will-change:transform; }
  .deckBar.cwOneFile .deckNav:not(.cwSave){ display:none; }
  .deckBar .cwSave{ margin-left:auto; white-space:nowrap; }
  @media (max-width: 480px){
    .deckBar.cwFile{ flex-wrap:wrap; }
    .deckBar.cwFile .cwSave{ flex:1 1 100%; margin-left:0; font-size:9px; padding:8px 10px; }
  }
/* ==== end CO-WORKER EMAIL ==== */


  /* ---------------------------------------------------------- DOCUMENTS
     The file list on Bob's computer. A window, not a desktop panel: it holds
     the player's actual work, so it keeps a tab and stays where it is put. */
  .documentsBox{ width:min(92%, 460px); }
  .documentsBody{ display:flex; flex-direction:column; gap:0; padding:0; min-height:180px; }
  .docPath{ display:flex; align-items:center; gap:3px; flex-wrap:wrap; padding:5px 8px; background:rgba(0,0,0,.22); border-bottom:1px solid var(--line, #4a473d); font-size:max(9px, calc(var(--ui) * 0.74)); }
  .docCrumb{ background:none; border:1px solid transparent; color:var(--gold, #e0b23a); padding:1px 5px; font:inherit; cursor:pointer; }
  .docCrumb:hover{ border-color:var(--gold, #e0b23a); }
  .docSep{ color:#8a8375; }
  .docSpacer{ flex:1; }
  .docNewFolder, .docDelete{ background:rgba(255,255,255,.06); border:1px solid #6a6656; color:inherit; font:inherit; padding:1px 8px; cursor:pointer; }
  .docNewFolder:hover, .docDelete:hover:not(:disabled){ background:rgba(255,255,255,.12); }
  .docDelete:disabled{ opacity:.4; cursor:default; }
  .docList{ flex:1; overflow:auto; max-height:min(46vh, 320px); }
  .docRow{ display:flex; align-items:center; gap:8px; padding:4px 8px; cursor:default; border-bottom:1px solid rgba(255,255,255,.045); }
  .docRow:hover{ background:rgba(255,255,255,.05); }
  .docRow.selected{ background:rgba(224,178,58,.18); }
  .docRow .docIcon{ width:18px; height:18px; flex:0 0 18px; display:grid; place-items:center; }
  .docRow .docIcon svg{ width:18px; height:18px; }
  /* A photo row shows the photograph itself rather than a camera: a folder of
     twenty pictures of the same office cannot be chosen from by name. The slot
     widens for it, since a photo at 18px tall is not a photo of anything. */
  .docRow .docIcon:has(.docThumb){ width:48px; height:36px; flex:0 0 48px; }
  .docRow .docThumb{ width:48px; height:36px; object-fit:cover; display:block; border:2px solid #17150f; }
  .docRow:has(.docThumb){ min-height:44px; }
  .docRow .docName{ flex:1; font-size:max(10px, calc(var(--ui) * 0.8)); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .docRow .docMeta{ color:#a49c88; font-size:max(8px, calc(var(--ui) * 0.66)); white-space:nowrap; }
  .docEmpty{ color:#a49c88; padding:18px 10px; text-align:center; font-size:max(9px, calc(var(--ui) * 0.74)); }
  .docFoot{ display:flex; align-items:center; padding:4px 8px; background:rgba(0,0,0,.22); border-top:1px solid var(--line, #4a473d); color:#a49c88; font-size:max(8px, calc(var(--ui) * 0.66)); }
  /* The trash's rows in Disposal: a file waiting for eleven o'clock. */
  .deskItemNote{ color:#a49c88; font-size:max(8px, calc(var(--ui) * 0.62)); margin-left:auto; margin-right:6px; white-space:nowrap; }
  /* FILES ON A PHONE (.docPhone, set by renderDocuments when the glass is
     narrow): a phone list, not the desktop's. Rows a thumb can hit, one tap
     opens, a Select button for several at once, and Recently Deleted at the
     bottom where a phone keeps it. */
  .docPhone .docPath{ padding:6px 10px; gap:8px; flex-wrap:nowrap; min-height:44px; font-size:14px; }
  .docPhone .docBack{ font-size:14px; padding:6px 4px; }
  .docPhone .docHere{ font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
  .docPhone .docNewFolder, .docPhone .docSelectBtn{ min-height:36px; padding:4px 12px; font-size:13px; white-space:nowrap; }
  .docSelectBtn{ background:rgba(255,255,255,.06); border:1px solid #6a6656; color:inherit; font:inherit; cursor:pointer; }
  .docPhone .docList{ max-height:none; }
  .docPhone .docRow{ min-height:48px; padding:6px 12px; gap:12px; }
  .docPhone .docRow .docIcon, .docPhone .docRow .docIcon svg{ width:26px; height:26px; flex-basis:26px; }
  .docPhone .docRow .docName{ font-size:15px; }
  .docPhone .docRow .docMeta{ font-size:12px; }
  .docPhone .docRow:hover{ background:none; }
  .docPhone .docTrashRow{ border-top:1px solid rgba(255,255,255,.12); margin-top:8px; }
  .docTick{ width:22px; height:22px; flex:0 0 22px; border:2px solid #a49c88; border-radius:50%; box-sizing:border-box; }
  .docRow.selected .docTick{ background:var(--gold, #e0b23a); border-color:var(--gold, #e0b23a); box-shadow:inset 0 0 0 3px #262419; }
  .docSelBar{ display:flex; gap:10px; padding:8px 12px; border-top:1px solid var(--line, #4a473d); background:rgba(0,0,0,.28); }
  .docSelBar button{ flex:1; min-height:44px; font:inherit; font-size:14px; color:inherit; background:rgba(255,255,255,.08); border:1px solid #6a6656; cursor:pointer; }
  .docSelBar .docBulkDelete:not(:disabled){ background:#c23b3b; border-color:#201d16; }
  .docSelBar button:disabled{ opacity:.45; cursor:default; }
  .docPhone .docFoot{ padding:8px 12px; font-size:12px; flex-wrap:wrap; gap:4px; }
  .docTrashItem .docName{ display:flex; flex-direction:column; gap:2px; white-space:normal; }
  .docTrashItem .docSub{ color:#a49c88; font-size:12px; }
  .docRestoreBtn, .docNowBtn{ min-height:40px; padding:4px 10px; font:inherit; font-size:13px; color:inherit; background:rgba(255,255,255,.08); border:1px solid #6a6656; cursor:pointer; white-space:nowrap; }
  .docNowBtn{ background:rgba(194,59,59,.85); }

  /* Its own dialogs, inside the game rather than the browser's: an alert() or
     a confirm() freezes everything, this one included. */
  .docScrim{ position:absolute; inset:0; background:rgba(0,0,0,.55); display:grid; place-items:center; z-index:9500; }
  /* ---- attaching a file to a reply ----
     The picker: which program the file came out of along the top, that
     program's files underneath, one of them chosen. Built from the Documents
     rows rather than a second list style, because it IS the Documents list --
     the player should recognise it as the same shelf they saved to. */
  /* Wide enough for Camera · DocuMax · DeckGen · Splotch · Files on ONE line.
     Every other panel in the game is capped by .panelBox's max-width, and at
     that cap the fifth button dropped onto a second row -- so the cap is what
     has to move, not the width. Kept in the same --ui units .panelBox uses, so
     it stays one line as the whole game scales, and still yields to a narrow
     screen. Named with .docConfirm as well because that rule sets the width of
     every confirm box in Files and is written further down this file. */
  .docConfirm.attachBox{ width:min(92%, calc(var(--ui) * 41)); max-width:min(92%, calc(var(--ui) * 41)); }
  /* The camera outside office hours: visibly shut, with the reason on it. */
  .attachSrc.attachShut{ opacity:.45; }
  .attachSources{ display:flex; flex-wrap:wrap; gap:5px; margin:2px 0 8px; }
  /* THE FIVE WAYS IN, on a phone. Camera . DocuMax . DeckGen . Splotch . Files
     fit on one line on a computer and cannot at 320; they were already allowed
     to wrap, and what was wrong was their height -- 21 to 23 pixels, against
     the 44 the phone build holds everything else to. Centred when they take two
     lines, so a row of three over a row of two reads as one group. */
  body.phoneLayout .attachSources{ justify-content:center; gap:6px; }
  body.phoneLayout .attachSrc{ min-height:44px; font-size:12px; padding:6px 10px; }
  body.phoneLayout .attachSrc .docIcon,
  body.phoneLayout .attachSrc .docIcon svg{ width:18px; height:18px; }
  /* And the list of files under them: a row is a thing to tap, not to read. */
  body.phoneLayout .attachRow{ min-height:44px; }
  .attachSrc{ display:flex; align-items:center; gap:5px; background:rgba(255,255,255,.05); border:1px solid #6a6656; color:inherit; font:inherit; font-size:max(9px, calc(var(--ui) * 0.74)); padding:3px 8px; cursor:pointer; }
  .attachSrc:hover{ background:rgba(255,255,255,.12); }
  .attachSrcOn{ border-color:var(--gold, #e0b23a); color:var(--gold, #e0b23a); }
  .attachSrc .docIcon{ width:15px; height:15px; display:grid; place-items:center; }
  .attachSrc .docIcon svg{ width:15px; height:15px; }
  .attachList{ max-height:min(38vh, 220px); overflow:auto; border:1px solid #4a473d; background:rgba(0,0,0,.18); margin-bottom:10px; }
  /* color:inherit matters. A <button> does NOT inherit colour the way a div
     does -- it takes the browser's own default, which is black -- so the file
     names in this list came out black on a dark panel and were very nearly
     unreadable. `font:inherit` was there and colour was not. */
  .attachRow{ width:100%; background:none; border:0; border-bottom:1px solid rgba(255,255,255,.045); font:inherit; color:inherit; text-align:left; cursor:pointer; }
  /* A file that will not answer THIS letter, because it was made before the
     letter asked. Greyed and listed rather than hidden: a file that quietly
     vanishes from a list reads as the game losing it, while one that is
     visibly out of bounds -- with the reason on it -- reads as a rule, and the
     rule is the whole point. Pressing it says why rather than doing nothing. */
  .attachRow.attachStale{ opacity:.42; cursor:not-allowed; }
  .attachRow.attachStale .docMeta{ font-style:italic; }
  .attachRow:hover{ background:rgba(255,255,255,.06); }
  /* The program a file came out of, so a list holding both kinds can be read at
     a glance rather than by opening things. */
  .attachRow .docMeta{ color:#a49c88; }
  .docPrimary{ background:var(--gold, #e0b23a); color:var(--ink, #201d16); border-color:var(--ink, #201d16); }
  /* The source chips are a way INTO the program now, not a filter, so they
     look like something you press: a hover, and a hint under the row saying
     what pressing one does. */
  .attachSrc{ cursor:pointer; }
  .attachHint{ color:#a49c88; font-size:max(8px, calc(var(--ui) * 0.66)); margin:-4px 0 8px; }
  .docPrimary:disabled{ opacity:.45; cursor:default; }

  /* ---- the clip on a reply ----
     The paperclip sits with the Send button rather than above the box, because
     it is a thing you do to the reply, not a thing you do to the message. */
  /* One row: SEND REPLY, then the paperclip, then whatever is clipped. The
     paperclip is an icon rather than a labelled button so the row costs no more
     height than the send button did on its own -- see replyAttachHtml. */
  .replyAttach{ display:flex; align-items:center; flex-wrap:wrap; gap:6px; margin:6px 0 0; }
  .replyClip{ background:var(--panel-light, #34322b); border:2px solid var(--ink, #201d16); color:var(--text, #f0eadb); font-family:inherit; font-size:13px; line-height:1; padding:7px 9px; cursor:pointer; }
  .replyClip:hover{ background:#454236; }
  .replyChip{ display:inline-flex; align-items:center; gap:5px; max-width:100%; background:rgba(224,178,58,.16); border:1px solid var(--gold, #e0b23a); padding:3px 4px 3px 7px; font-size:11px; }
  .replyChipName{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:150px; }
  .replyChipX{ background:none; border:0; color:inherit; font:inherit; cursor:pointer; padding:0 3px; line-height:1; }
  /* A photo on an email can be pressed to look at it again, so it is a button
     rather than a label -- and looks like the name it replaced. */
  .replyChipOpen{ background:none; border:0; color:inherit; font:inherit; cursor:pointer; padding:0;
    text-decoration:underline; text-underline-offset:2px; }
  .replyChipOpen:hover{ color:var(--gold, #e0b23a); }
  /* THE PAPERCLIP, THE CHIP AND ITS X, at phone size. This row is how a
     photograph is added to a reply, opened again, and taken off -- three taps a
     thumb has to land, and they were 21, 21 and 11 pixels tall (the pre-camera
     audit, 20 September 2026). The chip's name may run longer here too: the
     whole width of the screen is available and a truncated filename on a phone
     is harder to place than on a desk. */
  body.phoneLayout .replyClip{ min-height:44px; min-width:44px; font-size:16px; padding:9px 12px; }
  body.phoneLayout .replyChip{ min-height:44px; font-size:13px; padding:0 4px 0 10px; gap:8px; }
  body.phoneLayout .replyChipName{ max-width:min(52vw, 260px); min-height:44px;
    display:flex; align-items:center; padding:0; }
  body.phoneLayout .replyChipX{ min-width:44px; min-height:44px; font-size:16px; padding:0;
    display:flex; align-items:center; justify-content:center; }
  body.phoneLayout .replyAttach{ gap:8px; }
  /* Why the button is greyed, next to the button. A disabled control that never
     says what it wants is a dead end the player has to guess their way out of. */
  .replyNeeds{ flex:1 1 100%; color:var(--gold, #e0b23a); font-size:11px; }
  .replyBox .startbtn:disabled{ opacity:.5; cursor:default; }
  .docConfirm{ background:#262419; border:2px solid #201d16; box-shadow:4px 4px 0 rgba(0,0,0,.55); padding:12px 14px; width:min(88%, 320px); color:#f0eadb; }
  .docConfirm h2{ margin:0 0 8px; font-family:'Press Start 2P', monospace; font-size:max(8px, calc(var(--ui) * 0.66)); color:var(--gold, #e0b23a); letter-spacing:.5px; }
  .docConfirm p{ margin:0; font-size:max(9px, calc(var(--ui) * 0.76)); line-height:1.4; }
  .docInput{ width:100%; font:inherit; font-size:max(10px, calc(var(--ui) * 0.8)); padding:3px 6px; background:#f4efe0; color:#201d16; border:2px solid #201d16; }
  .docHint{ min-height:1.1em; margin:4px 0 0 !important; color:#e0b23a; font-size:max(8px, calc(var(--ui) * 0.64)) !important; }
  .docBtns{ display:flex; justify-content:flex-end; gap:6px; margin-top:10px; }
  .docBtn{ background:rgba(255,255,255,.08); border:1px solid #6a6656; color:inherit; font:inherit; font-size:max(9px, calc(var(--ui) * 0.74)); padding:3px 12px; cursor:pointer; }
  .docBtn.docPrimary{ background:var(--gold, #e0b23a); color:#201d16; }
  .docBtn.docDanger{ background:#c23b3b; }
  .docBtn:disabled{ opacity:.5; cursor:default; }
  /* The same questions on a phone, with buttons a thumb can hit. */
  .cabinet.glassNarrow .docConfirm{ width:min(92%, 340px); padding:14px 16px; }
  .cabinet.glassNarrow .docConfirm p{ font-size:14px; }
  .cabinet.glassNarrow .docBtn{ min-height:44px; padding:6px 16px; font-size:14px; }
  .cabinet.glassNarrow .docInput{ font-size:16px; padding:8px; }

  .deskPanel{ position:absolute; z-index:8500; box-sizing:border-box; background:#262419; border:1px solid #4a473d; box-shadow:4px 4px 0 rgba(0,0,0,.55); color:#f0eadb; }
  .deskPanelBar{ display:flex; align-items:center; justify-content:space-between; gap:8px; padding:4px 8px; background:linear-gradient(180deg,#34322b,#22211b); border-bottom:1px solid #4a473d; font-family:'Press Start 2P',monospace; font-size:max(6px, calc(var(--ui) * 0.6)); }
  .deskPanelTitle{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .deskPanelBtns{ flex:0 0 auto; display:flex; gap:calc(var(--ui) * 0.25); }
  .deskPanelBtn{ flex:0 0 auto; background:none; border:1px solid #6a6656; color:inherit; width:max(16px, calc(var(--ui) * 1.8)); height:max(14px, calc(var(--ui) * 1.6)); cursor:pointer; font-size:max(7px, calc(var(--ui) * 0.7)); line-height:1; padding:0; }
  .deskPanelBtn:hover{ background:#454236; }
  /* The bar is the handle the whole window is dragged by, so it says so, and
     nothing in it may be selected by a drag that starts on the text. */
  .deskPanelWin > .deskPanelBar{ cursor:move; user-select:none; -webkit-user-select:none; }
  .deskPanelBody{ position:relative; display:flex; flex-wrap:wrap; align-content:flex-start; gap:calc(var(--ui) * 0.6); padding:calc(var(--ui) * 0.8); min-height:max(70px, calc(var(--ui) * 8)); }
  /* Inside the panel an icon is a flow item, not a pinned-down thing at a
     percentage of the desktop -- and it keeps its own width rather than 15% of
     a box a fifth of the size, which would leave the labels unreadable. */
  .deskPanelBody .desktopIcon{ position:static; left:auto; top:auto; right:auto; bottom:auto; width:max(44px, calc(var(--ui) * 6.1)); cursor:pointer; }
  /* A FOLDER's body is a field, not a list: icons sit where they were put.
     Laid out from JavaScript against the body's measured width, because the
     body changes width every time the window is maximized and a grid written
     in CSS would have been right at exactly one size. Padding lives on the
     icons' computed places instead, or the first column would be inset twice.

     overflow stays visible on purpose -- an icon dragged out of the folder has
     to be visible on its way to the desk, and clipping it here would make the
     gesture look broken halfway through. */
  .deskPanelField{ display:block; padding:0; overflow:visible; }
  .deskPanelField .desktopIcon{ position:absolute; }
  /* Filling the monitor, not the game. A folder belongs to the computer; a
     folder that flew out of the screen and covered the office would say it
     did not. */
  .deskPanelWin{ display:flex; flex-direction:column; }
  .deskPanelWin > .deskPanelBody{ flex:1 1 auto; min-height:max(70px, calc(var(--ui) * 8)); }
  .deskPanelMaxed{ box-shadow:none; }
  /* Maximized, the field SCROLLS. Unmaximized it must not, because an icon
     being dragged out onto the desk has to stay visible as it crosses the
     frame -- but a maximized window covers the glass, so there is nowhere to
     drag it to and nothing to keep visible. Without this, a folder maximized
     on a small screen drew its lowest icon straight through the bottom of the
     frame and onto the taskbar. */
  .deskPanelMaxed > .deskPanelField{ overflow:auto; }
  /* Put away to the taskbar. Kept in the document rather than destroyed, so
     the arrangement inside it and its place on the glass both survive. */
  .deskPanelPutAway{ display:none !important; }
  /* Something is being dragged into this folder. */
  .deskPanelDrop{ outline:2px solid #e0b23a; outline-offset:-2px; }
  .deskPanelEmpty{ font-size:max(7px, calc(var(--ui) * 0.7)); color:#b8b09a; padding:calc(var(--ui) * 0.5); line-height:1.4; }
  /* Disposal is the same panel holding a LIST: what matters about a discarded
     thing is its name and the button that brings it back, not where it sat. */
  .deskBinBody{ flex-direction:column; flex-wrap:nowrap; gap:calc(var(--ui) * 0.3); max-height:min(46vh, 220px); overflow-y:auto; }
  .deskItem{ display:flex; align-items:center; gap:calc(var(--ui) * 0.5); padding:calc(var(--ui) * 0.25) calc(var(--ui) * 0.3); border:1px solid transparent; }
  .deskItem:hover{ border-color:#4a473d; background:rgba(255,255,255,.03); }
  .deskItemGlyph{ flex:0 0 auto; width:max(14px, calc(var(--ui) * 1.8)); height:max(14px, calc(var(--ui) * 1.8)); display:flex; align-items:center; justify-content:center; }
  .deskItemGlyph svg{ width:100%; height:100%; }
  .deskItemName{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:max(7px, calc(var(--ui) * 0.75)); }
  .deskRestore, .deskRestoreAll, .deskEmpty{ flex:0 0 auto; background:#34322b; border:1px solid #6a6656; color:#f0eadb; font-family:'VT323','Courier New',monospace; font-size:max(7px, calc(var(--ui) * 0.8)); padding:calc(var(--ui) * 0.15) calc(var(--ui) * 0.5); cursor:pointer; }
  .deskRestore:hover:not(:disabled), .deskRestoreAll:hover:not(:disabled), .deskEmpty:hover:not(:disabled){ background:#454236; }
  .deskRestore:disabled, .deskRestoreAll:disabled, .deskEmpty:disabled{ opacity:.45; cursor:default; }
  .deskPanelFoot{ display:flex; justify-content:flex-end; gap:calc(var(--ui) * 0.4); padding:calc(var(--ui) * 0.5); border-top:1px solid #4a473d; background:#22211b; }
  /* A greyed item that does not say why is a dead end, so disabled items keep
     room for a note and stay readable rather than fading out. */
  .goItemOff{ color:#8a8375; cursor:default; }
  .goItemOff:hover{ background:none; }

  /* ---- programs as windows on the glass (play/synergy-os.js) ----

     Every program in this game is an .overlay scrim with a .panelBox centred
     in it. `.asWindow` turns that same panel into a window WITHOUT touching
     the program: the scrim stops being a scrim, and the overlay shrinks to
     exactly the monitor glass, whose geometry the shell publishes as
     --glassL/T/W/H because every overlay is a child of .cabinet and spans all
     of it. No overlay is moved in the DOM, so the CSS and the tour highlights
     that expect them where they are all still work.

     pointer-events:none on the overlay is what lets several windows be open at
     once and still leaves the desktop icons behind them clickable. */
  /* A window covers the WHOLE GAME AREA, not the monitor glass.
     
     The glass is a small rectangle inside the desktop panel -- about a third of
     the game's width -- and the first version of this treated it as the screen
     a window manager owns: every window clamped inside it and force-sized to
     fit. That visibly broke things. The Calculator showed only its top half and
     Solitaire opened with half the board missing, because both are built for
     the game area and were being squeezed into a box a third of the size.
     
     The game is the screen. SYNERGY is a portal that launches programs onto it,
     not a container that holds them. So `.asWindow` spans the cabinet exactly
     as an ordinary overlay always did, and -- critically -- does NOT override
     the panel's own width or max-width. Every program keeps the size its own
     CSS gives it: .calcBox stays calculator-shaped, .solitaireBox still fits a
     whole board, .tpsBox is still nearly the full width. Windowing adds chrome
     and nothing else. */
  .overlay.asWindow{ inset:0; background:none; display:block; padding:0; pointer-events:none; }
  /* Email's name-and-icon row belongs to its WINDOW, like every other program's.
     The panel the desk opens during the tour has no room for it: Natalie's
     letter, the reply box and SEND REPLY must all fit without scrolling
     (tests/e2e/email-fits.spec.js), and the row costs that exact height. */
  #mailOverlay:not(.asWindow) #mailOverlayHeading{ display:none; }
  /* And on a phone, where Email's window is the whole game screen and the
     writing screen's Attach and SEND already sit just above where its toasts
     appear; the title bar still says EMAIL. */
  @media (max-width:600px){ #mailOverlayHeading{ display:none; } }
  .overlay.asWindow .panelBox{ position:absolute; pointer-events:auto; box-shadow:4px 4px 0 rgba(0,0,0,.55); }
  /* Once the player has resized one by hand, the explicit size wins over the
     program's own cap -- but not before. */
  .overlay.asWindow .panelBox.winSized{ max-width:none; max-height:none; width:auto; height:auto; }
  /* The front window is bright and the others dim, so which one has the
     keyboard is never a guess. */
  .overlay.asWindow .panelBox{ filter:brightness(.82); }
  .overlay.asWindow .panelBox.winFront{ filter:none; }

  /* The panel's own ✕ (and Solitaire's and the Assistant's own minimize and
     full-screen buttons) are absolutely positioned against the panel and now
     land outside the title bar, which has its own set. Hidden rather than
     removed: they are still the buttons that work when the same panel is
     opened as an ordinary dialog, and the title bar's ✕ calls exactly the same
     function they do. */
  .overlay.asWindow .panelClose,
  .overlay.asWindow .panelMaximize,
  .overlay.asWindow #assistantMinimizeBtn,
  .overlay.asWindow #assistantMaximizeBtn,
  .overlay.asWindow #solMinimizeBtn,
  .overlay.asWindow #solMaximizeBtn,
  .overlay.asWindow #msMinimizeBtn,
  .overlay.asWindow #msMaximizeBtn{ display:none; }

  /* With the desktop closed -- Log Off, its ✕, Escape, or the after-hours login
     in its place -- its windows and menu go too. They are siblings of the
     desktop, so nothing hides them for free; the first version left them
     floating over the office with no monitor behind them. */
  .cabinet.desktopHidden .overlay.asWindow .panelBox,
  .cabinet.desktopHidden #desktopBar,
  .cabinet.desktopHidden #deskPanel,
  .cabinet.desktopHidden #goMenu{ display:none !important; }
  /* Except a window opened from the DESK -- Email from Bob's monitor. It was
     never on the desktop, so closing the desktop must not take it away. */
  .cabinet.desktopHidden .overlay.asWindow .panelBox.winStandalone{ display:flex !important; }

  /* ============ THE DESKTOP ON A PHONE (the owner, 2 October 2026) ============
     "A square version that functions in a similar way as it does on desktop
     version, just a window with an x and all the apps that feels like a desktop
     computer -- including the start menu." The real desktop, opened by the
     monitor on Bob's desk (openDesktop({fromMonitor:true}) in game.js), shown as
     a square with the office dimmed round it. SYNERGY Mobile is a different
     screen and none of this touches it.

     "I do think the go bar might need to be bigger on mobile desktop and not
     able to hold as many icons": a thumb's height, GO and three Quickbar items.
     The fourth and later are still in GO; the saved Quickbar is not changed. */
  /* THE X AND ? ABOVE THE SQUARE, in the dim (the owner, 2 October 2026: they
     "are for closing out", and they were over the icons and cost the first row
     two of its four cells). The box's top edge is the square's, so they hang
     just above it, the X on the square's right edge and the ? beside it; each is
     still 44 x 44, and the overlay centres the square with far more than 44px
     above it on any phone. */
  /* Press and hold on the bare glass opens the desktop menu (synergy-os.js,
     onDeskHoldDown): the phone must not also select text or raise its own callout. */
  body.phoneLayout #desktopIcons{ -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
  /* A WINDOW'S TITLE BAR behind the X and ? (the owner, 3 October 2026: "add a bar
     behind it, like how it works on windows"). A real element (.deskTitleBar in
     beta.html and dev.html) rather than a pseudo-element so the monitor's picture
     can drop it (desktop-snapshot.js copyOf). 48px, sitting on the square's top
     edge, in .winBar's colours; the buttons (44px) are centred in it, the X at the
     far right and the ? beside it. Nowhere but the phone layout. */
  .deskTitleBar{ display:none; }
  body.phoneLayout .deskTitleBar{ display:flex; align-items:center; position:absolute; left:0; right:0; bottom:100%; height:48px; box-sizing:border-box; padding:0 100px 0 10px; z-index:1;
    background:linear-gradient(180deg, #4a4739 0%, #322f27 100%); border:2px solid var(--ink); border-bottom:0;
    font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.7059)); letter-spacing:.04em; color:var(--gold); text-shadow:2px 2px 0 rgba(0,0,0,.55);
    white-space:nowrap; overflow:hidden; user-select:none; -webkit-user-select:none; }
  /* THE X AND ? LOOK LIKE A WINDOW'S (the owner, 3 October 2026: "the x looks weird.
     Look at the style you have on the other x's in the game that's what you should
     use"). The X is the program-window close button's look, copied from .winBtn and
     .winCloseBtn: the same glyph, 44 x 44 as on a phone window, the same fill, 1px
     border, colours and red hover, no shadow and no press-shift. It sits centred in
     the bar's 46px of content (bar 48 less its 2px top border). The ? is the same
     .helpBtn a window's bar uses, 44 x 44, beside it. */
  body.phoneLayout #desktopBox > .panelClose{ top:-45px; right:2px; width:44px; height:44px; padding:0; box-shadow:none; border-radius:0;
    font-family:inherit; font-size:15px; line-height:1; color:#f4efe0; background:#565243; border:1px solid #6b6655; }
  body.phoneLayout #desktopBox > .panelClose:hover, body.phoneLayout #desktopBox > .panelClose:focus-visible{ background:#a33030; border-color:#c23b3b; }
  body.phoneLayout #desktopBox > .panelClose:active{ transform:none; box-shadow:none; }
  body.phoneLayout .desktopBox .desktopHead{ top:-45px; right:50px; }
  /* FULL WIDTH (the owner, 3 October 2026: "make sure it's big enough to actually
     get to both edges on the left and right"): no side gap, and no side borders or
     shadow to push the square past the game area's edges. */
  body.phoneLayout #desktopOverlay{ padding:0; }
  body.phoneLayout .desktopBox{ aspect-ratio:auto; }
  body.phoneLayout .desktopScreen{ aspect-ratio:1/1; border:2px solid var(--ink); border-left:0; border-right:0; box-shadow:none; }
  body.phoneLayout .desktopIcons{ bottom:var(--barH, 44px); }
  /* The glass's power light sat on top of the sound button once the bar grew. */
  body.phoneLayout .desktopScreen::after{ display:none; }
  body.phoneLayout .desktopBar{ min-height:44px; padding:4px; gap:2px; }
  body.phoneLayout .desktopBar .goBtn{ min-width:44px; min-height:34px; font-size:13px; padding:6px 8px; }
  body.phoneLayout .desktopBar .barSep{ display:none; }
  body.phoneLayout #quickbar{ gap:1px; }
  /* SIX QUICKBAR ITEMS (the owner, 3 October 2026: "I can't add any more icons to the
     go menu? There's definitely room for at least 3 more"): the cap was 3, it is 6.
     The saved bar (eight at most) is not changed, only how many are drawn. Each is
     30px so six, a tab, the clock and the two buttons still fit; the narrowest
     phones go to 28px, the smallest a thumb is asked to hit here. */
  body.phoneLayout #quickbar .quickItem{ width:30px; height:30px; }
  body.phoneLayout #quickbar .quickItem:nth-child(n+7){ display:none; }
  body.phoneLayout #goMenu .goItem, body.phoneLayout #goMenu .goCat{ min-height:38px; font-size:13px; }
  /* The bar's other two buttons, and the clock on one line. */
  body.phoneLayout .desktopBar .barTool{ width:32px; height:32px; margin-left:0; }
  body.phoneLayout .desktopBar .barTool svg{ width:22px; height:22px; }
  body.phoneLayout .desktopBar .desktopClock{ font-size:9px; letter-spacing:0; white-space:nowrap; }
  /* A window's tab: a thumb high, its picture only, so several fit. */
  body.phoneLayout .desktopBar .barTab{ flex:0 0 32px; width:32px; min-height:32px; height:32px; padding:0; justify-content:center; }
  body.phoneLayout .desktopBar .barTabLbl{ display:none; }
  body.phoneLayout .desktopBar .barTabGlyph{ width:20px; height:20px; }
  /* SIX ITEMS ON NARROW PHONES (the owner, 3 October 2026). GO, six items, a tab, the
     clock and two buttons come to 382px at the sizes above, so on a 360 phone the
     items are 28 (the least a thumb is given here) and the rest give a few px; on a
     320 phone even 28px items total 342 for six, so five are drawn there and the
     sixth is still in GO. */
  @media (max-width:375px){
    body.phoneLayout #quickbar .quickItem{ width:28px; height:28px; }
    body.phoneLayout .desktopBar{ gap:1px; padding:4px 3px; }
    body.phoneLayout .desktopBar .goBtn{ min-width:40px; padding:6px 5px; }
    body.phoneLayout .desktopBar .barTool{ width:30px; height:30px; }
    body.phoneLayout .desktopBar .barTab{ flex-basis:30px; width:30px; }
  }
  /* At 320 the bar's own tests hold GO to 44, the tab to 30 x 32 and the two buttons to
     32 (as before this change), so only the Quickbar and the gaps can give: five items
     at 28 touching, no gap anywhere, a 7px clock. */
  @media (max-width:344px){
    body.phoneLayout #quickbar .quickItem:nth-child(n+6){ display:none; }
    body.phoneLayout #quickbar{ gap:0; }
    body.phoneLayout .desktopBar{ gap:0; padding:4px 2px; }
    body.phoneLayout .desktopBar .goBtn{ min-width:44px; padding:6px 4px; }
    body.phoneLayout .desktopBar .barTool{ width:32px; height:32px; }
    body.phoneLayout .desktopBar .barTab{ flex-basis:30px; width:30px; height:32px; }
    body.phoneLayout .desktopBar .desktopClock{ font-size:7px; }
  }
  /* THE DEFAULT ICON PLACES, in four columns. The wide layout's percentages are
     written for a screen three times this size and the short-glass rule above
     stops applying once the glass is a square this tall, so the icons would sit
     on top of each other. Same order as the wide layout, column by column (3 October 2026, below); an icon the
     player has dragged keeps the place they gave it (a dragged icon's place is
     written on the icon itself, which wins over all of this). Programs that do
     not start on the desktop share the cells of ones that do: the collision
     pass deals them elsewhere when they are dragged out, as it does today. */
  body.phoneLayout .desktopIcon{ width:22%; }
  body.phoneLayout .desktopIcon{ gap:2px; padding:1px; }
  body.phoneLayout .desktopIcon .glyph{ width:32px; height:32px; }
  body.phoneLayout .desktopIcon .lbl{ font-size:10px; line-height:1.1; }
  /* FOUR ROWS OF FOUR, evenly spaced (the owner, 2 October 2026: "Can we get 4
     rows of them? Seems like we have space for it ... 4 rows, 4 columns all
     spaced out nice and evenly"). Columns 3/27/51/75% (a 22% icon, 2% between,
     3% at each side); rows 2/27/52/77% of the icon field, which is the glass
     above the bar. The first row is whole again because the X and ? that used
     to float over its right-hand end now sit above the square (see the top of
     this block). Order: the wide layout's, column by column (3 October 2026, below), with the bin in the last cell. Eleven icons start on a fresh
     desktop; the other cells are shared by programs that start elsewhere, which
     the collision pass deals apart when they are dragged out. */
  /* THE COMPUTER'S ORDER, COLUMN BY COLUMN (the owner, 3 October 2026: "mirror the
     default order that's on synergy desktop onto synergy desktop mobile going top to
     bottom left to right ... documax ... would be the top icon on the 2nd column
     because it is the 5th icon"). The computer's default places (the non-@container
     rules below), read top to bottom, then left to right, are: The Internet, Email,
     Company Resources, TPS Reports, | DocuMax, DeckGen, Solitaire, Splotch, |
     Memosweeper. Here they fill this grid the same way, four to a column, so
     DocuMax is column 2, row 1. The bin keeps the last cell. Programs that do not
     start on the desktop follow in the computer's own order and share cells, which
     the collision pass deals apart when they are dragged out. */
  body.phoneLayout #desktopInternetEl  { left:3%;  top:2%; }
  body.phoneLayout #desktopEmailEl     { left:3%;  top:27%; }
  body.phoneLayout .desktopIcon.desktopFolder{ left:3%; top:52%; }
  body.phoneLayout #desktopTpsEl       { left:3%;  top:77%; }
  body.phoneLayout #desktopDocumaxEl   { left:27%; top:2%; }
  body.phoneLayout #desktopDeckgenEl   { left:27%; top:27%; }
  body.phoneLayout #desktopSolitaireEl { left:27%; top:52%; }
  body.phoneLayout #desktopSplotchEl   { left:27%; top:77%; }
  body.phoneLayout #desktopMinesweeperEl, body.phoneLayout #desktopCameraEl, body.phoneLayout #desktop-folder-games { left:51%; top:2%; }
  body.phoneLayout #desktopContactsEl  { left:51%; top:27%; }
  body.phoneLayout #desktopDocumentsEl, body.phoneLayout #desktopCalcEl { left:51%; top:52%; }
  body.phoneLayout #desktopAssistantEl { left:51%; top:77%; }
  body.phoneLayout #desktopTerminalEl, body.phoneLayout #desktopMarketEl { left:75%; top:2%; }
  body.phoneLayout #desktopReadmeEl, body.phoneLayout #desktopSurveyEl { left:75%; top:27%; }
  body.phoneLayout #desktopSupportEl, body.phoneLayout .desktopSupportIcon { left:75%; top:52%; right:auto; bottom:auto; }
  /* Written #id.class so the unit test that reads the bin's own cell (tests/synergy-os.test.js) still finds the wide layout's rule, not this one. */
  body.phoneLayout #desktopDisposalEl.desktopIcon{ left:75%; top:77%; right:auto; width:22%; }

  /* A program opened from this desktop has all three buttons ("minimize and
     maximize buttons on the apps including the x"). One opened from SYNERGY Mobile
     is on its own (winStandalone) and keeps its two. */
  body.phoneLayout .overlay.asWindow .panelBox:not(.winStandalone) .winMaxBtn{ display:inline-flex !important; }
  /* RESTORE: "it conforms it to the mobile desktop size". The square (its top border out to the glass's own 2px; no side
     borders since 3 October 2026, so the sides are the glass's own), above its
     bar. Full screen needs no rule: every phone window already fills the screen. */
  body.phoneLayout .cabinet.glassNarrow .overlay.asWindow .panelBox.winOnGlass:not(.winStandalone){
    left:var(--glassL) !important; top:calc(var(--glassT) - 2px) !important; right:auto !important; bottom:auto !important;
    width:var(--glassW) !important; height:calc(var(--glassH) - var(--barH) + 2px) !important; }
  /* While the keyboard is up a fitted window is pinned above it like any other
     phone window: this is the keyboard rule's selector with the fitted state
     added, so it outranks the fitted rule above. */
  body.phoneLayout.keyboardUp .cabinet.glassNarrow .overlay.asWindow .panelBox.winOnGlass:not(.winStandalone){
    left:0 !important; width:auto !important; right:0 !important;
    top:var(--vvt, 0px) !important; bottom:auto !important; height:var(--vvh, 100%) !important; max-height:var(--vvh, 100%) !important; }
  .barTabFront{ background:linear-gradient(180deg, #5a5645 0%, #3d3a30 100%); border-color:#6b6655; }

  .winBar{ flex:0 0 auto; display:flex; align-items:center; justify-content:space-between; gap:6px; padding:2px 4px 2px 7px; background:linear-gradient(180deg, #4a4739 0%, #322f27 100%); border-bottom:2px solid var(--ink); cursor:grab; touch-action:none; user-select:none; }
  .winBar:active{ cursor:grabbing; }
  /* ONE HEADER PER WINDOW (see titleBar in play/synergy-os.js). The name is in
     the game's heading face, the program's own icon sits to the left of it, and
     the buttons are as tall as the line -- the owner's instruction of 15
     September, in place of a barely-readable label above a second heading that
     said the same thing. */
  .winBar{ padding:6px 8px 6px 10px; gap:9px; justify-content:flex-start; }
  .winIcon{ flex:0 0 auto; display:flex; align-items:center; }
  .winIcon svg{ width:max(14px, calc(var(--ui) * 1.25)); height:max(14px, calc(var(--ui) * 1.25)); display:block; }
  .winTitle{
    flex:1 1 auto; min-width:0;
    font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.7059));
    letter-spacing:.04em; color:var(--gold); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    text-shadow:2px 2px 0 rgba(0,0,0,.55);
  }
  .winBar .helpBtn{ flex:0 0 auto; }
  .winBtns{ flex:0 0 auto; display:flex; gap:4px; margin-left:auto; }
  .winBtn{ font-family:inherit; font-size:max(10px, calc(var(--ui) * 0.8)); line-height:1; color:#f4efe0; background:#565243; border:1px solid #6b6655; width:max(24px, calc(var(--ui) * 2.1)); height:max(24px, calc(var(--ui) * 2.1)); display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0; }
  /* The program's own heading row, while it is a window: the bar says it. */
  .winHeadHidden{ display:none !important; }
  .winBtn:hover{ background:#6b6655; }
  .winCloseBtn:hover{ background:#a33030; border-color:#c23b3b; }

  /* Minimized: still open, still holding its conversation or its draft, just
     not on the glass. display:none rather than visibility so it costs no
     layout while it is away. */
  .overlay.asWindow .panelBox.winMin{ display:none; }
  /* Maximize means the whole game area -- truly full screen, the way these
     panels filled the screen before windows existed. Not "as much of the
     monitor glass as will fit", which is what it used to mean and which made
     full screen smaller than the window's own natural size. */
  .overlay.asWindow .panelBox.winMax{ left:0 !important; right:0 !important; top:0 !important; bottom:0 !important; width:auto !important; height:auto !important; max-width:none !important; max-height:none !important; }
  /* THE MONITOR GLASS, not the whole game. The Internet opens this way so it
     reads as something the computer put up rather than something the game did:
     it covers the desktop's icons and stops at the top of the bar. The glass's
     own measurements are written by syncGlass on every layout. The maximize
     button takes it the rest of the way -- winMax above wins, being later. */
  .overlay.asWindow .panelBox.winOnGlass{
    left:var(--glassL, 0px) !important; top:var(--glassT, 0px) !important;
    width:var(--glassW, 100%) !important;
    height:calc(var(--glassH, 100%) - var(--barH, 22px)) !important;
    right:auto !important; bottom:auto !important; max-width:none !important; max-height:none !important;
  }

  /* The resize corner. Only on the bottom-right, which is the one every
     desktop has had for forty years, and hidden while maximized because there
     is nothing left to resize into. */
  .winGrip{ position:absolute; right:0; bottom:0; width:14px; height:14px; cursor:nwse-resize; touch-action:none; background:linear-gradient(135deg, transparent 45%, #6b6655 45%, #6b6655 55%, transparent 55%, transparent 70%, #6b6655 70%, #6b6655 80%, transparent 80%); }
  .winMax .winGrip{ display:none; }

  /* ---- a tab per open window, in the bar ---- */
  .barTab{ flex:0 1 auto; min-width:0; max-width:calc(var(--ui) * 8); display:flex; align-items:center; gap:calc(var(--ui) * 0.26); font-family:inherit; font-size:max(7px, calc(var(--ui) * 0.6)); color:#e6e0d0; background:linear-gradient(180deg, #403d33 0%, #2c2a23 100%); border:1px solid #55513f; border-radius:2px; padding:calc(var(--ui) * 0.12) calc(var(--ui) * 0.4); cursor:pointer; }
  .barTab:hover{ background:#4a4739; }
  .barTabMin{ opacity:.55; }
  .barTabGlyph{ flex:0 0 auto; width:max(10px, calc(var(--ui) * 0.95)); height:max(10px, calc(var(--ui) * 0.95)); display:flex; align-items:center; justify-content:center; }
  .barTabGlyph svg{ width:100%; height:100%; }
  .barTabLbl{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  /* Past about seven tabs the labels stop fitting, so they go and the icons
     stay -- which is what every taskbar does at the same point. */
  .barTabsTight .barTabLbl{ display:none; }

  /* ---- phones ----
     One window at a time, filling the glass, switched with the bar. Dragging
     and resizing are a desktop luxury: there is no room for a window smaller
     than the screen, and a title bar dragged off a 280px-wide monitor is
     unrecoverable with a finger. */
  /* Phones: one window, the whole game area. Same reasoning -- the panels are
     built for the screen, and a phone's screen is what they get. */
  .cabinet.glassNarrow .overlay.asWindow .panelBox{ left:0 !important; right:0 !important; top:0 !important; bottom:0 !important; width:auto !important; height:auto !important; max-width:none !important; max-height:none !important; }
  .cabinet.glassNarrow .winGrip{ display:none; }
  .cabinet.glassNarrow .winBar{ cursor:default; }

  /* ---- window chrome on a phone ----
     Placed here rather than up with the top bar's own phone rules, because
     .winBtn and .deskPanelBtn are defined below that point and would otherwise
     win on source order. Same media query, same reasoning.

     The oversight this fixes is the one the top bar's comment describes, a
     layer down: that bar was raised to 44px for a thumb and the buttons INSIDE
     a window were left at about sixteen, three pixels apart. On a real finger
     Close is easy to hit when you meant Minimize, and that is a program shut
     on you mid-sentence. It applied to every window -- Splotch, DocuMax,
     Documents, Email and the folder windows.

     FULL SCREEN GOES AWAY HERE rather than growing, for two reasons. It does
     nothing on a phone: a window already fills the glass at this width, so
     pressing it changed the icon and moved nothing, which reads as broken. And
     on the narrowest phones it did something worse than nothing -- a folder
     window came back SHORTER than it started and spilled its last icon out
     over the taskbar. Two thumb-sized buttons fit in a title bar where three
     never would. */
  @media (max-width: 430px){
    .winMaxBtn, .deskPanelMaxBtn{ display:none !important; }
    .winBtn, .deskPanelBtn{ width:44px; height:38px; font-size:15px; }
    .winBtns, .deskPanelBtns{ gap:6px; }
    .winBar, .deskPanelWin > .deskPanelBar{ padding-top:2px; padding-bottom:2px; }
    .winTitle, .deskPanelTitle{ min-width:0; flex:1 1 auto; }
  }
  /* THE KEYBOARD'S ROOM. body.keyboardUp is set by game.js from visualViewport
     while a phone's keyboard is up, with --vvh as what is left of the screen.
     A full-screen program stops above the keyboard instead of running under
     it, so Backslash's prompt and Email's Send stay where they can be seen.
     The programs that own their own scrolling get the same treatment for free:
     their panel body is what shrinks. */
  body.phoneLayout.keyboardUp .cabinet.glassNarrow .overlay.asWindow .panelBox{
    top:var(--vvt, 0px) !important; bottom:auto !important; height:var(--vvh, 100%) !important; max-height:var(--vvh, 100%) !important;
  }
  /* DOCKED TO THE KEYBOARD (keepFieldsAboveTheKeyboard in game.js). The part of
     a program marked data-dock="keyboard" -- Email's reply, a co-worker email's
     body -- sits on the keyboard with nothing between, and what it answers
     scrolls above it. Email's folder tabs step aside while typing, for room. */
  body.phoneLayout.keyboardUp .overlay.kbDocked .mailTabs{ display:none; }
  body.phoneLayout.keyboardUp .overlay.kbDocked #mailBody{ display:flex; flex-direction:column; padding-bottom:0; }
  body.phoneLayout.keyboardUp .overlay.kbDocked #mailBody > .mailDetail{ flex:1 0 auto; display:flex; flex-direction:column; }
  body.phoneLayout.keyboardUp .overlay.kbDocked [data-dock="keyboard"]{
    position:sticky; bottom:0; z-index:2; margin:auto -16px 0; padding:8px 12px; background:var(--panel); border-top:2px solid var(--ink);
  }
  body.phoneLayout.keyboardUp .overlay.kbDocked [data-dock="keyboard"] textarea{ min-height:0; height:4.4em; resize:none; margin-bottom:6px; }
  /* Splotch's and DocuMax's naming dialogs cover the whole screen and centre in
     it, which put their text box behind the keyboard. While it is up they cover
     what is visible and sit at the top of it. */
  body.phoneLayout.keyboardUp .spScrim, body.phoneLayout.keyboardUp .dxScrim{
    top:var(--vvt, 0px); bottom:auto; height:var(--vvh, 100%); place-items:start center; padding-top:10px; box-sizing:border-box;
  }
  /* A PHONE TURNED SIDEWAYS. Over everything, taking every tap, until it is
     upright again (syncSidewaysCard in game.js decides when). */
  .sidewaysCard{ display:none; }
  body.phoneSideways .sidewaysCard{
    display:flex; position:fixed; inset:0; z-index:2147483000; flex-direction:column; align-items:center; justify-content:center; gap:18px;
    padding:16px; background:#111008; color:var(--text); text-align:center; touch-action:none;
  }
  .sidewaysCard p{ margin:0; font-family:'Press Start 2P',monospace; font-size:12px; line-height:2; color:var(--gold); text-shadow:2px 2px 0 var(--ink); }
  .sidewaysPhone{ transform:rotate(90deg); animation:sidewaysTurn 2.4s ease-in-out infinite; }
  @keyframes sidewaysTurn{ 0%, 25%{ transform:rotate(90deg); } 55%, 100%{ transform:rotate(0deg); } }
  @media (prefers-reduced-motion: reduce){ .sidewaysPhone{ animation:none; transform:none; } }
  /* DONE, in the title bar of the window being typed in, while the keyboard is up. */
  .kbDone{ display:none; }
  body.phoneLayout.keyboardUp .winBar .kbDone{ display:flex; width:auto !important; padding:0 12px; margin-left:auto; margin-right:6px; font-family:'Space Mono',monospace; font-weight:700; font-size:13px; letter-spacing:1px; background:#3d5a2c; border-color:#5c7f45; }
  /* The phone layout: every program opened from SYNERGY Mobile is a full-screen
     window, and its ✕ is the only way back -- so a full thumb's square, and
     below the phone's own status bar or notch when the page runs under it. */
  body.phoneLayout .cabinet.glassNarrow .winBtn{ width:44px; height:44px; }
  body.phoneLayout .cabinet.glassNarrow .overlay.asWindow .panelBox.winStandalone > .winBar{ padding-top:max(2px, env(safe-area-inset-top, 0px)); }
  /* The four icon rows are spaced 27% of .desktopScreen's height apart
     (top:4/31/58/85% below), but each icon's glyph+gap+label is a fixed 71px
     tall -- fine at the desktop box's usual ~500px height, but on a
     phone-narrow .cabinet .desktopScreen can be as short as ~156px, where 27%
     is only ~42px and consecutive rows overlap outright (found alongside the
     column-width overlap above, same mobile runtime pass). Below 200px --
     comfortably under any normal width, well above the ~156px phone case --
     shrink the glyph and label together so the whole icon fits inside its own
     row band instead of spilling into the next one. */
  @container desktopScreen (max-height: 200px){
    .desktopIcon{ gap:1px; }
    .desktopIcon .glyph{ font-size:16px; width:24px; height:24px; }
    .desktopIcon .lbl{ font-size:7px; line-height:1.05; }
  }
  /* The icon field, which is everything above the bar. Positioned, so the
     percentage rows below resolve against IT rather than against the whole
     glass -- that is what keeps the bottom row clear of the bar without
     touching a single one of the percentages, and therefore without
     rearranging the desktop of anyone who never dragged an icon. */
  .desktopIcons{ position:absolute; left:0; right:0; top:0; bottom:max(22px, calc(var(--ui) * 2.3)); }
  /* FIVE ROWS, EVENLY SPACED, ON THE WIDER GLASS.

     The owner, 23 September 2026: "I hope that with the increased size we can
     also respace the icons a bit and make it so that there is room for up to 5
     rows of icons, and make sure that the distance between rows and distance
     between columns is the same" -- and, looking at the first drawing of it,
     "the space between the icons still feels too much, especially vertically.
     I'd like them to be 50% closer vertically and 10% closer horizontally."

     Those are about the AIR between the icons, not about the step from one to
     the next: an icon is 112 x 72 on a 1280 window, so a step is mostly icon.
     The vertical air halved to 21 pixels and the same 21 was used across, which
     is also the rule he set asking for five rows -- "make sure that the
     distance between rows and distance between columns is the same."

     He then asked for the columns closer again, the same evening: "close the
     gap between the columns by about 10% more." That one is the STEP, because
     ten per cent of 21 pixels is two and nobody can see two. So the column step
     came down a tenth, from 16.1% of the glass to 14.5%, which leaves about
     eight pixels between one icon's box and the next -- and an icon's box is a
     good deal wider than the picture and the label centred in it, so what you
     see between two labels is still comfortable.

     Row step 16.2% of the icon field, column step 14.5% of the glass. SIX rows
     and SIX columns fit: "ultimately there could be up to 6 rows of icons in
     this format, but we will only see 5 before we go to next column." So the
     default arrangement fills five rows and starts a new column; the sixth row
     is empty but for the bin in its far corner.

     Every icon needs a cell of its own even if it does not start on the
     desktop: without one it stacks at the corner on top of whatever is already
     there. His two columns come first, then the programs that live on the
     Quickbar or the Game menu, in the cells they take if he drags them out. */
  #desktopInternetEl  { left:3%;    top:3%; }
  #desktopEmailEl     { left:3%;    top:19.2%; }
  .desktopIcon.desktopFolder{ left:3%; top:35.4%; }     /* Company Resources */
  #desktopTpsEl       { left:3%;    top:51.6%; }
  #desktopDocumaxEl   { left:3%;    top:67.8%; }
  #desktopDeckgenEl   { left:17.5%; top:3%; }
  #desktopSolitaireEl { left:17.5%; top:19.2%; }
  #desktopSplotchEl   { left:17.5%; top:35.4%; }
  #desktopMinesweeperEl { left:17.5%; top:51.6%; }
  #desktop-folder-games { left:17.5%; top:51.6%; }   /* Games, under Splotch */
  /* The rest keep cells of their own even though none of them is on a fresh
     desktop: without one an icon stacks at the corner on top of whatever is
     already there the moment somebody drags it out of GO. */
  #desktopCameraEl    { left:17.5%; top:51.6%; }
  #desktopContactsEl  { left:17.5%; top:67.8%; }
  #desktopDocumentsEl { left:32%;   top:3%; }
  #desktopCalcEl      { left:32%;   top:19.2%; }
  #desktopAssistantEl { left:32%;   top:35.4%; }
  #desktopTerminalEl  { left:32%;   top:51.6%; }
  #desktopMarketEl    { left:32%;   top:67.8%; }
  #desktopReadmeEl    { left:46.5%; top:3%; }           /* out of the folder */
  #desktopSurveyEl    { left:46.5%; top:19.2%; }
  #desktopSupportEl   { left:75.5%; top:67.8%; }
  /* THE BIN, CENTRED OVER THE TWO CONTROLS AT THE RIGHT-HAND END of the bar --
     the Assistant and the sound button. The owner, 23 September 2026: "I want
     disposal to be further to the right- centered above the synergy ai and
     sound buttons in the bottom right."

     Placed from the RIGHT edge rather than the left, because that is the edge
     it is measured against, and given a narrower box than an ordinary icon so
     its middle can reach that far over without the box hanging off the glass.
     The arithmetic: the two controls' middle sits 2.405 type units in from the
     right (0.4 of bar padding, then half of 1.7 + 0.61 + 1.7), and this box's
     middle sits `right` plus half its 3.6 units, so `right` is 0.6.
     "Disposal" is a short word and fits the narrower box at any size. */
  #desktopDisposalEl  { left:auto; right:calc(var(--ui) * 0.6); top:84%; width:calc(var(--ui) * 3.6); }
  /* ---- SYNERGY's desktop on a short screen ----

     The window is 4:3 like a real monitor, and on a narrow screen that makes it
     SHORT: measured at 280x155 on an iPhone 12. The eight icons are laid out
     two columns wide and four rows deep, with the rows at fixed PERCENTAGES of
     the screen -- 4/31/58/85 -- and each icon a fixed ~36-44px tall. Those two
     facts do not survive a small screen together: 15% of 155px is 23px of room
     for the last row, so Survey and Calculator were cut in half, and the
     Company Handbook's two-line label ran into Survey on the way past.

     Two changes, and the second is the one that actually fixes it.

     1. A square window on a phone, which is the owner's call and the right one:
        the monitor's 4:3 shape is decoration and the icons are the point. Same
        width, about 80px more height.

     2. Below 330px of screen height -- which is every phone, and any window
        short enough for the old spacing to fail -- the icons lay out THREE
        ACROSS AND THREE DOWN instead of two across and four down. Nine slots
        for nine icons, Support the Dev included, which is what lets it come in
        off its bottom-right corner where it had nothing to do with the grid.

        Three columns fit because there was never a shortage of width: two
        columns at 3% and 18%, each 15% wide, used the left third of the screen
        and left the other two thirds empty. Spreading into that space buys
        BOTH the row it needed and wider labels, so "Company Handbook" stops
        wrapping to two lines and stops being the icon that collides with the
        one below it.

     Keyed on the screen's own height rather than the viewport's width, because
     the crop is a function of how short the window got -- a 620px window is
     299px tall and cropped just as surely as a 390px one. Only the DEFAULT
     positions move; an icon the player has dragged stays where they put it. */
  @media (max-width: 480px){
    .desktopBox{ aspect-ratio:1/1; }
  }
  /* FIVE columns, three rows, on a screen this short. Rows are already at
     4/36/68 with nothing underneath them and a fourth would need height the
     window does not have, so the only direction left is sideways -- and there
     was always width to spare.

     Four columns was the answer while there were ten icons. DeckGen made
     thirteen on 7 September and was given DocuMax's cell on the assumption
     that nudgeCollidedIcons would separate them; with twelve cells full it
     could not, so DocuMax spent a day underneath DeckGen on every phone and on
     any laptop 768px tall or less -- which is to say on the most common screen
     there is. Measured, 8 September: 1366x768 and every phone collided,
     1440x900 and above did not.

     Fifteen cells now, and two fewer icons to put in them: Attachments starts
     off the desktop (top of GO instead) and Disposal moved into the bar. That
     leaves eleven on a fresh desktop with room for the Survey, Support and
     Attachments if the player drags them back out, and a spare cell after
     that. Labels are already shrunk at this size by the max-height:200px rule
     below, which is what makes five fit. */
  @container desktopScreen (max-height: 330px){
    .desktopIcon{ width:18.4%; }
    #desktopInternetEl  { left:2%;    top:4%; }
    #desktopTerminalEl  { left:21.5%; top:4%; }
    #desktopReadmeEl    { left:41%;   top:4%; }
    #desktopAssistantEl { left:60.5%; top:4%; }
    #desktopSurveyEl    { left:80%;   top:4%; }
    #desktopEmailEl     { left:2%;    top:36%; }
    #desktopTpsEl       { left:21.5%; top:36%; }
    #desktopSolitaireEl { left:41%;   top:36%; }
    #desktopCalcEl      { left:60.5%; top:36%; }
    /* Minesweeper shares the Calculator's cell, beside Solitaire. The
       Calculator starts off the desktop; if both are out, the collision pass
       deals one of them elsewhere, as it does for the Camera and the Survey. */
    #desktopMinesweeperEl { left:60.5%; top:36%; }
    #desktop-folder-games { left:60.5%; top:36%; }
    .desktopSupportIcon { left:80%;   top:36%; right:auto; bottom:auto; }
    #desktopSplotchEl   { left:2%;    top:68%; }
    #desktopDocumaxEl   { left:21.5%; top:68%; }
    #desktopDeckgenEl   { left:41%;   top:68%; }
    #desktopContactsEl  { left:80%;   top:68%; }
    #desktopDocumentsEl { left:60.5%; top:68%; }
    /* Fifteen cells and, with the Camera, fourteen icons -- but only because
       the Survey and Support start inside the Company Resources folder. The
       Camera takes the Survey's cell; if a player drags the Survey back out,
       the collision pass deals one of them somewhere else. */
    #desktopCameraEl    { left:80%;   top:4%; }
    /* The bin is an icon again since 23 September 2026, so this layout has to
       give it a cell as well -- without one it takes the stylesheet's default
       of 84% down, which on a glass this short is underneath the bar. */
    #desktopDisposalEl  { left:80%;   top:68%; right:auto; width:18.4%; }
    /* The three Company Resources icons left the grid together, so the folder
       that holds them takes the first cell they vacated. */
    .desktopIcon.desktopFolder{ left:41%; top:4%; }
  }
  /* Support the Dev had the bottom-right corner to itself for a month. The bin
     is back in that corner since 23 September 2026, so Support takes a cell in
     the grid like everything else (above). */
  /* Bottom right, where every desktop has kept the bin since 1984 --
     just inboard of the Support mug, which already owns the corner. */
  /* Support the Dev was drawn half again the size of an ordinary icon while it
     sat alone in the corner. It is in the grid now, so it is the same size as
     its neighbours. */
  .internetBlocked{ display:none; position:absolute; inset:0; flex-direction:column; align-items:center; justify-content:center; text-align:center; padding:40px; background:#1a0e0a; }
  .internetBlocked.show{ display:flex; }
  .internetBlocked .iconbtn{ position:absolute; top:12px; right:12px; }
  .ibIcon{ font-size:max(8px, calc(var(--ui) * 3.251)); margin-bottom:10px; }
  .ibTitle{ font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 1.0837)); color:var(--danger); margin-bottom:16px; letter-spacing:1px; }
  .ibBody{ max-width:80%; color:#ddd; font-size:max(8px, calc(var(--ui) * 0.9482)); line-height:1.5; }
  .ibBody p{ margin:0 0 10px; }
  /* The same page as a program window of its own (the phone layout's route). */
  .internetBox{ background:#1a0e0a; }
  .internetBlocked.internetPage{ display:flex; position:relative; inset:auto; flex:1; min-height:260px; padding:32px 20px; }

  /* survey */
  .formfield{ width:100%; box-sizing:border-box; background:var(--panel-light); border:2px solid var(--ink); color:var(--text); font-family:'VT323','Courier New',monospace; font-size:max(8px, calc(var(--ui) * 0.9412)); padding:8px 10px; margin-bottom:12px; }
  /* The 16px floor for phones, said again HERE because the rule above is
     written after the phone block and wins the tie for a plain
     <input class="formfield"> -- the TPS initials box and the After Hours
     initials box, both of which zoomed an iPhone in on the first tap (the
     mobile audit of 16 September). */
  @media (max-width:600px){
    .formfield{ font-size:max(16px, calc(var(--ui) * 0.9412)); }
    /* AND TALL ENOUGH TO PRESS. 16px type in 8px of padding came to 36, under
       the 44 the phone build holds everything to -- and every text field in
       this game is one of these, including the one a player types their email
       into to sign in, which is the single most important tap on a phone.
       Every button beside them is 44 already; these were the exception. */
    .formfield{ min-height:44px; }
    /* One pixel short, which is a silly way to fail a rule. */
    .supportbtn{ min-height:44px; }
  }
  textarea.formfield{ min-height:70px; resize:vertical; }
  .surveyChoice{ display:block; width:100%; text-align:left; background:var(--panel-light); border:2px solid var(--ink); color:var(--text); font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.8235)); padding:10px 12px; margin-bottom:8px; cursor:pointer; box-shadow:3px 3px 0 var(--ink); }
  .surveyChoice:active{ transform:translate(3px,3px); box-shadow:none; }
  .surveyChoice.selected{ outline:2px solid var(--gold); outline-offset:2px; color:var(--gold); }
  .hp-field{ position:absolute; left:-9999px; width:1px; height:1px; opacity:0; }

  /* TPS reports minigame */
  .tpsBox{ max-width:calc(var(--ui) * 37.6435); }
  .solitaireBox{ max-width:calc(var(--ui) * 34.1144); max-height:98%; }
  .tpsPaper{ background:#efe9d6; color:var(--ink); border:2px solid var(--ink); padding:14px 16px; position:relative; }
  .tpsMasthead{ display:flex; justify-content:space-between; align-items:flex-start; gap:12px; font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7059)); border-bottom:2px solid var(--ink); padding-bottom:8px; margin-bottom:6px; }
  .tpsNote{ font-size:max(8px, calc(var(--ui) * 0.7059)); color:#5c5844; margin-top:12px; line-height:1.6; }
  /* Corporate explaining, at length, why it will not tell you what it wants.
     Italic and a shade lighter so it reads as the boilerplate it is -- the
     schedule above it is the part a player actually needs. */
  .tpsNoteJargon{ margin-top:8px; font-style:italic; color:#6b6754; line-height:1.5; }
  .tpsNoteHours{ margin-top:8px; }
  .tpsRow{ display:flex; flex-direction:column; gap:4px; margin-bottom:10px; }
  .tpsRow label{ font-family:'Space Mono',monospace; font-weight:700; font-size:max(8px, calc(var(--ui) * 0.7059)); }
  .tpsRow .formfield{ margin-bottom:0; }
  .tpsRow.tpsRowSaid{ flex-direction:row; align-items:baseline; gap:8px; flex-wrap:wrap; }
  /* A line of air between what the form TELLS you and the first thing it asks
     you to fill in (the owner, 15 September). */
  .tpsRow.tpsRowSaid + .tpsRow:not(.tpsRowSaid){ margin-top:1.4em; }
  .tpsPriorityCritical{ color:var(--danger); font-weight:700; }
  .tpsStampArea{ border:2px dashed #8f856b; min-height:70px; position:relative; margin:14px 0; display:flex; align-items:center; justify-content:center; }
  .tpsStamp{ border:4px solid var(--good); color:var(--good); background:none; font-family:'Press Start 2P',monospace; font-size:max(8px, calc(var(--ui) * 0.8824)); padding:8px 12px; transform:rotate(-8deg); opacity:0; letter-spacing:1px; }
  .tpsStamp.show{ opacity:0.85; }
  .tpsStamp.rejected{ border-color:var(--danger); color:var(--danger); }
  .tpsTrayRow{ display:flex; gap:10px; margin-top:14px; flex-wrap:wrap; }
  .tpsTrayRow .supportbtn{ flex:1; min-width:140px; }
  .supportbtn:disabled{ opacity:0.5; cursor:not-allowed; }
  .tpsQuota{ border:2px solid var(--warn); color:var(--warn); padding:10px 12px; text-align:center; font-size:max(8px, calc(var(--ui) * 0.8235)); width:100%; }
  .tpsProblems{ color:var(--danger); font-size:max(8px, calc(var(--ui) * 0.8235)); margin-top:12px; line-height:1.5; }
  .tpsProblems ul{ margin:6px 0; padding-left:18px; }

  /* terminal */
  .terminalOutput{ background:#0c0f0a; border:2px solid var(--ink); color:#8fef6a; font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647)); padding:10px; height:220px; overflow-y:auto; white-space:pre-wrap; margin-bottom:10px; }
  /* Full screen. Same shape as Solitaire's maximize, which is the only other
     window in the game that does this: the box fills the cabinet, the body
     takes the slack, and the scrollback -- normally a fixed 220px -- grows to
     whatever is left above the input row. The input row keeps its natural
     height so it never gets squeezed off the bottom. */
  .panelBox.termMaximized{ position:absolute; inset:0; width:auto; max-width:none; height:auto; max-height:none; }
  .panelBox.termMaximized .panelBody{ flex:1; min-height:0; display:flex; flex-direction:column; }
  .panelBox.termMaximized .terminalOutput{ flex:1; height:auto; min-height:0; }
  /* The close and full-screen buttons hang OUTSIDE the panel by 14px, which is
     off-screen once the panel is the screen. Brought inside, and only then. */
  .panelBox.termMaximized .panelClose{ top:6px; right:6px; }
  .panelBox.termMaximized .panelMaximize{ top:6px; right:44px; }
  .terminalInputRow{ display:flex; align-items:center; gap:6px; background:#0c0f0a; border:2px solid var(--ink); padding:8px 10px; }
  .terminalPrompt{ color:#8fef6a; font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647)); }
  .terminalInput{ flex:1; min-width:0; background:transparent; border:none; color:#8fef6a; font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647)); outline:none; }
  .terminalInput:focus-visible{ outline:2px solid var(--gold); outline-offset:-2px; }
  /* Backslash's own keyboard, phone layout only (see buildBackslashKeys in
     game.js). The scrollback takes what the keys leave, as in full screen;
     the real input stays in the row, invisible, and the line is echoed with
     a block cursor. Keys are the terminal's green on black, square, chunky. */
  .bsKeys, .bsLine{ display:none; }
  /* Edge to edge under the title bar: one black terminal, the scrollback, the
     prompt and the keys divided by hairlines rather than boxed apart (the owner,
     16 September 2026). */
  body.phoneLayout #terminalOverlay.bsDrawn .panelBody{ flex:1; min-height:0; display:flex; flex-direction:column; padding:0; background:#0c0f0a; }
  body.phoneLayout #terminalOverlay.bsDrawn .terminalOutput{ flex:1; height:auto; min-height:0; margin:0; border:0; padding:10px 12px; font-size:14px; line-height:1.35; }
  body.phoneLayout #terminalOverlay.bsDrawn .terminalInputRow{ position:relative; cursor:text; border:0; border-top:1px solid #2e4a22; padding:12px; }
  body.phoneLayout #terminalOverlay.bsDrawn .terminalPrompt{ font-size:14px; line-height:1.35; }
  body.phoneLayout #terminalOverlay.bsDrawn .terminalInputRow .terminalInput{ position:absolute; left:0; top:0; width:1px; height:1px; opacity:0; pointer-events:none; }
  /* One text size for what the terminal says, the prompt and what you type;
     words wrap whole, and only a single word longer than the row is broken. */
  body.phoneLayout #terminalOverlay.bsDrawn .bsLine{ display:block; flex:1; min-width:0; color:#8fef6a; font-family:'Space Mono',monospace; font-size:14px; line-height:1.35; white-space:pre-wrap; word-break:normal; overflow-wrap:anywhere; }
  /* The command strip floats over the foot of the scrollback, so the keys
     never move when it comes or goes. */
  .bsHints{ display:none; }
  body.phoneLayout #terminalOverlay.bsDrawn .bsHints.bsHintsOn{
    display:flex; gap:6px; position:absolute; left:0; right:0; bottom:100%; height:38px; padding:4px 12px; box-sizing:border-box;
    background:#0c0f0a; border-top:1px solid #2e4a22; touch-action:manipulation;
  }
  .bsHint{
    flex:1 1 0; min-width:0; height:30px; padding:0 8px; background:#10140c; color:#8fef6a; border:2px solid #2e4a22;
    font-family:'Space Mono',monospace; font-weight:700; font-size:14px; line-height:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
    cursor:pointer; -webkit-tap-highlight-color:transparent;
  }
  .bsHint:active{ background:#8fef6a; color:#0c0f0a; border-color:#8fef6a; }
  /* The block cursor sits ON the next character, inverted, and blinks by
     swapping back -- the character never disappears with it. */
  .bsCaret{ background:#8fef6a; color:#0c0f0a; animation:bsBlink 1.06s steps(1) infinite; }
  @keyframes bsBlink{ 50%{ background:transparent; color:#8fef6a; } }
  body.phoneLayout #terminalOverlay.bsDrawn .bsKeys{
    display:flex; flex-direction:column; gap:6px; flex:0 0 auto; margin:0; padding:8px 0 max(8px, env(safe-area-inset-bottom, 0px));
    background:#0c0f0a; border:0; border-top:1px solid #2e4a22; touch-action:none; user-select:none; -webkit-user-select:none; -webkit-touch-callout:none;
  }
  .bsRows{ display:flex; flex-direction:column; gap:12px; }
  body.phoneLayout #terminalOverlay.bsDrawn .bsKeys{ position:relative; }
  /* The letter under the finger, a balloon growing out of the top of its key,
     as both phones do. */
  .bsPop{
    display:none; position:absolute; z-index:5; transform:translate(-50%, -100%); margin-top:0; min-width:44px; height:52px; padding:0 8px;
    align-items:center; justify-content:center; background:#8fef6a; color:#0c0f0a; border:2px solid #0c0f0a; border-bottom-width:0; box-shadow:0 0 0 2px #8fef6a;
    font-family:'Space Mono',monospace; font-weight:700; font-size:28px; line-height:1; pointer-events:none;
  }
  .bsPop.bsPopOn{ display:flex; }
  /* Caps lock: the shift key stays lit, with a bar under its arrow. */
  .bsKey-shift.bsLock{ box-shadow:inset 0 -5px 0 #0c0f0a; }
  /* Dragging the cursor on the space bar: the letters fade, as on a phone. */
  .bsTrackpad .bsKey{ color:transparent !important; background:#10140c !important; border-color:#1f3317 !important; }
  .bsTrackpad .bsKey svg{ visibility:hidden; }
  /* THE KEYS ARE AN IPHONE'S KEYS. Measured against the iPhone keyboard at the
     same width (the owner, 16 September 2026, asked for them "as identical to
     the keys as possible"): letter keys a tenth of the row less its nine 6px
     gaps (33 x 42 on a 390-wide phone), rows 12px apart, shift and delete 1.3
     letters wide with the gap to z and m that an iPhone leaves, the 123 key
     and the keyboard key where the iPhone's 123 and emoji keys sit, and return
     2.7 letters wide. Touches in the gaps still find the nearest key, so the
     whole 54px row pitch is the target, as on the phone. */
  .bsRow{ --kw:calc((100% - 54px) / 10); display:flex; gap:6px; justify-content:center; }
  .bsKey{
    flex:0 0 var(--kw); min-width:0; height:42px; padding:0; background:#161c11; color:#8fef6a; border:2px solid #2e4a22; border-bottom-width:4px;
    font-family:'Space Mono',monospace; font-weight:700; font-size:22px; line-height:1; cursor:pointer; touch-action:none; -webkit-tap-highlight-color:transparent;
  }
  .bsKey:focus{ outline:none; }
  .bsKey.bsDown{ background:#8fef6a; color:#0c0f0a; border-color:#8fef6a; border-bottom-width:2px; transform:translateY(2px); }
  .bsKey-shift, .bsKey-back, .bsKey-123, .bsKey-abc, .bsKey-sym2, .bsKey-phone, .bsKey-enter, .bsKey-space{ font-size:13px; letter-spacing:1px; background:#10140c; color:#6fc24f; }
  .bsKey-shift, .bsKey-back{ flex-basis:calc(var(--kw) * 1.3); font-size:20px; }
  .bsKey-shift{ margin-right:auto; }
  .bsKey-back{ margin-left:auto; }
  .bsKey-shift.bsOn{ color:#0c0f0a; background:#6fc24f; }
  .bsKey-123, .bsKey-abc, .bsKey-sym2, .bsKey-phone{ flex-basis:calc(var(--kw) * 1.3); }
  .bsKey-phone svg{ display:block; margin:0 auto; pointer-events:none; }
  .bsKey-space{ flex:1 1 auto; }
  .bsKey-enter{ flex-basis:calc(var(--kw) * 2.7); color:#0c0f0a; background:#6fc24f; border-color:#3d6b2c; }
  /* The symbol pages' third rows have no shift: the page key and delete keep
     their width and the marks between them share the rest. */
  .bsRow-sym-2 .bsKey:not(.bsKey-back):not(.bsKey-sym2), .bsRow-sym2-2 .bsKey:not(.bsKey-back):not(.bsKey-123){ flex:1 1 0; }
  .bsRow-sym-2 .bsKey-sym2, .bsRow-sym2-2 .bsKey-123{ margin-right:auto; }
  @media (prefers-reduced-motion: reduce){ .bsCaret{ animation:none; } .bsKey.bsDown{ transform:none; } }

  /* quick \-bar */
  .quickBar{ position:absolute; left:14px; right:14px; bottom:14px; z-index:9999; background:rgba(8,8,6,0.94); border:2px solid var(--ink); box-shadow:4px 4px 0 rgba(0,0,0,0.5); display:none; flex-direction:column; opacity:0; transition:opacity 0.25s; }
  .quickBar.show{ display:flex; opacity:1; }
  .quickBar.fading{ opacity:0; }
  .quickBarLog{ max-height:180px; overflow-y:auto; padding:8px 10px 0; color:#8fef6a; font-family:'Space Mono',monospace; font-size:13px; white-space:pre-wrap; }
  .quickBarLog:empty{ padding:0; }

  /* A player who has told their OS they get motion sick or distracted by
     animation is asking every app to respect that, not just this one. 43
     separate animations (the fluorescent flicker, the toast slide, the
     background drift, every hover/press transition) is too many to safely
     hunt down one at a time without missing one, so this is a blanket kill
     switch rather than a rule-by-rule opt-out: every animation and transition
     collapses to effectively instant, its end state applied immediately.
     forwards fill-mode animations (the toast fade, for one) still reach their
     final state, just without the motion getting there. */
  @media (prefers-reduced-motion: reduce){
    *, *::before, *::after{
      animation-duration:0.001ms !important;
      animation-iteration-count:1 !important;
      transition-duration:0.001ms !important;
      scroll-behavior:auto !important;
    }
  }

  /* ============ the SYNERGY(tm) Assistant, as its own program ============
     Modelled on the chat products people already use all day, because that is
     the only reference anyone brings to a text box with a cursor in it. The
     Employee's question sits right, in a small box, in the panel colours. The
     Assistant's answer sits in no box at all -- it simply types onto the glass
     in the terminal green the machine has always spoken in. Boxing both sides
     turned every reply into a stack of separate blocks; unboxing the Assistant
     makes an answer read as one continuous thing being said to you, which is
     what it is.

     Sized like Solitaire rather than like the terminal: a window with a title
     bar, not a pop-up with a sticker in its corner. */
  .assistantBox{ max-width:calc(var(--ui) * 34); }
  /* The close badge normally hangs outside the panel by 14px, as a sticker on
     the corner. In a window with a real title bar it belongs IN the title bar,
     beside minimize -- the same un-floating Solitaire does, and for the same
     reason: two buttons in two different coordinate systems is not a window. */
  .assistantBox .panelClose{ position:static; top:auto; right:auto; }
  /* Full screen. Same shape, same glyphs and the same non-persistence as
     Solitaire and Backslash: the box fills the cabinet, the body takes the
     slack, and the transcript -- normally a fixed height -- grows into
     whatever is left above the input row. */
  .panelBox.assistantMaximized{ position:absolute; inset:0; width:auto; max-width:none; height:auto; max-height:none; }
  .panelBox.assistantMaximized .panelBody{ flex:1; min-height:0; }
  .panelBox.assistantMaximized .assistantLog{ flex:1; height:auto; min-height:0; }
  .assistantBody{ display:flex; flex-direction:column; gap:10px; min-height:0; }
  /* The transcript, watermarked with the platform's own wordmark.

     Every chat product in the world puts its mark on the empty screen, and
     SYNERGY has one already -- the same green wordmark the after-hours login
     screen carries. It is laid into the ground rather than drawn on top: a flat
     wash of the panel colour is blended over the image so the logo sits at a
     fraction of its real contrast, far enough back that a paragraph of green
     text reads cleanly across it and near enough to be unmistakably there on an
     empty screen. Background images do not scroll with the content of a
     scrolling box, so it stays put behind a conversation as that conversation
     grows. */
  .assistantLog{
    background-color:#0c0f0a;
    background-image:linear-gradient(rgba(12,15,10,0.90), rgba(12,15,10,0.90)), url("/play/skins/synergy-logo.png");
    background-repeat:no-repeat, no-repeat;
    background-position:center, center;
    background-size:cover, min(46%, calc(var(--ui) * 10)) auto;
    image-rendering:pixelated;
    border:2px solid var(--ink); padding:10px 12px;
    height:calc(var(--ui) * 15.5); min-height:0; overflow-y:auto;
    display:flex; flex-direction:column; gap:14px;
    font-family:'Space Mono',monospace; font-size:max(8px, calc(var(--ui) * 0.7647));
  }
  .assistantMsg{ max-width:86%; white-space:pre-wrap; }
  /* The Assistant. No border, no ground of its own, the full width of the
     glass: an answer, not an artefact. The generous line height is doing real
     work -- monospace at this size is dense, and a paragraph of it needs air
     to stay readable while it is still arriving. */
  .assistantMsg.fromSynergy{
    align-self:stretch; max-width:100%; color:#8fef6a; line-height:1.6;
  }
  /* The Employee, in the panel's own colours and in a box, so it reads as
     coming from this side of the glass. */
  .assistantMsg.fromYou{
    align-self:flex-end; background:var(--panel-light); color:var(--text);
    padding:6px 9px; border:2px solid var(--ink);
  }
  /* The caret that trails the text as it types, and stands alone during the
     beat before an answer begins. A blinking block is the whole of the
     "something is coming" signal now -- the old "analyzing your inquiry..."
     placard said out loud what a cursor says by existing. */
  .assistantCursor{
    display:inline-block; width:0.55em; height:1em; vertical-align:-0.15em;
    background:#8fef6a; margin-left:2px;
    animation:assistantBlink 1s steps(1, end) infinite;
  }
  @keyframes assistantBlink{ 0%,50%{ opacity:1; } 50.01%,100%{ opacity:0; } }
  /* The stamp that lands on a sentence the Assistant was not permitted to
     finish. Amber rather than red: this is not an error, it is a censor. */
  .assistantStamp{ color:var(--warn); }
  /* The wrong voice. Not styled as an alert or an error -- it is not a system
     message, it is the thing underneath saying something plainly for once, so
     it loses the terminal green and simply speaks. The pale text against the
     same dark ground is enough to register as a different speaker without
     announcing itself as a feature. */
  .assistantMsg.unfiltered{ color:#e8e4d8; }
  .assistantMsg.unfiltered .assistantCursor{ background:#e8e4d8; }
  .assistantInputRow{ display:flex; align-items:center; gap:6px; background:#0c0f0a; border:2px solid var(--ink); padding:8px 10px; flex:none; }
  /* Faded, inside the field, gone the moment a character is typed -- the
     convention every chat product shares, and the reason the ASSISTANT>
     placard beside the field is no longer there to explain the obvious. */
  #assistantInput::placeholder{ color:#4c7343; opacity:1; }
  .assistantSendBtn{
    flex:none; background:var(--panel-light); border:2px solid var(--ink); color:var(--gold);
    font-family:'Press Start 2P',monospace; font-size:max(7px, calc(var(--ui) * 0.5));
    padding:6px 8px; cursor:pointer; box-shadow:2px 2px 0 var(--ink);
  }
  .assistantSendBtn:active{ transform:translate(2px,2px); box-shadow:none; }
  .assistantSendBtn:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
  .assistantSendBtn:disabled{ opacity:0.5; cursor:not-allowed; }
  /* A phone gets the whole glass, one program at a time, which is what every
     other panel already does here. The log takes whatever height is left rather
     than a fixed number of rows, so the input never slides off the bottom. */
  @media (max-width: 480px){
    .assistantBox{ max-width:100%; }
    .assistantLog{ height:auto; flex:1; }
    /* "SYNERGY ASSISTANT" is a long title in a pixel face, and it now shares
       the bar with three window controls rather than two. At phone widths it
       comes down a size and is allowed to wrap tightly rather than pushing the
       buttons off the end -- the controls beside it keep their 44px thumb
       target either way. */
    .assistantBox .solHead{ font-size:max(7px, calc(var(--ui) * 0.48)); line-height:1.25; }
    /* The icon and the title share one span, so a wrap used to drop the second
       word underneath the icon. Making the span a flex row keeps the icon
       beside the text it belongs to and lets the words wrap next to it. */
    .assistantBox .solHead > span{ display:flex; align-items:center; gap:5px; min-width:0; }
  }
  /* iOS zooms a text field under 16px and does not zoom back. The shared rule
     for this lives with the other inputs; this is the one field added since. */
  @media (pointer: coarse){
    #assistantInput{ font-size:16px; }
  }

/* Dev: the Refined terminal's replay (game.js, mdrOpenReplay) -- a big live
   copy of the screen over the room. */
#mdrReplay{ position:fixed; inset:0; z-index:99990; background:rgba(0,0,0,.84); display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:14px; pointer-events:none; }
#mdrReplay[hidden]{ display:none; }
#mdrReplay svg{ width:min(92vw, 110vh); aspect-ratio:100 / 80; border:3px solid #2c5a68; border-radius:10px; background:#07161d; }
#mdrReplay .mdrReplayHead{ font-family:'Space Mono', monospace; font-size:14px; letter-spacing:1px; color:#9fe3ee; }
