/* WHAT THE APPEARANCE SETTINGS DO TO ARTHUR.
 *
 * ⚠️ HAND-WRITTEN. Its sibling, /site/appearance-page.css, is GENERATED from the mockup and is
 * the look of the settings PAGE. This file is the other half: the page has no mockup for this,
 * because there is nothing to draw - it is the app wearing what you picked.
 *
 * Everything here keys off flags that APPR.applyAll() writes, and off nothing else:
 *   <html data-bg-light|-lightpos|-stars|-mark|-twinkle>   the backdrop's three layers
 *   <html style="--bg-lstr|--bg-sstr|--bg-mstr|--bg-msz">  their strengths and the mark's size
 *   <html data-motion="off">                               Movement, the app's existing flag
 *   <body class="cn-sharp cn-round dn-compact noshadow nofaces outline bigtap
 *                stillbg followrm ispaid xt-glass xt-midnight">
 *
 * ★ EVERY STRENGTH IS DOUBLED. The sliders start at 50% and 50% has to look like the app looked
 * before any of this existed, which is how the mockup was tuned ("the CSS bases were re-tuned so
 * 50% reads about like the old 100% did"). So a base of .055 is written `calc(.055 * 2 * str)`.
 */

/* =================================================================================
   THE BACKDROP - three independent layers
   ================================================================================= */

/* ---- THE LIGHT ---------------------------------------------------------------------
   .stage::after is the accent bloom (backdrop.css). Its three pools are a fixed shape there;
   here the shape is the setting. ★ HIS SHIPPING DEFAULT IS MIDDLE - he nearly removed the light
   from Arthur altogether over exactly this ("the light bothers me, it is uncentred"). */
html[data-bg-lightpos="mid"] .stage::after{
  background:radial-gradient(1100px 900px at 50% 46%, var(--accent), transparent 70%)}
html[data-bg-lightpos="corners"] .stage::after{
  background:
    radial-gradient(760px 680px at 86% 4%, var(--accent), transparent 62%),
    radial-gradient(620px 580px at 8% 88%, var(--accent), transparent 62%)}
html[data-bg-lightpos="top"] .stage::after{
  background:radial-gradient(1400px 820px at 50% -14%, var(--accent), transparent 68%)}
html .stage::after{opacity:calc(.20 * 2 * var(--bg-lstr, .5))}
html[data-bg-light="off"] .stage::after{display:none}

/* The app paints a SECOND half of the light on body.stage.app::before: a vignette plus the
   site's blue hueWash. The wash is part of the light and goes with it; the vignette is
   structure (it is what makes the middle of the screen read as lit) and stays either way.
   ⚠️ !important is required on both: the rule it overrides carries !important itself, because
   backdrop.css turns .stage::before off with one. Losing that is how the first version of the
   app's own lighting block painted absolutely nothing. */
html[data-bg-light="off"] body.stage.app::before{
  background:radial-gradient(125% 90% at 50% 42%, transparent 48%, rgba(0,0,0,.55) 100%) !important}

/* ---- THE MARK ----------------------------------------------------------------------
   #bgart / #bgart2 already exist and already drift. What is new is that you can turn them off,
   change how strong they are, how big they are, and which mark it is. */
html[data-bg-mark="off"] #bgart,
html[data-bg-mark="off"] #bgart2{display:none}
html #bgart {opacity:calc(.055 * 2 * var(--bg-mstr, .5))}
html #bgart2{opacity:calc(.035 * 2 * var(--bg-mstr, .5))}
html #bgart {width:calc(560px * var(--bg-msz, 1));height:calc(560px * var(--bg-msz, 1))}
html #bgart2{width:calc(320px * var(--bg-msz, 1));height:calc(320px * var(--bg-msz, 1))}
/* A PICTURE IS NOT A MASK. The Arthur A is white art that can sit at 5% and still read; a photo
   or a plan badge is full-colour and disappears at that weight, which is the same trap the
   mockup hit with the plan badge on a light theme. Both get roughly double, plus a little
   contrast, and the picture is `cover` so a photo of any shape fills its corner. */
#bgart.bgpic, #bgart2.bgpic{background-size:cover;background-position:center;border-radius:18px}
html #bgart.bgpic {opacity:calc(.11 * 2 * var(--bg-mstr, .5));filter:saturate(1.1) contrast(1.05)}
html #bgart2.bgpic{opacity:calc(.07 * 2 * var(--bg-mstr, .5));filter:saturate(1.1) contrast(1.05)}
/* the plan badge is an SVG INSIDE the element, so the A's background has to come off and the
   drawing has to fill the box. It is full-colour art, so it carries its own paint - no stroke. */
#bgart.bgplan, #bgart2.bgplan{background-image:none}
#bgart.bgplan svg, #bgart2.bgplan svg{width:100%;height:100%;display:block;stroke:none}
html #bgart.bgplan {opacity:calc(.10 * 2 * var(--bg-mstr, .5))}
html #bgart2.bgplan{opacity:calc(.07 * 2 * var(--bg-mstr, .5))}
:root[data-theme="light"] #bgart.bgpic,
:root[data-theme="light"] #bgart2.bgpic{filter:saturate(1.25) contrast(1.12)}

/* ---- THE STARS ---------------------------------------------------------------------
   The starfield was taken OUT of Arthur on 2026-08-21 ("it looked fake, it looked bad... remove
   the stars from everywhere"). This setting is what brings it back, and it ships OFF, which is
   his own shipping default - so nobody sees it again unless they ask for it. */
/* z-index 1, ABOVE the two drifting marks (z-index 0). His: "when we have the stars and the A,
   the A goes on top of the stars." It does: #appsky is inserted BEFORE #bgart, so at the same
   z-index the mark paints over it and swallows the stars wherever the A covers. The sky is the
   smaller, sharper thing and belongs in front. It stays behind the app, which is z-index 1 and
   later in the DOM, and it catches nothing either way (pointer-events:none). */
#appsky{position:fixed;inset:0;pointer-events:none;z-index:1;overflow:hidden}
#appsky i{position:absolute;display:block;width:2px;height:2px;border-radius:50%;
  background:#fff;opacity:calc(.55 * 2 * var(--bg-sstr, .5))}
/* ★ 2026-09-24 HIS: "the stars are not visible at all" - on LIGHT mode. They are #fff, 2px, on
   a near-white page. Same fault as the A mark beside them and as half of this session: art
   painted for a black page with no light version. Dark specks on light, and a touch smaller,
   because dark ink at 2px reads heavier than white ink does. */
:root[data-theme="light"] #appsky i{background:#25324a;
  opacity:calc(.42 * 2 * var(--bg-sstr, .5))}
:root[data-theme="light"] #appsky{mix-blend-mode:multiply}
/* ★ AN ANIMATED PROPERTY CANNOT ALSO CARRY A SETTING. The twinkle writes `opacity` every frame,
   so a strength written on the star is thrown away 60 times a second - the exact bug the mockup
   hit ("I lower how strong the stars are and it feels off"). The SKY carries the strength and
   the star carries the twinkle. */
