/* The desk apps' title bar. See app-bar.js for what it replaces and why.
 *
 * Everything here reads tokens.css, which every desk already loads, so the
 * four desks cannot drift into four bars again. The only per-app value is the
 * accent, set from the app's own icon colour — the same blue, copper, green
 * and grey the launcher tiles and the installed icons use, so arriving at a
 * desk looks like opening the thing you tapped.
 */
/* The Buyer and Cost desks pinned their old strip and reserved its height on
   the body. The strip is hidden once this bar exists, so the reservation goes
   with it or the page opens with 64px of nothing. */
html.ab-ready body{padding-top:0!important}

/* No rubber-band, no pull-to-refresh chrome. This file is loaded only by the
   four desk apps and the Team Desk (never the facade pages), so it is safe to
   set this on the document itself rather than behind an app-bar selector.
   Inner scrollers that legitimately want their own overscroll containment
   (AsiaSource's rail, the Guide's sticky TOC on other pages) are unaffected —
   this only stops the OUTER page from bouncing or handing the gesture to the
   browser's own refresh/back-navigation. */
html,body{overscroll-behavior:none}

/* Tapping a control on the mobile web shows a grey highlight box for a beat —
   correct on a page of links, wrong on a title bar meant to read as an app's
   own chrome. Cosmetic only; it does not affect focus rings or :active
   styling, which every button here already has its own rule for. */
html{-webkit-tap-highlight-color:transparent}

/* The bar itself is chrome, not content: its labels should feel like part of
   the app frame, not selectable page text. Deliberately scoped to #app-bar
   ONLY, not to the desks below it — a buyer copying a number out of a
   landed-cost table or a market's unit-value figure is a real, expected use
   of this site, and a blanket rule would have taken that away from every
   desk to fix a title bar. */
#app-bar.ab{-webkit-user-select:none;user-select:none;cursor:default}
#app-bar.ab input,#app-bar.ab [contenteditable]{-webkit-user-select:text;user-select:text;cursor:text}

#app-bar.ab{
  --ab-accent:var(--jade-900);
  position:relative;z-index:300;
  border-bottom:1px solid var(--line);
  background:var(--paper);
  color:var(--ink-900);
}
#app-bar.ab[data-app="news"]{--ab-accent:#1B5C9B}
#app-bar.ab[data-app="buyer"]{--ab-accent:#A95324}
#app-bar.ab[data-app="cost"]{--ab-accent:#146A4C}
/* My Market colours its own numbers red and green by the sign of the change,
   the way a Western market screen does. Its chrome therefore cannot be red or
   green without saying something it does not mean — hence plum, which is the
   one strong hue the other three apps have not taken. */
#app-bar.ab[data-app="market"]{--ab-accent:#6D4A86}
#app-bar.ab[data-app="team"]{--ab-accent:#465561}
/* A hairline of the app's colour along the top: enough to tell the apps
   apart at a glance, not enough to become a coloured bar. */
#app-bar.ab::before{content:"";display:block;height:3px;background:var(--ab-accent)}

.ab-in{
  display:flex;align-items:center;gap:var(--sp-3);
  width:min(1920px,calc(100% - 40px));min-height:58px;margin:auto;
}

/* ---- identity ---- */
.ab-id{display:inline-flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}
/* The app's own icon, the one the browser installs to a home screen. It is a
   drawing on paper, not a glyph on a colour, so it gets an icon's edge
   instead of a tinted plate. */
.ab-ico{
  width:34px;height:34px;border-radius:9px;
  border:1px solid var(--line);background:var(--paper);
  box-shadow:0 1px 2px rgba(7,60,51,.10);
}
/* Team has no drawing; its line glyph keeps the old tinted plate. */
.ab-ico-line{padding:5px;border:0;color:#fff;background:var(--ab-accent);box-shadow:none}
.ab-id b{font:700 var(--fs-small)/1 var(--ff-sans);letter-spacing:.02em;white-space:nowrap}

/* ---- adopted status chips ----
   The desks style their own chips; this only decides where they sit and stops
   any one of them from pushing the controls off the end. */
.ab-status{
  display:flex;align-items:center;gap:var(--sp-2);min-width:0;flex:1 1 auto;
  overflow:hidden;
  color:var(--ink-500);font:500 var(--fs-micro)/1 var(--ff-mono);white-space:nowrap;
  /* These chips belong to the desk, not to this bar, and there can be three of
     them: on a narrow laptop the last one is cut off mid-word, which reads as
     a broken layout rather than as a row that ran out of room. The fade says
     "there is more here" the way a scrolling list does. */
  -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent);
  mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent);
}
.ab-status>*{flex:0 0 auto}
.ab-status [data-hq-label]{font-weight:700;color:var(--ink-700)}
.ab-status .is-online [data-hq-label]{color:var(--jade-700)}