html[data-bg-twinkle="on"] #appsky{opacity:1}
html[data-bg-twinkle="on"] #appsky i{animation:apSkyTwinkle 3.4s ease-in-out infinite}
@keyframes apSkyTwinkle{0%,100%{opacity:calc(.30 * 2 * var(--bg-sstr,.5))}50%{opacity:calc(.75 * 2 * var(--bg-sstr,.5))}}
/* ⚠️ THE TWINKLE OWNS opacity WHILE IT RUNS, so the light-mode dimming above is thrown away
   60 times a second on any star that is twinkling. It needs its own keyframes. */
@keyframes apSkyTwinkleLight{0%,100%{opacity:calc(.24 * 2 * var(--bg-sstr,.5))}50%{opacity:calc(.58 * 2 * var(--bg-sstr,.5))}}
:root[data-theme="light"] html[data-bg-twinkle="on"] #appsky i,
:root[data-theme="light"][data-bg-twinkle="on"] #appsky i{animation-name:apSkyTwinkleLight}
html[data-motion="off"] #appsky i{animation:none}
/* ★★ MOVEMENT STOPS THE DRIFT. IT DOES NOT DELETE THE MARK.
   backdrop.css turns motion off with `html[data-motion="off"] #bgart, #bgart2 {display:none}` -
   it REMOVES them. That was defensible when "Background motion" was one buried switch meaning
   "no moving background at all", but Movement is now a row inside the Mark's own settings, and a
   switch inside a layer that makes the whole layer vanish is a bug: you turn off the drifting and
   the mark you were adjusting is simply gone, while its own Mark tile is still ticked.
   The mark stays and stands still. ⚠️ The phone's `#bgart2{display:none!important}` under 600px
   still wins, which is what we want - one mark on a small screen. */
html[data-motion="off"] #bgart, html[data-motion="off"] #bgart2{display:block;animation:none}
html[data-bg-stars="off"] #appsky{display:none}
/* the site window stacks its own backdrop over this one; the marks already hide there */
body.sitewin-in #appsky{display:none}

/* =================================================================================
   EXTRA THEMES.  ★ HIS TEST, and it is the one to use from now on: a theme changes what every
   surface is MADE OF, not what colour it is. A palette swap belongs with Dark/Light.
   ================================================================================= */

/* ---- MIDNIGHT: the colour stripped out. True black instead of dark blue-grey, panels
   near-black, so the accent is the only colour on screen. On a phone the black pixels are simply
   off, which is why it is the one people leave on. He likes this one best: "midnight is probably
   the best, looks so good."
   ★★ A THEME MUST OVERRIDE EVERY SURFACE TOKEN, NOT THE ONES YOU REMEMBER. The first version set
   eight and missed `--topbar`, which paints BOTH the top bar and the left rail - so the page went
   black and those two stayed dark blue, sitting on it like two panels that had not got the memo.
   His: "look at the sidebar. Look at the background. In the top bar." The app's full surface set
   is --bg --panel --panel2 --topbar --line --line2 --dim --mid --fg; redesign.css adds
   --good-dim, --shadow and --mshadow. All of them are here. If a new one is ever added to :root,
   it belongs here too. */
body.xt-midnight{
  --bg:#000000; --panel:#0b0b0e; --panel2:#131318; --topbar:#08080b;
  --line:#1b1b21; --line2:#2a2b32;
  --dim:#7e8088; --mid:#b0b2b9; --fg:#f4f4f6; --good-dim:#0c2b22;
  --shadow:0 1px 2px rgba(0,0,0,.5), 0 8px 22px rgba(0,0,0,.6);
  --mshadow:0 1px 2px rgba(0,0,0,.5), 0 22px 60px rgba(0,0,0,.75);
}
body.xt-midnight.stage.app{background:#000000}
/* ⚠️ NO RING ON THE CHROME. A thin inset edge is what stops a small card melting into black, and
   it is right on a card. On the TOP BAR and the RAIL - full-height, full-width surfaces - the same
   ring draws a visible rectangle around the whole side of the app. His: "look at the side thing,
   look how it looks... it shouldn't have this like border around it." Cards only. */
body.xt-midnight .card, body.xt-midnight .modal, body.xt-midnight .ttile,
body.xt-midnight .wscard, body.xt-midnight .collcard, body.xt-midnight .optcard,
body.xt-midnight .kcube{
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.05)}

/* ---- GLASS: every surface shows what is behind it. The one extra theme that is a material and
   not a palette, which is why it survived the cut ("keep Midnight and Glass, nothing else").
   ★★ THE FIRST VERSION DID NOTHING VISIBLE ("I put glass and it doesn't work"), for two reasons.
   It tinted --panel, but the app's big surfaces - the top bar and the left rail - are painted from
   --topbar, which it never touched. And a translucent panel only READS as glass if there is
   something behind it: the blur has to be on the surface AND the surface has to sit over the
   backdrop, so the bloom and the drifting mark show through it.
   ⚠️ backdrop-filter is expensive. It goes on the big surfaces only, never on a row or a chip. */
/* ★★ A DARK FILM OVER A DARK PAGE IS INVISIBLE. The first version tinted the panels with a
   darker version of themselves, which is physically reasonable and reads as absolutely nothing on
   an app that is already near-black - his "I put glass and it doesn't work". Frosted glass reads
   because it is LIGHTER than what is behind it: a white film that picks up the light and the
   colour underneath. So on dark, panels are white at 5-9%; on light they are white at 55%. */
body.xt-glass{
  --panel:rgba(255,255,255,.055); --panel2:rgba(255,255,255,.085);
  --topbar:rgba(255,255,255,.035);
  --line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.15);
}
:root[data-theme="light"] body.xt-glass{
  --panel:rgba(255,255,255,.55); --panel2:rgba(255,255,255,.4); --topbar:rgba(255,255,255,.45);
  --line:rgba(20,30,50,.08); --line2:rgba(20,30,50,.14);
}
body.xt-glass .top, body.xt-glass .side, body.xt-glass .card, body.xt-glass .modal,
body.xt-glass .wscard, body.xt-glass .collcard, body.xt-glass .optcard, body.xt-glass .kcube,
body.xt-glass .moresheet .morecard, body.xt-glass .tmodal{
  backdrop-filter:blur(18px) saturate(1.35); -webkit-backdrop-filter:blur(18px) saturate(1.35)}
/* a table tile is a small surface and there are dozens on screen at once: it takes the
   translucency without the blur, which is the part that costs. */
body.xt-glass .ttile{background:color-mix(in srgb, var(--panel) 70%, transparent)}
/* the backdrop has to be VISIBLE for any of this to read as glass, so glass turns the light up */
body.xt-glass .stage::after{opacity:calc(.20 * 2.6 * var(--bg-lstr, .5))}
/* ★★ IT HAS TO BE SEEN TO BE A THEME. Translucency and a blur are physically correct and read as
   almost nothing on a dark app - which is why his verdict was "I put glass and it doesn't work".
   Real glass is legible because of its EDGE: a bright line where the light catches the top lip,
   and a darker one underneath. That edge is what makes a pane look like a pane. */
body.xt-glass .top, body.xt-glass .side, body.xt-glass .card, body.xt-glass .modal,
body.xt-glass .wscard, body.xt-glass .collcard, body.xt-glass .optcard, body.xt-glass .ttile{
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14), inset 0 0 0 1px rgba(255,255,255,.06),
             0 10px 30px -18px rgba(0,0,0,.8)}
/* the backdrop is the whole point of glass, so the mark behind it comes up too */
body.xt-glass #bgart {opacity:calc(.055 * 3.2 * var(--bg-mstr, .5))}
body.xt-glass #bgart2{opacity:calc(.035 * 3.2 * var(--bg-mstr, .5))}

/* =================================================================================
   WHAT IS ON SCREEN
   ================================================================================= */

/* ---- "Menu on the left: icons only" is GONE (2026-09-21, his call) ---------------------------
   He looked at the icons-only rail and said: "I think you should remove this actually. It doesn't
   really do anything, it just makes things smaller. So you can only have it big."
   He is right, and for a reason worth writing down: Arthur's rail is not a nav bar. It carries the
   ACTIVITY list - who is on shift, by name - and the account row. Stripping it to symbols throws
   away the part that is information and keeps only the part that is navigation, which was never
   the expensive part. Discord can afford an icon rail because its rail is servers; ours is people.
   The setting, its flag, its preview mode and all of its CSS are removed rather than hidden, so
   nothing can quietly come back with a regenerate. */

/* ---- Photos of people. PC only, which is his call: "you can't even see the team" on a phone.
   ★ The first version aimed at `.avatar`, which is the profile EDITOR's picture - the one place
   the setting must NOT reach, because you are looking at it to change it. The faces people
   actually see are elsewhere, and each list has its own class:
     .avimg    a photo layered over the initial, in the rail's ACTIVITY list and in member lists
     .acctava  the account row at the foot of the rail
     .memava   the team screen        .msgav   a chat message
     .actava   an activity entry      .dsmeava  the settings header      .uc-av  a mention chip
   ⚠️ .wsava and .bizlogo are the WORKSPACE's logo, not a person, and stay whatever happens.
   The image goes and the circle with the initial underneath stays, so a row keeps its shape. */
body.nofaces .avimg{display:none !important}
body.nofaces .acctava, body.nofaces .memava, body.nofaces .msgav, body.nofaces .meav,
body.nofaces .actava, body.nofaces .dsmeava, body.nofaces .uc-av, body.nofaces .pfxava{
  background-image:none !important}

/* ---- Cards: filled, or just an outline ---- */
body.outline #floorTables .ttile{background:transparent}
body.outline #floorTables .ttile{box-shadow:inset 0 0 0 1px var(--line2)}

/* =================================================================================
   THE TWO PHONE-ONLY SETTINGS
   ★ 2026-09-21 AUDIT: both of these were PLACEBOS. The page wrote the flag, the preview answered
   it, and the real app never read either one - "Cards across the screen" set --pvcols, which only
   the little preview screen uses, and "Everything a bit bigger" set body.bigtap against selectors
   that matched nothing at that width. A switch that moves the preview and not the app is worse
   than no switch, because you believe it.
   ================================================================================= */
@media (max-width:820px){
  /* Cards across the screen. The real floor is `.tilesgrid`, an auto-fill grid, and its phone
     rules carry !important - so this has to as well. */
  body .tilesgrid{grid-template-columns:repeat(var(--pvcols, 2), minmax(0, 1fr)) !important}
  body[style*="--pvcols: 1"] .tilesgrid{grid-auto-rows:132px}

  /* Everything a bit bigger. Measured against the real thing: a table tile is 96px and a bottom
     bar row is 74px, so these are a real step up rather than a number that happens to be larger
     than a selector nobody matched. */
  /* ⚠️ !important. The phone pass pins the tile with `.ttile{min-height:0!important;
     padding:13px 14px!important}` to claw back space, and !important beats any specificity, so
     the first version of this rule was simply ignored. Anything that fights the phone pass has to
     shout back. */
  /* ⚠️ AND the tile's height is the GRID ROW, not the tile. `.tilesgrid` sets grid-auto-rows, so
     a min-height on the tile - !important or not - changes nothing at all: the row decides. This
     is the second time the same measurement saved a wrong fix. */
  body.bigtap .tilesgrid{grid-auto-rows:120px!important}
  body.bigtap .ttile{min-height:0!important;padding:15px 16px!important}
  body.bigtap nav .tab{min-height:64px!important}
  body.bigtap nav .tab .lb{font-size:11.5px}
  body.bigtap .chip, body.bigtap .dchip{min-height:40px}
  body.bigtap button:not(.iconbtn):not(.sbbtn):not(.tab){min-height:46px}
  body.bigtap .strow{min-height:56px}
}

/* =================================================================================
   SHAPE AND SPACE - CORNERS
   ★ HIS RULE: "if I press sharp it's not only gonna be in the workspace. It's gonna be
   everywhere. Settings, everywhere. I want consistency."
   Arthur had ~900 hard-coded `border-radius` values, so a corner token nothing read would have
   been a switch that did nothing. scratchpad/gift/mkcorners.py rewrote 695 of them to
       calc(Npx * var(--Rk, 1) + var(--Rf, 0px))
   and these two numbers are the whole setting. A SCALE, not a set of buckets, because a scale of
   1 is the app exactly as it was - that is provable, and it was proved by comparing the computed
   radius of all 1269 rounded elements before and after.
   Circles, pills (>=100px) and zeros were left alone: an avatar that turns square on Sharp is not
   a sharper avatar, it is a different component. The mockup makes the same choice.
   ⚠️ The `, 1` and `, 0px` fallbacks are load-bearing. If this stylesheet ever fails to load, every
   corner in Arthur falls back to the number it always had instead of collapsing to zero. */
body{--Rk:1;   --Rf:0px}
body.cn-sharp{--Rk:0;   --Rf:2px}
body.cn-round{--Rk:1.9; --Rf:0px}

/* =================================================================================
   SHAPE AND SPACE - ROOMY OR COMPACT
   ★ his: "the roomy and the compact don't really make that much of a difference" - so it moves
   the things you actually look at: the rows, the cards, the tiles and the top bar.
   ⏳ This is the first pass over the app's own screens; the page itself already follows it. */
body.dn-compact .card{padding:11px 12px}
body.dn-compact .ttile{padding:8px 10px}
body.dn-compact .tiles{gap:8px}
body.dn-compact main{padding:11px}
body.dn-compact .grp{margin:11px 0 7px}

/* ---- Shadows off. The DROP shadows go; the inset 1px edges stay, because they are what holds a
   panel's shape once its shadow is gone (the mockup's own rule, same reasoning). */
body.noshadow .card, body.noshadow .collcard, body.noshadow .wscard, body.noshadow .ttile,
body.noshadow .kcube, body.noshadow .modal, body.noshadow .optcard, body.noshadow .authcard,
body.noshadow .top, body.noshadow nav{box-shadow:inset 0 0 0 1px var(--line)}