/* ---- the app's own chips ----
   Two facts the desks cannot tell you themselves: how old the data is, and
   which build you are looking at. They sit between the adopted status and the
   controls, dressed as the other pills in this bar — hairline, paper, mono —
   so they read as part of the chrome and not as a notice. */
.ab-chips{display:inline-flex;align-items:center;gap:var(--sp-2);flex:0 0 auto}
.ab-chip{
  display:inline-flex;align-items:center;gap:6px;
  padding:6px 10px;border:1px solid var(--line);border-radius:999px;
  background:var(--paper);color:var(--ink-700);
  font:500 var(--fs-micro)/1 var(--ff-mono);letter-spacing:.04em;white-space:nowrap;
}
/* The dot is the whole state: green when the edition is known, flat when the
   feed could not be reached. */
.ab-dot{
  width:7px;height:7px;border-radius:50%;flex:0 0 auto;
  background:var(--jade-700);
}
/* Offline is the one state that must not be mistaken for a colour with a
   meaning, so it is a literal grey rather than --flat: AsiaSource and CostNow
   both redefine --flat to an amber, and an amber dot on "Offline" reads as a
   warning about the data rather than about the connection. */
.ab-fresh[data-state="off"]{color:#7A8480}
.ab-fresh[data-state="off"] .ab-dot{background:#9AA3A0}
.ab-fresh[data-state="wait"]{color:#7A8480}
.ab-fresh[data-state="wait"] .ab-dot{background:#C3C9C6}

.ab-verwrap{position:relative;display:inline-flex}
button.ab-ver{
  cursor:pointer;font-weight:700;color:var(--jade-900);letter-spacing:.07em;
  transition:background-color .2s ease,color .2s ease,border-color .2s ease;
}
button.ab-ver:hover,button.ab-ver[aria-expanded="true"]{
  border-color:var(--ab-accent);background:var(--ab-accent);color:#fff;
}
button.ab-ver:focus-visible{outline:2px solid var(--ab-accent);outline-offset:2px}

/* Three lines and a build number. Same well-lit card as the language panel,
   anchored to the chip rather than to the bar so it cannot drift. */
.ab-whats{
  position:absolute;left:0;top:38px;z-index:320;width:264px;
  padding:var(--sp-3);border:1px solid var(--line);background:var(--card);
  box-shadow:0 16px 42px rgba(7,60,51,.14);
}
.ab-whats[hidden]{display:none}
.ab-whats h2{
  margin:0 0 var(--sp-2);color:var(--jade-900);
  font:700 var(--fs-small)/1.2 var(--ff-sans);letter-spacing:.02em;
}
.ab-whats ul{margin:0;padding:0;list-style:none}
.ab-whats li{
  padding:7px 0;border-top:1px solid var(--line-soft);
  color:var(--ink-900);font:500 var(--fs-micro)/1.5 var(--ff-sans);
}
.ab-whats li:first-child{border-top:0}
/* Same reason as the offline dot: --flat is an amber on two of the three
   desks, and a build number is not a warning. */
.ab-whats-v{
  margin:var(--sp-2) 0 0;padding-top:var(--sp-2);border-top:1px solid var(--line);
  color:#7A8480;font:500 var(--fs-micro)/1 var(--ff-mono);letter-spacing:.07em;
}

/* ---- controls ---- */
.ab-act{display:flex;align-items:center;gap:var(--sp-2);flex:0 0 auto;margin-left:auto}
.ab-act .bl-language{position:relative}
.ab-act .bl-language summary,
.ab-home{
  display:inline-flex;align-items:center;gap:6px;
  padding:7px var(--sp-3);border:1px solid var(--line);border-radius:999px;
  background:var(--paper);color:var(--jade-900);cursor:pointer;list-style:none;
  font:700 var(--fs-micro)/1 var(--ff-mono);letter-spacing:.07em;text-decoration:none;
  white-space:nowrap;transition:background-color .2s ease,color .2s ease,border-color .2s ease;
}
.ab-act summary::-webkit-details-marker{display:none}
.ab-act .bl-language summary:hover,.ab-home:hover{
  border-color:var(--ab-accent);background:var(--ab-accent);color:#fff;
}
.ab-home svg{width:14px;height:14px}
.ab-act .bl-language[open]>div{
  position:absolute;right:0;top:44px;z-index:70;width:210px;padding:var(--sp-2);
  border:1px solid var(--line);background:var(--paper);box-shadow:0 16px 42px rgba(7,60,51,.14);
}
.ab-act .bl-language select{
  width:100%;padding:var(--sp-2) 8px;border:1px solid var(--line);border-radius:0;
  background:var(--card);color:var(--jade-900);font:600 var(--fs-micro) var(--ff-sans);
}
.ab-act .bl-language small{display:block;margin-top:var(--sp-2);color:var(--flat);font-size:var(--fs-micro);line-height:1.45}

/* ---- the desk switch ----
   The three apps themselves, sitting in a shallow well: a dock, the way a
   phone shows the apps you keep. It replaced a "Desks" dropdown — three
   pictures are quicker to read than a word, and one click closer.

   The Terminal stays out of the desks. A launcher inside an app is a
   launcher you have to close before you can work. */
.ab-dock{
  display:inline-flex;align-items:center;gap:5px;
  padding:4px;border:1px solid var(--line);border-radius:13px;
  /* the well: a hairline recess, not a raised card */
  background:rgba(7,60,51,.05);
  box-shadow:inset 0 1px 2px rgba(7,60,51,.07);
}
.ab-app{
  position:relative;display:block;line-height:0;border-radius:9px;
  text-decoration:none;
  transition:transform .18s var(--ease,cubic-bezier(.22,.8,.28,1)),box-shadow .18s ease;
}
.ab-app-ico{
  display:block;width:36px;height:36px;border-radius:9px;
  border:1px solid rgba(7,60,51,.14);background:var(--paper);
  box-shadow:0 1px 2px rgba(7,60,51,.12);
  /* an icon you are not on reads as available, not as loud as the one you are on */
  filter:saturate(.82);opacity:.9;
  transition:filter .18s ease,opacity .18s ease;
}
.ab-app-name{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
.ab-app:hover{transform:translateY(-2px)}
.ab-app:hover .ab-app-ico{filter:none;opacity:1;box-shadow:0 5px 12px rgba(7,60,51,.20)}
.ab-app:focus-visible{outline:2px solid var(--ab-accent);outline-offset:3px}
.ab-app:active{transform:translateY(0) scale(.95)}
/* The app you are in: full colour, ringed in its own accent, and a dot under
   it — the same mark a phone puts under the app that is running. */
.ab-app[aria-current="page"] .ab-app-ico{
  filter:none;opacity:1;
  border-color:var(--ab-accent);
  box-shadow:0 0 0 2px var(--ab-accent),0 3px 9px rgba(7,60,51,.18);
}
.ab-app[aria-current="page"]::after{
  content:"";position:absolute;left:50%;bottom:-6px;width:4px;height:4px;
  margin-left:-2px;border-radius:50%;background:var(--ab-accent);
}
.ab-dock:has(.ab-app[aria-current="page"]){margin-bottom:0}
@media(prefers-reduced-motion:reduce){
  .ab-app,.ab-app-ico{transition:none}
  .ab-app:hover,.ab-app:active{transform:none}
}

/* ---- My Market is a dark app ----
   The other three desks are paper. This one is a dark screen, and a paper
   title bar on top of it reads as another site's chrome bolted on — plus the
   launch screen would flash white before a black page, which is the one moment
   the reader is watching most closely.
   The bar is built entirely out of tokens, so re-pointing the tokens ON THE BAR
   re-skins every part of it, popovers included, without a second copy of the
   rules. Scoped to this one app deliberately: these names are shared with the
   whole site, and shadowing them any wider would repaint the other three. */
#app-bar.ab[data-app="market"],
.ab-splash[data-app="market"]{
  --paper:#0C0F13;
  --card:#12171D;
  --line:#1D232B;
  --line-soft:#171D24;
  --ink-900:#DEE4E8;
  --ink-700:#B6C0C8;
  --ink-500:#8A96A0;
  --jade-900:#E7ECF0;   /* the "ink" the controls and the app name are set in */
  --jade-700:#4FB98A;   /* the freshness dot still has to read as go */
  --flat:#8A96A0;
}
/* The well and the icon plates are drawn with a dark-green alpha, which is
   invisible against a dark ground. On this app they are a light alpha, and the
   shadows go black instead of jade. */
#app-bar.ab[data-app="market"] .ab-dock{
  background:rgba(255,255,255,.05);
  box-shadow:inset 0 1px 2px rgba(0,0,0,.45);
}
#app-bar.ab[data-app="market"] .ab-app-ico{
  border-color:rgba(255,255,255,.14);
  box-shadow:0 1px 2px rgba(0,0,0,.5);
}
#app-bar.ab[data-app="market"] .ab-app:hover .ab-app-ico{box-shadow:0 5px 12px rgba(0,0,0,.6)}
#app-bar.ab[data-app="market"] .ab-ico{box-shadow:0 1px 2px rgba(0,0,0,.5)}
#app-bar.ab[data-app="market"] .ab-whats,
#app-bar.ab[data-app="market"] .ab-act .bl-language[open]>div{box-shadow:0 16px 42px rgba(0,0,0,.55)}
.ab-splash[data-app="market"] .ab-splash-ico{box-shadow:0 10px 30px rgba(0,0,0,.55)}
.ab-splash[data-app="market"] .ab-splash-bar{background:rgba(255,255,255,.14)}
/* These two are literal greys rather than tokens, so they need saying twice. */
.ab-splash[data-app="market"] .ab-splash-in em{color:#8A96A0}
.ab-splash[data-app="market"] .ab-splash-in small{color:#6E7A84}

/* ---- the text-size control, injected by text-size.js after the picker ---- */
.ab-act .bl-textsize{margin-left:0}

/* Under Large the bar is chrome, like every other header on this site: the
   reading scale is for the desk's tables, not for its title bar. */
html.ui-lg #app-bar .ab-id b,
html.ui-lg #app-bar .ab-status{font-size:12px}

/* ---- the band where the status strip had no room left ----
   .ab-status is the only flexible item in this row; everything to its right is
   fixed width. Between 900px and roughly 1250px what was left for it was under
   40px, so a three-chip strip rendered as "● S" and then the mask fade — a
   sliver that reads as a rendering fault rather than as a row that ran out of
   space. Measured on all five desks: the first chip alone needs 107px on
   AsiaSource and CostNow, 132px on ABrief and 229px on the Team Desk.

   The strip gets its own row instead of being cut down. Nothing is hidden and
   nothing is truncated, which is the difference between this and simply moving
   the existing display:none up a few hundred pixels.

   1600 rather than the 1250 where the sliver stops: the full strip only fits
   on one line from 1570px (AsiaSource and CostNow, three chips, 573px), so
   between those two figures the fade was still eating "16:14 Taipei" and
   cutting "WORKING" in half on a 1440 laptop. These chips are the reason the
   desks are worth opening; a second row is a cheaper price than a half-word. */
@media(max-width:1600px){
  .ab-in{flex-wrap:wrap;row-gap:var(--sp-2)}
  .ab-status{order:9;flex:1 1 100%;min-width:100%;-webkit-mask-image:none;mask-image:none}
  /* On its own row the strip has room for all of it, so the fade that said
     "there is more here" would now be lying. */
}
@media(max-width:900px){
  .ab-status{display:none}          /* the desks repeat their own status below */
  /* Between roughly 760 and 870 the row was already ~33px wider than the space
     it had, and the fourth app tile added another 41px on top — the controls
     ran off the end of the bar, and on a page with no other wide content that
     came out as horizontal scroll on the whole document.
     The word "Birdland" beside the home glyph is what pays for it: at this
     width the glyph is unambiguous, and the word stays in the accessible name
     rather than being deleted, so a screen reader still announces the link in
     full. The rest comes from closing the gaps a little. */
  .ab-home span{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;
  }
  .ab-home{padding-left:var(--sp-2);padding-right:var(--sp-2)}
  .ab-in{gap:var(--sp-2)}
  .ab-act{gap:6px}
  /* The bar cannot rely on the 700px collapse alone: between 700 and 900 the
     dock, home and both new chips already exceed a tablet row, so the
     freshness stamp folds to its dot here, not at phone width. */
  .ab-fresh-t{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;
  }
  .ab-fresh{padding:6px 8px;gap:0}
}
/* The freshness chip's text is the first thing that would overflow a phone
   bar, and it is the least of what the chip says — the dot already carries the
   state, and the full stamp stays in the chip's title. The version chip is two
   characters and stays as it is. */
@media(max-width:700px){
  .ab-fresh-t{
    position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip-path:inset(50%);white-space:nowrap;
  }
  .ab-fresh{padding:6px 8px;gap:0}
  /* On a phone the version chip sits close to the left edge, so a card hung
     off the chip runs off the screen in one direction or the other. Anchor it
     to the bar's row instead and let it start at the page margin: the card is
     tied to the bar, which is what it is about. */
  .ab-in{position:relative}
  .ab-verwrap{position:static}
  .ab-whats{
    left:0;right:auto;top:100%;margin-top:6px;
    width:min(300px,calc(100vw - 40px));
  }
}
@media(max-width:680px){
  .ab-in{width:calc(100% - 24px);flex-wrap:wrap;gap:var(--sp-2);padding:var(--sp-2) 0}
  .ab-act{width:100%;margin-left:0;justify-content:flex-start;flex-wrap:wrap;row-gap:8px}
  /* Once the actions row is its own row the picker sits at the page margin,
     and a 210px card right-aligned to an 89px summary starts 121px off the
     left edge of the screen. The popover opened, held its ten editions, and
     not one of them was on the phone: the reader could reach the control and
     not the choice. Hang it off the row instead of off the summary — the same
     move the version card makes two breakpoints up — so it always opens from
     the page margin, whichever end of the row the picker has drifted to. */
  .ab-act{position:relative}
  .ab-act .bl-language{position:static}
  .ab-act .bl-language[open]>div{
    left:0;right:auto;top:100%;margin-top:6px;
    width:min(210px,calc(100vw - 32px));
  }
  .ab-home span{display:none}
  .ab-home{padding:7px 10px}

  /* ---- the dock becomes a bottom tab bar ----
     A thumb rests near the bottom of a phone, not the top — the switch that
     matters most on a phone was living exactly where a thumb has to travel
     furthest to reach it. Pulled out of .ab-act into a fixed strip along the
     bottom edge, the way every native app on the reader's phone already
     arranges its own switcher; the row above keeps the identity chip, the
     freshness/version chips, and the language picker exactly as they were,
     just without the dock's width to share.
     position:fixed removes it from .ab-act's flex flow entirely, so nothing
     else in that row needs to change to make space for it. */
  .ab-dock{
    position:fixed;left:0;right:0;bottom:0;z-index:400;
    display:flex;align-items:stretch;justify-content:space-around;gap:0;
    padding:6px 4px calc(6px + env(safe-area-inset-bottom,0px));
    border:0;border-top:1px solid var(--line);border-radius:0;
    background:var(--paper);
    box-shadow:0 -6px 20px rgba(7,60,51,.10);
  }
  /* Each tile becomes a tab: icon over a now-visible label, evenly sharing
     the width, sized for a thumb rather than a cursor. The name was
     accessibility-only markup up to this point (clipped to 1px); this is the
     one context on the whole site where it is meant to be read, not just
     announced. */
  .ab-app{
    flex:1 1 0;display:flex;flex-direction:column;align-items:center;gap:2px;
    padding:4px 2px 2px;border-radius:10px;
  }
  .ab-app-ico{width:26px;height:26px;margin:0 auto}
  .ab-app-name{
    position:static;width:auto;height:auto;margin:0;padding:0;overflow:visible;
    clip-path:none;white-space:nowrap;
    font:600 9.5px/1.2 var(--ff-sans);color:var(--flat);letter-spacing:.01em;
  }
  .ab-app[aria-current="page"] .ab-app-name{color:var(--ab-accent)}
  /* The under-dot doesn't fit a bottom bar the way it fit a top one — there
     is nothing below it to point at — so the current tab is carried by the
     ring on its icon and its own coloured label instead. */
  .ab-app[aria-current="page"]::after{display:none}
  .ab-app:hover{transform:none}                    /* :hover on a touch tab bar is a stuck hover, not a real one */
  .ab-app:active{transform:scale(.95)}

  /* Room for the strip so it never sits over the desk's own last row of
     content. Height is measured, not guessed: 26px icon + ~12px label line +
     12px padding + the hairline border ≈ 56px, plus whatever inset the
     device's own home indicator needs. */
  html.ab-ready body{padding-bottom:calc(56px + env(safe-area-inset-bottom,0px))!important}

  /* My Market's dark-theme block (above) redefines .ab-dock as a translucent
     well meant to sit ON TOP of the bar's own solid --paper background. Fixed
     to the bottom of the viewport, this element no longer has that backdrop
     behind it — it needs to BE the solid surface, not a film over one. Same
     selector specificity as the rule it corrects, so it wins by simply
     coming later in the file, and the strip cannot go translucent over
     whatever content is scrolled beneath it. */
  #app-bar.ab[data-app="market"] .ab-dock{
    background:var(--paper);
    box-shadow:0 -6px 20px rgba(0,0,0,.4);
  }
}
@media(max-width:680px) and (prefers-reduced-motion:reduce){.ab-app:active{transform:none}}
@media print{#app-bar.ab{display:none!important}}

/* ---- the launch screen ----
   Fixed, and deliberately unrelated to the bar: the bar is in the document
   flow, this is a sheet over the viewport. It is paper, not a brand colour —
   the app opening should look like the app, so the only colour is the accent
   hairline under the name, the same one the bar wears.

   Removed by app-bar.js within 1.5s at the latest; the pointer-events drop on
   the way out means the last 400ms of the fade never eats a click. */
.ab-splash{
  position:fixed;inset:0;z-index:9000;
  display:flex;align-items:center;justify-content:center;
  background:var(--paper);
  opacity:1;transition:opacity .4s ease;
}
.ab-splash.is-out{opacity:0;pointer-events:none}
.ab-splash[data-app="news"]{--ab-accent:#1B5C9B}
.ab-splash[data-app="buyer"]{--ab-accent:#A95324}
.ab-splash[data-app="cost"]{--ab-accent:#146A4C}
.ab-splash[data-app="market"]{--ab-accent:#6D4A86}
.ab-splash[data-app="team"]{--ab-accent:#465561}
.ab-splash-in{
  display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);
  padding:0 24px;text-align:center;
}
.ab-splash-ico{
  width:88px;height:88px;border-radius:22px;
  border:1px solid var(--line);background:var(--paper);
  box-shadow:0 10px 30px rgba(7,60,51,.16);
}
.ab-splash-ico-line{padding:16px;border:0;color:#fff;background:var(--ab-accent);box-shadow:none}
.ab-splash-in b{
  display:block;padding-bottom:10px;border-bottom:3px solid var(--ab-accent);
  color:var(--jade-900);font:700 clamp(20px,4vw,26px)/1 var(--ff-sans);letter-spacing:.02em;
}
/* The one line that says what the app is for. Same sentence as the switch's
   tooltip and the Terminal's menu — see APPS[].desc in app-bar.js. */
.ab-splash-in em{
  display:block;max-width:30ch;margin-top:calc(var(--sp-2) * -1);
  color:#5D6B66;font:500 var(--fs-small)/1.35 var(--ff-sans);font-style:normal;
  text-wrap:balance;
}
.ab-splash-in small{
  color:#7A8480;font:500 var(--fs-micro)/1 var(--ff-mono);letter-spacing:.10em;
}
/* Runs for exactly as long as the splash's own ceiling (1.5s in app-bar.js),
   so it reads as the app opening rather than as a fake loading bar: when it
   reaches the end, the screen is already going. */
.ab-splash-bar{
  display:block;position:relative;overflow:hidden;
  width:140px;height:2px;border-radius:2px;background:rgba(7,60,51,.12);
}
.ab-splash-bar::after{
  content:"";position:absolute;top:0;bottom:0;left:0;width:0;
  border-radius:2px;background:var(--ab-accent);
  animation:ab-fill 1.5s cubic-bezier(.4,0,.2,1) forwards;
}
@keyframes ab-fill{from{width:0}to{width:100%}}
/* The icon arrives a beat before its name, the name before the line under it.
   Three frames, not an animation reel. */
.ab-splash-ico{animation:ab-rise .45s cubic-bezier(.2,.8,.2,1) both}
.ab-splash-in b{animation:ab-rise .45s .07s cubic-bezier(.2,.8,.2,1) both}
.ab-splash-in em{animation:ab-rise .45s .14s cubic-bezier(.2,.8,.2,1) both}
@keyframes ab-rise{
  from{opacity:0;transform:translateY(9px) scale(.96)}
  to{opacity:1;transform:none}
}
/* app-bar.js already declines to build the splash under reduced motion; this
   is the belt to that braces, for a cached page that predates the check. */
@media (prefers-reduced-motion:reduce){
  .ab-splash-ico,.ab-splash-in b,.ab-splash-in em,.ab-splash-bar::after{animation:none}
  .ab-splash-bar::after{width:100%}
}
@media print{.ab-splash{display:none!important}}

/* ---- the launch screen, but at the first byte ----

   The rules above dress a sheet that app-bar.js built. app-bar.js is deferred,
   so it ran AFTER the document had already painted: the reader got a flash of
   the bare desk, and only then the launch screen dropped on top of it. That is
   exactly backwards — it read as a web page putting on a costume.

   So the four desk pages now ship the sheet in their own markup, before any
   script. app-bar.css is render-blocking, so by the time <body> paints these
   rules are already in force and the first frame is the app's own colour with
   its own icon. app-bar.js ADOPTS that node rather than building one: it fills
   in the name, the purpose and the build (the three things it alone knows, from
   APPS[]), and from there the timing is the same as it ever was.

   Everything below exists because an element in the markup cannot check
   anything for itself. */

/* 1. Once per session. app-bar.js writes ab-splash-<key> into sessionStorage;
      a two-line inline script in each desk's <head> reads it back and stamps
      the root, because by the time a deferred script could do it the sheet has
      already been on screen for a frame. */
html.ab-seen .ab-splash{display:none!important}

/* 2. Reduced motion. The JS declined to BUILD the splash in this case, which
      the markup version cannot do; hiding it is the same answer. */
@media (prefers-reduced-motion:reduce){.ab-splash{display:none!important}}

/* 3. No script, no adoption, and therefore nothing to take the sheet away —
      a broken script would have left the desk covered forever. This is the
      dead man's handle: a zero-length animation that fires at 3s and pins the
      sheet gone. app-bar.js is finished by ~2.1s in the worst case, so in
      normal life this never runs. It is deliberately opacity+visibility rather
      than display, so it cannot fight the transition on the way out. */
.ab-splash{animation:ab-splash-safety 0s 3s forwards}
@keyframes ab-splash-safety{to{opacity:0;visibility:hidden;pointer-events:none}}

/* 4. The icon does not fade with the sheet — it flies to the seat it occupies
      in the title bar, so the reader sees that the icon on the launch screen
      and the icon in the bar are one object. app-bar.js measures both and
      writes the transform; this only has to get out of the animation's way,
      because a CSS animation on transform REPLACES a transform property rather
      than adding to it. */
.ab-splash-ico.is-flying{animation:none!important;transition:transform .42s cubic-bezier(.4,0,.2,1),opacity .42s ease}

/* There is deliberately no fifth rule staging the desk's own content behind
   the sheet. Hiding the body until a script says otherwise means a script that
   never arrives leaves a blank desk, and the sheet is a full-viewport cover
   already — the content under it cannot be seen early, so the only thing a
   staged reveal would buy is a second fade that nobody can distinguish from
   the sheet's own. The beats that survive are the ones that are visible: the
   icon flying to its seat, and the sheet lifting off the desk behind it. */

/* ---- desk-to-desk transitions ----

   Every dock tile and the Terminal are plain <a href> links: this is five
   documents, not one router. Cross-document View Transitions get a slide
   between them for the cost of this rule alone — the browser snapshots the
   outgoing document, navigates, and animates into the new one; a browser
   without support just navigates, so there is no feature check to write and
   nothing to fall back to by hand.

   This file is loaded only by the desk apps and the Team Desk, and the spec
   requires BOTH the outgoing and incoming document to opt in — so a jump from
   a desk to a facade page (index.html, about.html, none of which load this
   file) is correctly never transitioned, with no extra rule needed to say so. */
@view-transition{navigation:auto}

/* The browser's own default is a cross-fade of the whole page, which reads as
   a flicker on two documents that mostly agree (same app-bar, same paper).
   A horizontal slide says "you moved sideways to a sibling app" instead —
   closer to what tapping another icon on a phone actually looks like. */
::view-transition-old(root){animation:ab-slide-out .32s cubic-bezier(.4,0,.2,1) both}
::view-transition-new(root){animation:ab-slide-in .32s cubic-bezier(.4,0,.2,1) both}
@keyframes ab-slide-out{to{transform:translateX(-3%);opacity:0}}
@keyframes ab-slide-in{from{transform:translateX(3%);opacity:0}}

/* The spec does not read prefers-reduced-motion on its own — @view-transition
   still fires, it would just be the transform above running regardless. This
   is the documented way to keep the navigation (no flash of unstyled content
   between documents) while removing the motion itself. */
@media(prefers-reduced-motion:reduce){
  ::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){
    animation:none!important;
  }
}

/* ---- "an update is ready" ----
   Appears once, only in a tab that was already open when a newer deploy took
   over — see wireUpdateToast() in app-bar.js for why that can happen mid-
   session. Bottom-right and small: this is information, not an interruption,
   and the reader's own next reload would have picked the update up anyway. */
.ab-toast{
  position:fixed;right:var(--sp-4);bottom:var(--sp-4);z-index:9500;
  display:flex;align-items:center;gap:var(--sp-3);
  padding:var(--sp-2) var(--sp-3);border:1px solid var(--line);border-radius:10px;
  background:var(--card,var(--paper));color:var(--ink-900);
  box-shadow:0 12px 32px rgba(7,60,51,.18);
  font:500 var(--fs-small)/1.3 var(--ff-sans);
  opacity:0;transform:translateY(8px);transition:opacity .28s ease,transform .28s ease;
}
.ab-toast.is-in{opacity:1;transform:none}
.ab-toast button{
  appearance:none;border:1px solid var(--ab-accent,var(--jade-900));background:none;
  color:var(--ab-accent,var(--jade-900));border-radius:6px;
  padding:6px 12px;font:700 var(--fs-micro)/1 var(--ff-sans);cursor:pointer;white-space:nowrap;
}
.ab-toast button:hover{background:var(--ab-accent,var(--jade-900));color:var(--paper)}
@media(prefers-reduced-motion:reduce){.ab-toast{transition:none}}
@media print{.ab-toast{display:none!important}}

/* ---- the veil ahead of a language-switch reload ----
   See the click handler in app-bar.js for why this is still a reload rather
   than a re-render. The veil's job is narrower: make that reload read as the
   page changing language, not as a page going blank and coming back. It
   resolves to fully opaque BEFORE location.reload() is called, so whatever
   the browser's own cross-document view transition does with the outgoing
   frame, it is compositing this solid colour, not a half-torn-down page. */
.ab-lang-veil{
  position:fixed;inset:0;z-index:9800;background:var(--paper);
  opacity:0;transition:opacity .22s ease;
}
.ab-lang-veil.is-in{opacity:1}