/* =================================================================================
   YOUR COLOUR, AND YOUR GRADIENT
   ★ His: "gradients don't really work... I want the gradients to also be on the logo. I want
   things like the logo to be implemented with the colors and everything."
   A gradient is TWO colours: --accent and --acc2. On a plain colour both ends are the same, so
   every rule below is a no-op there and nothing has to be written twice. What was missing is that
   the app fills things with the FLAT `var(--accent)`, so picking Ember gave you one orange.
   These are the surfaces big enough to show a gradient; a 2px dot or a 1px border is not.
   ================================================================================= */

/* ---- THE WORDMARK. It was a flat PNG, so it stayed Arthur-blue while the app went teal.
   The image is a MASK now and two layers fill it: the left 22.7% is the A (measured - the blue
   glyph is columns 0-207 of a 950px file, with the gutter ending at 223), the rest is the
   lettering in --fg. Same idea as the preview's own logo, which is a gradient A beside a neutral
   bar, so the little screen and the real top bar finally say the same thing. */
.brandwmc{
  display:inline-block; height:var(--bwh,19px); width:auto; aspect-ratio:950/154;
  -webkit-mask:url(/logo-word.png) no-repeat center/100% 100%;
          mask:url(/logo-word.png) no-repeat center/100% 100%;
  background:
    linear-gradient(120deg, var(--accent), var(--acc2)) left center / 22.7% 100% no-repeat,
    linear-gradient(var(--fg), var(--fg))              right center / 77.3% 100% no-repeat;
}
.brandwmc.brandwm-lg{--bwh:32px}
/* the splash sits on the brand's own dark background in both themes, so its lettering stays white */
#splash .brandwmc{background:
    linear-gradient(120deg, var(--accent), var(--acc2)) left center / 22.7% 100% no-repeat,
    linear-gradient(#f2f4f7,#f2f4f7)                    right center / 77.3% 100% no-repeat}

/* ---- the things the app fills with the accent, filled with the gradient instead ---- */
/* ⚠️ `.tab.active .ic` was in this list and it should never have been: .ic is the SPAN around the
   icon, so a background-image painted a coloured rectangle behind the glyph - a blue plate on the
   selected menu row, which is half of "look at the highlight, it looks weird". A gradient belongs
   on a surface, not behind a 17px symbol. */
button.primary, .btnq.primary, .primary.fit,
#prefsHub .btnq.primary, #profileCard .btnq.primary, .sbbtn.primary{
  background-image:linear-gradient(120deg, var(--accent), var(--acc2))}
/* New order is the loudest button on the floor and the one he looks at */
#newOrderBtn, .newbtn, [data-new-order]{
  background-image:linear-gradient(120deg, var(--accent), var(--acc2))}
/* the workspace mark in the rail, and the avatar fallback, are accent plates */
.wsava, .wslogo, .avatar.noimg{
  background-image:linear-gradient(120deg, var(--accent), var(--acc2))}

/* ---- --accent-dim is MIXED TOWARDS THE PAGE, whatever the page is now ----------------
   It is the faint fill behind a selected row or a chip. APPR mixes it against the page colour,
   but Midnight moves the page to pure black, so the mix has to move with it or a selected menu
   row reads as a bright slab on black. His: "it's blocky, look at the colour highlight." */
body.xt-midnight{--accent-dim:color-mix(in srgb, var(--accent) 15%, #000000)}
body.xt-midnight .tab.active{box-shadow:inset 2px 0 0 var(--accent)}

/* =================================================================================
   THE LIGHT THEME'S CHROME - a bug this page exposed
   ★ 2026-09-21. `--topbar` is declared ONCE, at :root, and the light theme block never overrode
   it - so on Light the top bar and the whole left rail stayed #12161f, near-black, while the rest
   of the app went white. Every word in that rail is painted with --mid / --fg, which are DARK ink
   on Light, so the menu was dark text on a dark bar: unreadable.
   It has been true for as long as the light theme has existed; nobody met it because Light was a
   buried row. It is a first-class setting now, with a preview showing what it should look like,
   so it has to actually work. The rail keeps a faint tint so it still reads as chrome rather than
   as more page.
   ================================================================================= */
:root[data-theme="light"]{--topbar:#f7f9fc}
:root[data-theme="light"] .side{border-right:1px solid var(--line)}
:root[data-theme="light"] .top{border-bottom:1px solid var(--line)}

/* =================================================================================
   THE LIGHT THEME, FINISHED
   ★ 2026-09-21. Light has been a row in a settings list for a year and nobody ever really used
   it, so nobody found out that it does not work. It is a tile with a preview now, so it has to.
   Three separate causes, all of them long-standing:
   ================================================================================= */

/* 1. THE PAGE ITSELF WAS HARDCODED DARK. `body.stage.app{background:#0a0e18}` - a literal, set so
      the app would sit on the shared backdrop - ignores --bg completely. So on Light the tokens
      all went light, every panel went white, and the floor they sat on stayed navy. */
:root[data-theme="light"] body.stage.app{background:var(--bg)}

/* 2. THE VIGNETTE IS A DARK-THEME DEVICE. It paints rgba(0,0,0,.55) around the edge so the middle
      of a dark screen reads as lit. On a light page it is a grey smear in the corners. Light gets
      a soft blue-tinted lift instead, which does the same job upwards.
      ⚠️ !important, because the rule it replaces carries one (backdrop.css turns .stage::before
      off with !important, so the app had to shout to paint anything at all). */
:root[data-theme="light"] body.stage.app::before{
  background:
    radial-gradient(125% 90% at 50% 42%, transparent 58%, rgba(24,42,76,.10) 100%),
    radial-gradient(1200px 720px at 50% 14%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 66%) !important}

/* 3. ★★ redesign.css KEYS ITS LIGHT VARIANTS OFF `[data-mode="light"]` AND THE APP SETS
      `data-theme`. The app's own notes call this out as the reason "the light theme never really
      applied", and it is still true for the handful of rules that were never brought across. Six
      of them, and one is the EMPTY TABLE: it kept `rgba(16,22,34,.6)`, a dark translucent fill,
      while its label is painted with --fg, which is dark ink on Light. Every free table on the
      floor was black-on-black.
      Re-keyed here rather than by setting data-mode on every .rd wrapper: there are four rules,
      and .rd is added to elements at runtime all over the app. */
:root[data-theme="light"] .ttile.free{background:rgba(255,255,255,.58)}
:root[data-theme="light"] .ttile.free:hover{background:rgba(255,255,255,.74)}
:root[data-theme="light"] .msglist2{background:rgba(255,255,255,.62);border-color:var(--line2)}
:root[data-theme="light"] .mcatbody{background:rgba(0,0,0,.03)}

/* ---- and the Appearance page's own tiles ----------------------------------------------------
   `.aptile` (the card a little screen sits in) is a hardcoded #1f2836 in the mockup, which only
   ever ran on a dark page. On Light the tile stayed dark while its caption is painted with --mid,
   dark ink - so "Dark", "Match my device" and "Extra themes" were unreadable labels on their own
   tiles. Audited the whole generated stylesheet for hardcoded darks: 41 of them, and this is the
   only one that is CHROME. The other 40 are inside the little screens, which are drawing a dark
   app on purpose and must stay dark even while you are reading them on a light one. */
/* ★★ 2026-09-21 SWEEP: the page's own surfaces are LITERALS from the mockup, which only ever ran
   on the dark blue-grey palette. `.aptile` is #1f2836 and `--panel3` (the switch track, the
   slider's unfilled half) is #1b2331 - so on MIDNIGHT the app goes black and the Appearance
   page's own tiles and switches stay blue-grey, sitting on it. Found by setting Midnight with a
   magenta accent and scanning every surface for a colour that still leans blue.
   Dark is left on its exact literals - that is the look he signed off - and the other three
   materials get their own. */
:root[data-theme="light"] .apa.apa.apa .aptile{background:var(--panel2)}
:root[data-theme="light"] .apa.apa.apa{--panel3:#dfe4ec}
body.xt-midnight .apa.apa.apa .aptile{background:#17171d}
body.xt-midnight .apa.apa.apa{--panel3:#212128}
body.xt-glass .apa.apa.apa .aptile{background:rgba(255,255,255,.07)}
body.xt-glass .apa.apa.apa{--panel3:rgba(255,255,255,.1)}
:root[data-theme="light"] .apa.apa.apa .apfloat,
:root[data-theme="light"] .apa.apa.apa .apdlgbox{box-shadow:0 0 0 1px var(--line2), 0 30px 70px -34px rgba(24,42,76,.45)}

/* =================================================================================
   THINGS THAT MUST STAY SOLID WHATEVER THE THEME DOES
   ★ His: "whenever you do a change in the appearance, so let's say you go to glass, the unsaved
   changes is transparent." Glass turns --panel into a white film, and the save bar is painted
   with --panel - so the bar you have to read went see-through and the gradients showed through
   the words. A bar you can see through is not a bar (his own rule, from the phone pass).
   `--solid` is --panel everywhere except in a material theme that dissolves it.
   ================================================================================= */
body{--solid:var(--panel)}
body.xt-glass{--solid:color-mix(in srgb, var(--bg) 84%, #ffffff)}
:root[data-theme="light"] body.xt-glass{--solid:#ffffff}
.savebar{background:var(--solid)}
body.xt-glass .savebar{backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}

/* ---- the backdrop mark on a light page ------------------------------------------------------
   His: "the marks do not exist if it's bright mode." The Arthur A is WHITE art, so at 5% on a
   white page it is nothing at all. A photo or a plan badge is full colour and must be left alone.

   ★★ 2026-09-24 THE INVERT IS GONE, AND THIS IS WHY - two fixes for one problem is worse than
   none. This rule used to say `filter:invert(1)`, which turns the WHITE A into dark ink. That was
   right while it was the only fix. Then backdrop.css's own light-mode section started working
   (nothing had ever set `data-mode`, so it had been dead since the theme switch was built) and
   that section swaps the art to arthur-a-dark.png, which is real dark ink, rgb(26,38,64).

   Dark ink INVERTED is rgb(229,217,191): a warm cream. Measured on the page it lands on:

       page rgb(238,241,246)  ->  mark rgb(238,240,243)

   Three levels, and warm against a cool page, which is exactly the dirty non-mark he was looking
   at when he said "the marks still don't look good here".

   ⚠️ ONE MECHANISM. The real dark asset wins over inverting a white one: invert flips the
   anti-aliased edge pixels too, so the inverted mark carries a faint coloured fringe the painted
   one does not have. */
/* ★★★ 2026-09-24, THIRD TIME ON THIS, AND THE SLIDER'S TOP END IS THE POINT. His: "it still
   looks transparent when it's full."
   Checked first whether anything was over it, because he thought something was: every layer above
   #bgart - #app, .appbody, main, the vignette - is fully transparent, no filter, no
   backdrop-filter. Nothing covers the mark. It was simply sitting at opacity .18, because .18 is
   what 100% mapped to.
   That is the real fault: 100% on a slider should look like 100% of something. A LINEAR range
   cannot do both jobs here - make the top bold and the default is heavy, keep the default light
   and the top is still a watermark, which is the corner I kept painting myself into.
   So the range is a curve. Doubling the strength more than doubles the mark:

       25%  .049     a whisper
       50%  .145     the default: a watermark you notice and can read through
       75%  .289
      100%  .480     a mark, not a hint

   ⚠️ calc() CAN multiply two custom properties when both are unitless <number>, which is what
   makes the curve possible in pure CSS. Verified in the running app before relying on it.
   ⚠️ LIGHT ONLY. The dark theme is the one he likes ("if we go to the dark mode it looks nice"),
   and its ground is a different problem, so its range is left exactly as it was. */
/* ★ 2026-09-25 AND THE LIGHT MARK IS NEUTRAL, like the dark one. His: "colour problems, they
   look weird." Measured off his screenshot:

       cards      rgb(255,255,255)   hue   0 deg   saturation  0%
       page       rgb(230,236,245)   hue 219 deg   saturation 43%
       the mark   rgb(209,216,228)   hue 219 deg   saturation 26%

   The two themes were not doing the same thing. On dark the mark is arthur-a-lg.png, PURE WHITE,
   so it adds brightness and no colour at all. On light it is arthur-a-dark.png, rgb(26,38,64) -
   a NAVY - so it adds a blue shape, and at the strong end of the slider that is a blue-grey
   blotch in the corner rather than texture. A watermark should not introduce a hue.
   grayscale() on the navy gives a neutral dark grey, so the light mark now does to the page
   exactly what the white one does on dark: changes the lightness, nothing else.
   ⚠️ Only the plain A. A photo (.bgpic) and the plan badge (.bgplan) are meant to be in colour
   and are excluded by the selector already. */
:root[data-theme="light"] #bgart:not(.bgpic):not(.bgplan){
  filter:grayscale(1);
  opacity:calc(.10 * var(--bg-mstr, .5) + .38 * var(--bg-mstr, .5) * var(--bg-mstr, .5))}
:root[data-theme="light"] #bgart2:not(.bgpic):not(.bgplan){
  filter:grayscale(1);
  opacity:calc(.065 * var(--bg-mstr, .5) + .245 * var(--bg-mstr, .5) * var(--bg-mstr, .5))}
/* ⚠️ AND THE PLAN BADGE HAS TO SAY NO TO THE SWAP. backdrop.css paints the dark A on
   `.stage[data-mode="light"] #bgart`, which out-specifies the plain `#bgart.bgplan{background-image:none}`
   above - so picking "Your plan" as the mark would put the dark A back BEHIND the badge, two marks
   in one corner. A photo is safe without this because applyMarkArt sets it inline. */
:root[data-theme="light"] #bgart.bgplan,
:root[data-theme="light"] #bgart2.bgplan{background-image:none}

/* =================================================================================
   THE PREVIEW HEADER AND THE POPPED-OUT WINDOW
   ================================================================================= */

/* ★★ THE "WEIRD LIGHTING" IS A FLAT RECTANGLE OVER A GLOW.
   `.apstick` carries `background:var(--bg)` - it had to, because in the mockup it was STICKY and
   content scrolled under it. In the app it is static (the preview does not follow you any more),
   so all that background does is paint an opaque 760x536 box on top of `#prefsHub .dsglow`, the
   soft accent light across the top of every settings page. A flat colour over a gradient leaves a
   hard edge exactly where the box ends, which is the band he keeps pointing at above PREVIEW.
   It is the same mistake round 25 found in the mockup, arriving by a different door.
   Nothing paints there now, so the glow passes through unbroken. */
.apa.apa.apa .apstick{background:none}

/* ★ AND THE GLOW ITSELF BANDS. 11% alpha spread over 420px quantises into visible steps on an
   8-bit screen - the rule he met three times on the mockup ("a very low alpha over a long
   distance will band"). On the Appearance pane, where the eye is on a big flat preview, it is
   the most visible place in the app. A very fine noise breaks the steps up without changing the
   colour; it is 1px of SVG turbulence at 1.8%, which reads as nothing on its own. */
/* ⚠️⚠️ DO NOT GIVE .dsglow `position:relative`. It is `position:absolute` - OUT OF FLOW - and it
   is 420px tall. Making it relative to "contain" the noise layer put those 420px back INTO the
   flow and pushed every settings pane down by 448px: a screen of nothing above the heading, on
   every page in the dialog, not just this one. An absolutely-positioned element is ALREADY a
   containing block for an absolutely-positioned child, so nothing was needed here in the first
   place. Shipped and caught by him in one screenshot. */
#prefsHub .dsglow::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:.018;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E")}

/* ★ A WINDOW HAS TO LOOK LIKE A WINDOW. His: "it doesn't show that it's popped out." It had a
   1px ring and a soft shadow, which on a dark page beside a dark dialog is almost nothing. It
   gets a lifted chrome bar, a brighter edge and a real drop shadow, so it reads as sitting ON TOP
   of the page rather than being part of it. */
.apa.apa.apa .apfloat{
  box-shadow:0 0 0 1px color-mix(in srgb, var(--fg) 16%, transparent),
             0 18px 50px -12px rgba(0,0,0,.75), 0 40px 90px -30px rgba(0,0,0,.9)}
.apa.apa.apa .apfbar{
  background:color-mix(in srgb, var(--panel2) 70%, var(--fg) 6%);
  box-shadow:inset 0 -1px 0 color-mix(in srgb, var(--fg) 12%, transparent)}

/* ★★ RESIZING SCALES THE PREVIEW, IT DOES NOT RESHAPE IT. His: "the problem is that you're not
   zooming it, you're not making it smaller." Dragging the window narrower used to RE-LAY-OUT the
   preview - the menu squeezed, the tables reflowed, the text stayed the same size - so a small
   window showed a cramped different app instead of the same app, smaller. The preview is pinned
   to its base width inside the window and TRANSFORMED, so every proportion inside it is exactly
   the one he signed off, at whatever size the window is. */
.apa.apa.apa .apfbody{overflow:hidden}
.apa.apa.apa .apfbody .apprev{transform-origin:0 0}

/* =================================================================================
   ACCESSIBILITY - TEXT SIZE
   ★ 2026-09-21. "Text size" has four options and had never done anything a person could see.
   It writes data-tsize on <html>, and the only two rules that read it zoomed the INSIDE of
   full-screen settings windows - `m` and `xl` had no rule at all. So two of the four options did
   nothing whatsoever, and the other two resized a screen you visit occasionally while the floor,
   the nav, the orders and every board you read during service stayed exactly as they were.
   scratchpad/gift/mktextsize.py rewrote 1,427 hard-coded font sizes to
       calc(Npx * var(--Tk, 1))
   and these four numbers are the setting. A SCALE, not buckets, so --Tk:1 is the app exactly as
   it was - proved by digesting the computed font-size of all 6,353 elements before and after.
   ★ TEXT ONLY, NOT ZOOM. zoom would have been one line and is what the old rules did, but it
   scales padding, icons and layout too - that is "make everything bigger", and this setting says
   Text size. Someone who wants to READ the screen wants type, not a magnifying glass.
   The range is deliberately modest at the top: past ~1.25 the app's fixed-height rows start
   clipping their own text, which is worse than small text.
   ================================================================================= */
/* ★ 2026-09-23 HIS SCALE, off the accessibility mockup: 80 / 100 / 120 / 150, named rather than
   numbered - "don't name them percentages, small medium large extra large". It replaces the
   .92/1/1.12/1.25 above, which he found too narrow to see.
   ★★ 2026-09-23: EXTRA LARGE IS GONE, AND THE WARNING ABOVE IS WHY. It said the top of this scale
   was not safe until the app's fixed-height rows stopped clipping, and it was right - he found the
   sidebar's Working-now section crushed to its heading at Large and to nothing at Extra large.
   That particular bug is fixed (see .sideactivity), but he made the call to stop at Large anyway:
   "the maximum size is going to be large and not extra large, so there's going to be small, medium,
   large". Three stops, .8 / 1 / 1.2.
   ⚠️ IF EXTRA LARGE EVER COMES BACK, it needs more than this line. Anything saved as "xl" is
   migrated to "l" by applyReading(), so re-adding the rule alone would not bring anyone back to it,
   and the audit that has to happen first is every fixed height in the app at 1.5 - not just the one
   sidebar section that happened to be noticed. */
body{--Tk:1}
:root[data-tsize="s"]  body{--Tk:.8}
:root[data-tsize="m"]  body{--Tk:1}
:root[data-tsize="l"]  body{--Tk:1.2}

/* =================================================================================
   ACCESSIBILITY - SPACE BETWEEN LINES
   ★ 2026-09-23, off the accessibility mockup. Size and weight are the two halves of "I can see
   this"; leading is the half of "I can follow this", and it is the most useful of the three for a
   long list read at speed, which is most of Arthur.
   scratchpad/gift/mklineheight.py rewrote 261 hard-coded line-heights to
       calc(<what it was> * var(--Lh, 1))
   so one number moves all of them. Proved by digesting the computed line-height of every element
   in the app before and after: 2,379 of them resolve to a length, and the total is IDENTICAL at
   --Lh:1 - so Normal is the app exactly as it was - and 24% larger at Loose.
   ================================================================================= */
body{--Lh:1}
:root[data-lh="roomy"] body{--Lh:1.16}
:root[data-lh="loose"] body{--Lh:1.33}

/* =================================================================================
   ACCESSIBILITY - BIGGER BUTTONS
   ★ 2026-09-23. The setting already half existed: `body.bigtap` has raised minimum sizes since
   the phone pass - but every one of those rules lives inside @media (max-width:820px), so on a
   PC or a tablet in landscape the switch did nothing at all. The accessibility mockup puts it
   under TOUCH with no mention of phones, which is right: a tablet propped on a pass is exactly
   where fat targets matter and it is not a phone.
   So the same idea, unscoped. MINIMUMS rather than multiplied padding, deliberately: multiplying
   every padding inflates cards and panels that were never tap targets, while a floor only grows
   the things that are too small and leaves everything else where it was.
   The numbers are the phone pass's own, which were measured against the real thing rather than
   picked: a table tile is 96px and a bottom bar row 74px, so these are a real step up.
   ⚠️ Ownership moved with it. Appearance used to own this as a phone-only switch and wrote the
   class on every apply, which would have quietly turned the accessibility setting off again -
   that toggle is gone from the module, and this is the only writer now.
   ================================================================================= */
/* ⚠️ .acsw IS EXCLUDED because a toggle switch is a SHAPE, not a line of text in a box. This rule
   sets a floor on the height only, which is right for a button whose words got bigger and wrong
   for a 44x26 track: the Accessibility switches became 44x46 pills with a small knob floating at
   the top of them. Accessibility's own sheet grows both dimensions together instead. Adding it
   here rather than out-specifying it there is deliberate - this selector already carries five
   :not()s and beats anything a sanely-scoped page rule can say, so the next person to hit this
   would have had to win a specificity race they did not know they were in. */
body.bigtap button:not(.iconbtn):not(.sbbtn):not(.tab):not(.dsx):not(.dsbk):not(.acsw){min-height:46px}
body.bigtap .strow{min-height:56px}
body.bigtap .chip, body.bigtap .dchip{min-height:40px}
body.bigtap .stseg button{min-height:38px}
body.bigtap .dsit, body.bigtap .dstop{min-height:42px}
body.bigtap .tilesgrid{grid-auto-rows:120px}

/* =================================================================================
   ACCESSIBILITY - BOLDER TEXT and HIGHER CONTRAST
   ================================================================================= */

/* ---- BOLDER TEXT. The third pass of the same shape as corners and text size: 904 hard-coded
   font-weights now read --Wb, so one number moves all of them. 0 normally, 100 when it is on, so
   400 -> 500, 700 -> 800, 800 -> 900; a font with no 900 clamps to its heaviest, which is the
   right failure. Weight and size are the two halves of "I can read this", and this is the half
   that helps most with dyslexia. */
body{--Wb:0}
body.boldtext{--Wb:100}

/* ---- HIGHER CONTRAST. ★ This one is for a Greek terrace at midday, which is where a lot of
   Arthur is actually used: a tablet in daylight loses the low-contrast greys first, and --dim is
   the colour most of the app's secondary text is written in. It is a TOKEN change, not a pass -
   text goes towards the extremes, and the hairlines that separate rows get strong enough to
   survive sunlight, because in bright light a 1px #1c2430 border simply is not there.
   Deliberately NOT a "high contrast mode" that repaints everything black and white: that throws
   away the status colours the floor is read by. */
body.hicontrast{
  --fg:#ffffff; --mid:#dfe4ec; --dim:#b4bcc7;
  --line:#3a4557; --line2:#55627a;
}
:root[data-theme="light"] body.hicontrast{
  --fg:#000000; --mid:#1f242c; --dim:#3d4551;
  --line:#9aa5b5; --line2:#6d7a8c;
}
body.xt-midnight.hicontrast{
  --fg:#ffffff; --mid:#e2e3e8; --dim:#b6b8c0;
  --line:#3a3b44; --line2:#565862;
}
/* the accent has to clear the background too, or the one colour that means "act on this" is the
   first thing to disappear in sunlight */
body.hicontrast{--accent-lt:#ffffff}
:root[data-theme="light"] body.hicontrast{--accent-lt:var(--accent)}
/* and a focus ring you can see, since the same conditions make the default one vanish */
body.hicontrast :focus-visible{outline-width:3px;outline-offset:3px}

/* =================================================================================
   THE SETTINGS PAGES' OWN HEADING
   ★ 2026-09-23, HIS: "where is the text that says accessibility", and then "look, it's different
   in the mockup than in the actual Arthur."
   Both mockups open with a 21px heading. Neither reached Arthur: mkapcss SKIPs `subhead` on the
   way in, on the reasoning that it is Arthur's class and not the page's, so nothing in the app
   styled these two <h2>s at all and they fell back to the BROWSER'S default - 16px, because the
   page around them is 14px and a default h2 is 1.5em. No rule to find, which is why it looked
   like a missing stylesheet rather than a missing rule.
   It lives here, in the hand-written glue both settings pages already read, rather than in either
   mockup's generated sheet: one owner for a thing the two pages share. Scoped to .apa, so it is
   the Appearance and Accessibility pages only and no other .subhead in the app moves.
   ⚠️ Written in the app's tokens, not the mockup's numbers. A fixed 21px would have been the one
   heading in Arthur that ignored Text size and Bolder text. */
.apa.apa.apa h2.subhead{
  margin:0 0 16px;
  font-size:calc(21px * var(--Tk, 1));
  font-weight:calc(800 + var(--Wb, 0));
  letter-spacing:-.4px;
}

/* =================================================================================
   TURN OFF ANIMATIONS  -  the Accessibility row, finally doing what it says
   ★★ 2026-09-23. The row existed, was labelled "Turn off animations" and promised "screens stop
   sliding and cards stop fading in. Everything lands at once instead." It did none of it. The
   switch wrote body.followrm and NOTHING read that class - the word appeared once in the whole
   codebase, in a comment. Measured with it on: 177 running animations and 1369 transitions, the
   same as with it off.
   ⚠️ ALMOST-ZERO, NOT `none`. Killing an animation outright also kills its animationend /
   transitionend event, and anything that waits for one to clean up, unhide or advance a step
   never hears back - the classic way "no animations" turns into "the screen is stuck". 1ms
   finishes instantly to the eye and still fires.
   ⚠️ AND THE DEVICE IS HONOURED SEPARATELY. Someone who has asked their phone or their laptop for
   less motion has already answered this question; they should not have to answer it again inside
   Arthur. The switch is the override for everyone else.
   ================================================================================= */
body.noanim *, body.noanim *::before, body.noanim *::after{
  animation-duration:1ms !important;
  animation-delay:0s !important;
  animation-iteration-count:1 !important;
  transition-duration:1ms !important;
  transition-delay:0s !important;
  scroll-behavior:auto !important;
}
/* ★★ 2026-09-23, HIS: "I want turn off animations to mean turn off the UI animation, not the
   BACK animation. Right now I press it and it stops the A from moving. If you want to stop the A,
   you can go to the Appearance settings."
   He is right, and it is an ownership point rather than a taste one. The backdrop already HAS a
   switch - Movement, inside Appearance's own backdrop settings, which writes data-motion - so the
   drift answering to two different switches on two different pages meant neither page could tell
   you the truth about it. This one turns off the UI: screens sliding, cards fading, panels
   opening. The backdrop is not the UI.
   ⚠️ THE LINE IS "DOES IT EVER STOP". Every backdrop animation is `infinite` - the two marks, the
   bloom, the twinkle, and the previews' copies of them - and every UI animation runs once and
   settles. That is also why the spinners are on this side of the line: a frozen spinner reads as a
   crash, and it is a status indicator, not decoration.
   ⚠️ THE DURATIONS ARE RE-STATED, which is duplication, and it is the lesser evil. CSS cannot
   say "undo that !important", and dropping the !important above would hand the fight back to
   rules like `main section.entering .tilesgrid > *` which out-specify the blanket on element
   count. The numbers come from _preview/backdrop.css (bgDriftA 24s, bgDriftB 33s, bgBloom 23s),
   arthur-ui-v2.html (#bgart phone 26s, apSkyTwinkle 3.4s) and appearance-page.css (ap_pvDrift 11s,
   ap_pvBreathe 6s). If one is re-timed there and not here, the backdrop keeps moving - just at the
   old speed - which is a wrong number rather than a dead feature. */
body.noanim #bgart {animation-duration:24s !important;animation-iteration-count:infinite !important}
body.noanim #bgart2{animation-duration:33s !important;animation-iteration-count:infinite !important}
body.noanim .stage::after{animation-duration:23s !important;animation-iteration-count:infinite !important}
body.noanim #appsky i{animation-duration:3.4s !important;animation-iteration-count:infinite !important}
/* the previews are pictures OF the backdrop, so they follow the backdrop's rule, not the UI's */
body.noanim .pvmark{animation-duration:11s !important;animation-iteration-count:infinite !important}
body.noanim .pvstar{animation-duration:6s !important;animation-iteration-count:infinite !important}
/* ★★★ 2026-09-25 THE SETTINGS DIALOG'S OWN BACKDROP, AND THE GIFT CARD'S WEATHER.
   HIS DIAGNOSIS, and it was a better one than mine: "I was in accessibility, I had turn off
   animations on. So the animations were off... the marks in the background are not moving and the
   right bottom mark fully disappears."

   The rule above this was written for the APP's backdrop and names #bgart / #bgart2. The settings
   dialog does not use those: it builds its own pair, .dsmk.a and .dsmk.b, off the same two sets of
   keyframes. So the moment you opened Settings with the switch on, the blanket caught them and the
   two marks stopped dead - which is also why the bottom-right one looked like it had vanished: the
   drift is what carries it into view from -120px, and frozen at 1ms on its first frame there is
   almost nothing of it on screen.

   ⚠️ SAME NUMBERS AS #bgart AND #bgart2 BECAUSE IT IS THE SAME MOTION. plDriftA/plDriftB are
   bgDriftA/bgDriftB under another name. If one pair is re-timed, re-time the other.

   ★ AND THE GIFT CARD KEEPS ITS WEATHER, his: "whenever I hover over the box with animations off,
   I want the stars to still flicker and the box to still work. Animations are not just animations,
   it's literally only UI animations, like panels."
   That is the same line the block above draws, applied one level in: the sparkles and the box are
   the card being alive, not a screen arriving. The six sparkles keep their own staggered timings,
   because a stagger is what stops six identical dots looking like a machine. */
body.noanim #prefsHub .dsmk.a{animation-duration:24s !important;animation-iteration-count:infinite !important}
body.noanim #prefsHub .dsmk.b{animation-duration:33s !important;animation-iteration-count:infinite !important}
/* Pricing's own pair, which is switched off today but answers the same keyframes if it comes back */
body.noanim #prefsHub #dsPane_plan .plmk.a{animation-duration:24s !important;animation-iteration-count:infinite !important}
body.noanim #prefsHub #dsPane_plan .plmk.b{animation-duration:33s !important;animation-iteration-count:infinite !important}

/* the gift card: the box lifts, its art turns, its shadow spreads, and the sparkles arrive */
body.noanim .gfa.gfa.gfa .hubcard{transition-duration:.18s,.2s,.2s !important}
body.noanim .gfa.gfa.gfa .hart svg, body.noanim .gfa.gfa.gfa .hart img{transition-duration:.32s !important}
body.noanim .gfa.gfa.gfa .hart::after{transition-duration:.3s,.3s !important}
body.noanim .gfa.gfa.gfa .spk i{transition-duration:.34s,.42s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i,
body.noanim .gfa.gfa.gfa.ph .spk i{animation-iteration-count:infinite !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(1),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(1){transition-delay:.03s !important;animation-delay:.9s,0s !important;animation-duration:4.8s,11s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(2),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(2){transition-delay:.14s !important;animation-delay:1.7s,1.2s !important;animation-duration:5.6s,13s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(3),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(3){transition-delay:.25s !important;animation-delay:1.1s,2.4s !important;animation-duration:4.4s,10s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(4),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(4){transition-delay:.36s !important;animation-delay:2.1s,.6s !important;animation-duration:6s,14s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(5),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(5){transition-delay:.47s !important;animation-delay:1.4s,3.1s !important;animation-duration:5.2s,12s !important}
body.noanim .gfa.gfa.gfa .hubcard:hover .spk i:nth-child(6),
body.noanim .gfa.gfa.gfa.ph .spk i:nth-child(6){transition-delay:.58s !important;animation-delay:2.4s,1.8s !important;animation-duration:4.6s,15s !important}

/* a spinner that has stopped looks like a crash */
body.noanim .spin, body.noanim [class*="spin"]{animation-duration:inherit}
@media (prefers-reduced-motion:reduce){
  body *, body *::before, body *::after{
    animation-duration:1ms !important;
    animation-delay:0s !important;
    animation-iteration-count:1 !important;
    transition-duration:1ms !important;
    transition-delay:0s !important;
    scroll-behavior:auto !important;
  }
}

/* =================================================================================
   FLASH FOR ALERTS  -  the Accessibility page's sound row
   The screen edge pulses once when Arthur needs you, so the alert has a channel that is not
   sound. See flashAlert() for why it hangs off one function.
   ⚠️ IT IS EXEMPT FROM "Turn off animations", and that is deliberate rather than an oversight.
   The two settings can both be on: someone who cannot hear the alert and does not want screens
   sliding about wants the flash MORE, not less. Left to the blanket rule above it would run for
   1ms and be invisible - the one animation in Arthur whose whole job is to be noticed would be
   the one that was silently switched off.
   It is a single opacity pulse, not movement: nothing slides, nothing scales, so it does not
   reintroduce what that setting is there to remove. */
#acFlash{position:fixed;inset:0;z-index:99999;pointer-events:none;opacity:0;
  box-shadow:inset 0 0 0 6px var(--accent), inset 0 0 90px -20px var(--accent)}
body.noanim #acFlash.on, #acFlash.on{
  animation:acFlashPulse 620ms ease-out 2 !important;
  animation-duration:620ms !important;
  animation-iteration-count:2 !important;
}
@keyframes acFlashPulse{0%{opacity:0}18%{opacity:.85}100%{opacity:0}}
