/* =========================================================================
   Airoid — the jungle floor.

   Bootstrap is themed through its own custom properties rather than by
   restyling thirty pages, so a screen nobody has touched still arrives in
   this world instead of the default one.
   ========================================================================= */

/* Self-hosted, no CDN. The nursery runs on a Pi behind poor wifi: a webfont
   that has to leave the building is a webfont that will not arrive. 47KB for
   all three. */
@font-face{
  font-family:"Arcade";
  src:url("../fonts/press-start-2p-latin.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Lilita";
  src:url("../fonts/lilita-one-latin.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Chivo";
  src:url("../fonts/chivo-latin.woff2") format("woff2");
  font-weight:100 900;font-style:normal;font-display:swap;
}

:root{
  /* The parts of the interface this stylesheet cannot reach: date pickers,
     select popup lists, scrollbar gutters, autofill. Without this they render
     in the browser's light chrome — a white calendar dropping out of a dark
     filter row is the one surface in the app with no jungle in it, and it is
     worst on Android, which is the primary device. */
  color-scheme:dark;

  /* The canopy floor: green, and actually green. The first pass sat around 12%
     saturation and read as dark grey with a hint of moss — a jungle has to be
     more sure of itself than that. Shade runs blue-green (it is sky light
     filtered through leaves) and light runs yellow-green, which is what makes
     the two ends read as depth rather than as two tints of one colour.
     Text contrast has room: bone on the brightest panel is still 9:1. */
  /* Day jungle, not night. The whole ramp moves up: light comes through the
     canopy rather than a torch being carried under it. Checked rather than
     eyeballed — bone on the panel face is 8.5:1 and on the ground 11:1, so the
     brightness costs nothing in legibility. The one thing it did cost was the
     old secondary grey, which fell to 4.2:1 and had to lighten with it. */
  /* The ground lifts, the panel does not. A finish review measured the shipped
     field varying by under 5 sRGB units top to bottom — the day had been moved
     in these comments and not in the render. Raising the whole ramp was the
     obvious answer and it is not available: it drops muted text on a panel to
     4.4:1, under the floor. So the ground moves and the surfaces above it stay,
     which brightens the field and narrows the panel-to-ground step from 1.9x to
     1.5x. That step is carried by a 2px border and a hard shadow anyway, which
     is how this world separates things — not by fill. */
  --canopy-950:#0B2E17;   /* deepest shade, blue-green */
  --canopy-900:#15421F;   /* the ground — was #123A1D at 3.2% luminance */
  --canopy-850:#1A4E25;
  --canopy-800:#1A5126;   /* panel face */
  --canopy-700:#22662F;   /* raised, nested */
  --canopy-600:#2C7F3A;   /* borders */
  --canopy-500:#3A9F49;   /* hover, structure */

  /* Five families. Jungle is not one colour — it is green with things living
     in it, and the accents should look like the things. */
  --leaf:#45D96B;      /* alive, growing, done */
  --lime:#B6FF3D;      /* the highlight — active, primary, now */
  --lime-ink:#06210A;  /* text on lime */
  --sun:#FFD029;       /* wants you today */
  --sun-ink:#2A1D00;
  --clay:#FF5C33;      /* overdue, wrong, stop — as a fill, a border, or a
                          headline, where it is a mass big enough to carry it */
  --clay-ink:#2C0A00;
  /* The same red, lightened until it can be read at body size. Full clay is
     3.0:1 on a panel, and .text-danger is what every validation message in
     every form uses — an error nobody can read is worse than no error. This
     one is 4.8:1. Use it for any clay that is a word rather than a shape. */
  --clay-text:#FFA183;
  --orchid:#FF5FBF;    /* delight only — never state, never structure */
  --parakeet:#2FD9D2;  /* the second bird: rare, and only ever one per screen */

  --bone:#F4FAF0;
  /* Lightened with the ground: at #9FB49C this was 4.2:1 on the new panel face,
     under the floor. Now 6.4:1. Still tinted from the ground, never grey. */
  --bone-dim:#C8D9C2;

  /* Pixels: hard edges, no radii, no soft shadows on chrome. */
  --edge:2px;
  --px:4px;            /* the grid everything snaps to */

  /* ---- Three faces, and each one has a job -----------------------------
     Press Start 2P is drawn on an 8px grid. At any size that is not a whole
     multiple of 8 an authored pixel lands across a fraction of a device pixel,
     the renderer smears some stems and not others, and the letterforms come out
     visibly malformed. Every arcade size in this file used to be a rem fraction
     — .6rem, .68rem, .72rem — and the root is 15px on a phone and 16px on a
     desktop, so not one of them landed on 8 and none of them landed the same
     way on both devices.

     So the arcade face is now display only, always at a whole multiple of 8px,
     never in rem. It is also a 5x7 pixel face, which is hard to read at small
     sizes however crisply it is drawn — the chrome labels it used to set are
     more legible in Lilita and lose nothing, because the 8-bit character in
     this world is carried by the sprites, the hard edges and the palette, not
     by setting a table header in a pixel font. */
  --face-arcade:"Arcade",ui-monospace,monospace;   /* display only, 8px steps */
  --face-display:"Lilita",ui-sans-serif,system-ui,sans-serif;  /* chrome labels */
  --face-text:"Chivo",ui-sans-serif,system-ui,-apple-system,sans-serif;

  /* ---- Bootstrap, retuned ---- */
  --bs-body-bg:var(--canopy-900);
  --bs-body-color:var(--bone);
  --bs-body-font-family:var(--face-text);
  --bs-body-font-size:1rem;
  --bs-body-line-height:1.55;
  --bs-emphasis-color:var(--bone);
  --bs-secondary-color:var(--bone-dim);
  --bs-tertiary-color:var(--bone-dim);
  --bs-border-color:var(--canopy-600);
  --bs-border-radius:0;
  /* Bootstrap 5.3 resolves a{color:rgba(var(--bs-link-color-rgb), …)} — the
     non-rgb variable is not in that path at all. Setting only --bs-link-color
     did nothing, and every link in the application had been rendering
     #0d6efd: lot codes, "catalogue", "Review stock →", the login page. About
     1.9:1 on a panel, in a colour that exists nowhere in this world.
     The triples must be kept in step with the tokens by hand; there is no
     rgb() indirection that survives a var() inside rgba(). */
  --bs-link-color:var(--lime);
  --bs-link-color-rgb:182,255,61;      /* --lime */
  --bs-link-hover-color:#C8FF7E;
  --bs-link-hover-color-rgb:200,255,126;
  --bs-heading-color:var(--bone);
  --bs-primary:var(--lime);
  --bs-primary-rgb:182,255,61;         /* --lime, was the night #A6F04B */
  --bs-danger:var(--clay);
  --bs-danger-rgb:255,92,51;           /* --clay, was the night #E4572E */
  --bs-warning:var(--sun);
  --bs-warning-rgb:255,208,41;         /* --sun, was the night #FFC53D */
  --bs-success:var(--leaf);
  --bs-success-rgb:69,217,107;         /* --leaf, was the night #3FBF5F */
  --bs-info:var(--canopy-600);
  --bs-info-rgb:44,127,58;             /* --canopy-600, was the night #234A31 */
  --bs-secondary-bg:var(--canopy-800);
  --bs-tertiary-bg:var(--canopy-850);
  --bs-code-color:var(--lime);
}

html{font-size:15px;position:relative;min-height:100%}
@media (min-width:768px){html{font-size:16px}}

body{
  margin:0;
  background-color:var(--canopy-900);
  /* Depth in bands rather than one flat field: light breaking through the
     canopy from the upper left, a warm pool at the floor. Daylight, not
     moonlight — the field used to bottom out at #0A2712, which is very nearly
     black and made every panel look like it was lit by a torch. The floor is
     still the darkest band, because in a jungle it is, but it is now a shade
     rather than an absence.

     Plus scanlines — one dark line every 3px, the cheapest possible signal
     that this is a screen from a machine that had them. Lighter than the night
     pass at .16, which banded visibly against the brighter ground. Kept off
     text; the lines sit on the ground and content paints over them. */
  background-image:
    repeating-linear-gradient(180deg, rgba(0,0,0,.11) 0 1px, transparent 1px 3px),
    /* One pool at the floor, kept. The three soft radials that used to sit here
       were each below perceptual threshold on a ground this dark — declared
       depth that never arrived. The shaft that replaces them is a real element
       with a hard edge; see .shaft below. A blurred radial was never this
       world's handwriting anyway. */
    radial-gradient(1200px 700px at 55% 118%, rgba(69,217,107,.14), transparent 62%),
    linear-gradient(180deg, var(--canopy-900) 0%, var(--canopy-850) 44%, #12381C 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E");
  /* One entry per layer above, in order. Two radials went; this list has to
     shrink with them or the noise texture stops scrolling. */
  background-attachment:fixed,fixed,fixed,scroll;
  min-height:100vh;
}

/* ---- Foliage -----------------------------------------------------------
   Real leaves, and each one its own element so it can move on its own. The
   previous pass painted them as background images, which meant the whole
   canopy could only ever move as one sheet — and a jungle that sways in
   lockstep is a curtain.

   Masked rather than embedded, so the colour comes from CSS: near leaves take
   the deep shade, far ones the canopy light, which is what builds depth. Held
   at low contrast on purpose — "too busy" is the stated failure of this
   interface, and scenery that competes with a table has stopped being scenery.
*/
/* Behind everything, at -1, rather than at 0 with the whole application
   lifted above it. Lifting .shell to z-index:1 was the obvious way to keep
   content off the scenery and it was wrong: it made .shell a stacking context,
   which trapped the rail's z-index:1040 inside a box worth 1. On a phone the
   scrim is a sibling of .shell at 1035, so it painted over the entire
   application including the open menu — the drawer looked dimmed and every tap
   landed on the scrim instead of the link under it.

   -1 needs no counterpart. The body's background paints on the canvas, this
   paints above it, and in-flow content paints above this without being told to.
   Nothing else in the app has to know the canopy exists. */
.canopy{position:fixed;inset:0;pointer-events:none;z-index:-1;overflow:hidden}

.frond{
  position:absolute;display:block;
  background-color:currentColor;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-size:contain;mask-size:contain;
  -webkit-mask-position:center;mask-position:center;
  /* Hanging leaves pivot where the petiole meets the branch, not at their
     middle — a leaf that rotates about its centre reads as a spinning sticker. */
  transform-origin:50% 0;
  will-change:transform;
}
.frond-up{transform-origin:50% 100%}

/* ---- The sun shaft ------------------------------------------------------
   The contract promises light breaking through the canopy. Three blurred
   radials were doing that job and none of them was visible: a review measured
   the whole field varying by under 5 sRGB units.

   A wedge with a hard clip instead. Two reasons and they agree — it is
   actually visible, and a soft airbrushed glow was the one thing on this page
   that did not belong to a machine with a pixel grid.

   Two nested wedges, flat-filled, not a repeating band. The first attempt used
   repeating-linear-gradient and shipped a barber-pole: regular stripes across
   the whole lower page, louder than anything on it and worse than the flat
   field it replaced. Light does not repeat. A pixel-art shaft is two or three
   steps of brightness with hard edges between them — the wide one, and a
   narrower brighter one inside it.
*/
.shaft{
  position:absolute;top:-6%;left:-6%;width:64%;height:118%;
  clip-path:polygon(12% 0%, 30% 0%, 78% 100%, 44% 100%);
  background-color:rgba(214,255,150,.038);
  pointer-events:none;
}
.shaft::after{
  content:"";position:absolute;inset:0;
  clip-path:polygon(17% 0%, 25% 0%, 62% 100%, 51% 100%);
  background-color:rgba(214,255,150,.05);
}
@media (max-width:900px){
  /* Steeper and narrower. On a phone a wedge this wide is just a lighter half. */
  .shaft{width:110%;left:-24%;clip-path:polygon(20% 0%, 34% 0%, 74% 100%, 56% 100%)}
  .shaft::after{clip-path:polygon(24% 0%, 30% 0%, 66% 100%, 60% 100%)}
}

.f-monstera{-webkit-mask-image:url("../img/monstera.svg");mask-image:url("../img/monstera.svg")}
.f-philo{-webkit-mask-image:url("../img/philodendron.svg");mask-image:url("../img/philodendron.svg")}
.f-fern{-webkit-mask-image:url("../img/fern.svg");mask-image:url("../img/fern.svg")}

/* Every size below is a whole multiple of the drawing's own grid — monstera is
   64x84, philodendron 56x76, fern 56x104. It was arbitrary before, so the same
   leaf shipped at 260px in one place and 150px in another and neither landed on
   a whole pixel. In a world that polices its sprites to 1:1, the scenery does
   not get to be the exception.

   Opacities are up across the board. A review found the fenestrations and the
   pinnae — the whole reason these are drawings rather than blobs — erased by
   the values they were held at. Scenery must not compete with a table, but it
   has to be legible as the thing it is, or it is just texture. */

/* Canopy, hanging in from the top. Far leaves are lighter and hazier. */
.f1{top:-70px;left:-40px;width:192px;height:252px;color:var(--leaf);opacity:.30}      /* monstera 3x */
.f2{top:-90px;left:150px;width:168px;height:228px;color:var(--canopy-500);opacity:.40} /* philo 3x */
.f3{top:-60px;right:-50px;width:256px;height:336px;color:var(--leaf);opacity:.27}     /* monstera 4x */
.f4{top:-40px;right:190px;width:112px;height:208px;color:var(--lime);opacity:.18}     /* fern 2x */
.f5{top:-30px;left:44%;width:112px;height:152px;color:var(--canopy-500);opacity:.32}  /* philo 2x */

/* Understorey, rising from the floor. Nearer, so darker and more solid.
   Their own deep shade rather than --canopy-950: with the ground lifted, the
   token sat 8 sRGB units from the field behind them and they read as smudges
   rather than as leaves. This does not move when the ground does. */
.f6{bottom:-60px;left:-30px;width:192px;height:252px;color:#04180A;opacity:.62}       /* monstera 3x */
.f7{bottom:-80px;right:-40px;width:224px;height:304px;color:#04180A;opacity:.58}      /* philo 4x */
.f8{bottom:-50px;left:30%;width:112px;height:208px;color:#04180A;opacity:.48}         /* fern 2x */

/* Everything the app draws sits above the scenery. */
/* No z-index on .shell, deliberately. The toggle, the scrim and the offline bar
   each set their own where they are positioned; the rail sits at 1040 in the
   root stacking context, above the scrim's 1035, which is the whole point of
   the number and only true while nothing wraps it in a context of its own. */
.rail{z-index:1040}

@media (max-width:900px){
  /* Fewer and smaller on a phone. The same canopy at the same scale would be
     standing in front of the work rather than behind it. */
  .f2,.f4,.f5,.f8{display:none}
  .f1{width:128px;height:168px;top:-40px;left:-30px}   /* monstera 2x */
  .f3{width:128px;height:168px;top:-40px;right:-40px}  /* monstera 2x */
  .f6{width:128px;height:168px;bottom:-30px;left:-30px}/* monstera 2x */
  .f7{width:112px;height:152px;bottom:-40px;right:-30px} /* philo 2x */
}

/* ---- Type: three sizes, and they are properly different ------------------
   The old world declared a display scale and then every page wrote h1.h3 over
   it, so nothing was ever big and the whole app read flat. Here the page title
   is the arcade face at a size nothing else approaches, and it is not
   overridable by a utility class. */
h1,.h1{
  font-family:var(--face-arcade);
  /* 16px, stepping to 24px where there is room. Whole multiples of 8, in px,
     not rem — a clamp() cannot land on the grid this face needs, and rem would
     land differently on a phone and a desktop. */
  font-size:16px;
  line-height:1.35;letter-spacing:0;
  color:var(--bone);
  margin:0;
  text-shadow:0 var(--px) 0 rgba(0,0,0,.55);
}
/* Beats Bootstrap's .h3/.h4 utilities, which 49 pages still carry.
   The family has to be re-stated here, not just the size. `h1` is one element
   selector (0,0,1); the `.h3` in the h2/h3/h4 rule below is a class (0,1,0) and
   wins — so every page title in the application rendered in Lilita instead of
   the arcade face, from the day this world shipped. The size was re-specified
   and the family was not, which is exactly why it looked deliberate. */
h1.h3,h1.h4,h1.h5,h1.h6{font-family:var(--face-arcade);font-size:16px;letter-spacing:0}
@media (min-width:768px){
  h1,.h1,h1.h3,h1.h4,h1.h5,h1.h6{font-size:24px}
}

h2,h3,h4,.h2,.h3{
  font-family:"Lilita",ui-sans-serif,system-ui,sans-serif;
  font-weight:400;letter-spacing:.015em;color:var(--bone);
}
h2{font-size:1.5rem}
h3{font-size:1.25rem}
/* Section headings inside panels: the app writes h2.h6.text-uppercase 56 times.
   Give that pattern a real voice rather than a muted whisper. */
h2.h6,h3.h6{
  font-family:var(--face-display);
  font-size:.86rem;letter-spacing:.06em;line-height:1.6;
  color:var(--lime);text-transform:uppercase;
}
h2,h3{margin-top:2.1rem;margin-bottom:.85rem}
.card h2:first-child,.card h3:first-child,
.panel h2:first-child,.panel h3:first-child{margin-top:0}

main{padding-top:1.6rem;padding-bottom:2rem}

.pagehead{
  display:flex;flex-wrap:wrap;gap:.75rem 1.25rem;
  align-items:center;justify-content:space-between;margin-bottom:1.1rem;
}
.pagehead h1{margin:0}
@media (max-width:600px){
  .pagehead{flex-direction:column;align-items:stretch}
  .pagehead .btn{width:100%}
}

.lede{max-width:68ch}
.card[id],.panel[id]{scroll-margin-top:1.5rem}

/* Data is read as quantity: line the digits up. */
td,th,.qty,.tabular,time,.font-monospace{font-variant-numeric:tabular-nums}

/* ---- The one figure that never shrinks ----------------------------------
   Borrowed discipline: a single number at a scale nothing else on the page
   approaches. For counts that decide whether today is calm or not. */
.headline{
  font-family:var(--face-arcade);
  /* Stepped, not fluid. A clamp() sweeps continuously through every fractional
     size between its bounds, which is the one thing this face cannot survive. */
  font-size:40px;
  line-height:1;color:var(--lime);
  text-shadow:0 var(--px) 0 rgba(0,0,0,.6);
  display:block;
}
@media (min-width:768px){.headline{font-size:56px}}
@media (min-width:1200px){.headline{font-size:72px}}
.headline-quiet{color:var(--bone)}
.headline-late{color:var(--clay)}
.headline-soon{color:var(--sun)}

/* ---- Today: the first thing anyone sees ---------------------------------
   The home page used to open with four stat cards of equal weight, which is
   the arrangement this world refuses: four numbers the same size say nothing
   about which one matters, and not one of them was work you could start.

   One figure instead, larger here than anywhere else in the app, answering
   the only question worth asking on opening it — is today calm or not.
*/
.today{padding:.5rem 0 1.6rem}
.today .headline{
  /* Bigger than the general headline scale. This is the one place a number is
     allowed to be the size of the page. Still on the 8px grid — letter-spacing
     is zero because a fractional em tracking pushes each glyph onto a
     half-pixel and undoes the whole point of snapping the size. */
  font-size:56px;
  letter-spacing:0;
}
@media (min-width:768px){.today .headline{font-size:96px}}
@media (min-width:1200px){.today .headline{font-size:128px}}

/* Except when it is not a number. The borrowed discipline is one *figure* that
   never shrinks; a word at the same size is just shouting, and "All clear" set
   across half a monitor read as a joke rather than as good news. Still the
   largest thing on the page, and no longer the size of the page. */
.today .headline-word{font-size:32px}
@media (min-width:768px){.today .headline-word{font-size:48px}}
.headline-caption{
  font-family:var(--face-display);
  font-size:.82rem;letter-spacing:.07em;text-transform:uppercase;
  color:var(--bone-dim);margin:.9rem 0 0;
}

/* A heading with the rest of the line drawn out beside it — a rule the eye
   follows rather than a label floating over a table. Pixel-thick, like
   everything else that is a line here. */
.section-rule{
  display:flex;align-items:center;gap:.9rem;
  font-family:var(--face-display);
  font-size:.88rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--bone-dim);margin:2.2rem 0 .9rem;
}
.section-rule::after{
  content:"";flex:1 1 auto;height:var(--edge);
  background-color:var(--canopy-600);
}

/* A sentence you can act on, not a number in a box.
   The whole 2px edge carries the state, which is how this world already says
   it — the offline bar, the chips and the focus ring all state themselves in
   one hard pixel rule. A thick accent stripe down one side would be a
   different interface's handwriting. */
.notice{
  margin:1.4rem 0;padding:.85rem 1rem;
  background-color:var(--canopy-800);
  border:var(--edge) solid var(--canopy-600);
  box-shadow:0 var(--px) 0 0 var(--canopy-950);
}
.notice-warn{border-color:var(--sun)}
.notice-late{border-color:var(--clay)}
.notice strong{color:var(--sun);font-variant-numeric:tabular-nums}
.notice-late strong{color:var(--clay-text)}

/* ---- The ledger ---------------------------------------------------------
   One figure that leads and four that support it, rather than five of equal
   weight — the same discipline as the headline, one step down the page. The
   supporting rows are a description list because that is what they are: a
   label and its value, not a table of one row.
*/
.ledger{
  display:flex;flex-wrap:wrap;gap:1.4rem 3rem;align-items:flex-end;
  padding:1.1rem 0 .3rem;
}
.ledger-lead{display:flex;flex-direction:column;gap:.35rem}
.ledger-figure{
  font-family:var(--face-arcade);
  font-size:24px;
  line-height:1;color:var(--lime);
  text-shadow:0 var(--px) 0 rgba(0,0,0,.6);
  font-variant-numeric:tabular-nums;
}
@media (min-width:768px){.ledger-figure{font-size:40px}}
/* A loss is not a smaller profit. */
.ledger-figure.is-down{color:var(--clay-text)}
.ledger-label{
  font-family:var(--face-display);
  font-size:.8rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--bone-dim);
}
.ledger-margin{color:var(--sun);margin-left:.5rem}

.ledger-rows{display:flex;flex-wrap:wrap;gap:.9rem 2.4rem;margin:0}
.ledger-rows>div{display:flex;flex-direction:column;gap:.15rem}
.ledger-rows dt{
  font-size:.74rem;letter-spacing:.03em;text-transform:uppercase;
  color:var(--bone-dim);font-weight:400;
}
.ledger-rows dd{
  margin:0;font-size:1.15rem;color:var(--bone);
  font-variant-numeric:tabular-nums;
}
@media (max-width:575.98px){
  /* Two columns rather than one long list: these are four short numbers and a
     column of them would push the stock figures off the screen. */
  .ledger-rows{display:grid;grid-template-columns:1fr 1fr;gap:.9rem 1rem;width:100%}
}

/* True, useful, and not urgent. One line at the foot rather than a quarter of
   the first viewport. */
.figures{
  display:flex;flex-wrap:wrap;gap:.2rem 0;
  margin-top:2.4rem;color:var(--bone-dim);font-size:.92rem;
}
.figures .qty{color:var(--bone);font-weight:600}
.figures span+span::before{
  content:"·";margin:0 .7rem;color:var(--canopy-500);
}

/* ---- Browser surfaces: the parts nobody draws still carry the design ---- */
::selection{background:var(--lime);color:var(--lime-ink)}
:focus-visible{outline:var(--edge) solid var(--lime);outline-offset:2px}
.btn:focus,.btn:active:focus,.form-control:focus,.form-check-input:focus,
.form-select:focus{box-shadow:0 0 0 3px rgba(182,255,61,.35)}
*{scrollbar-color:var(--canopy-500) var(--canopy-950);scrollbar-width:thin}
*::-webkit-scrollbar{width:12px;height:12px}
*::-webkit-scrollbar-track{background:var(--canopy-950)}
*::-webkit-scrollbar-thumb{background:var(--canopy-500);border:3px solid var(--canopy-950)}
*::-webkit-scrollbar-thumb:hover{background:var(--leaf)}
a{text-underline-offset:.18em}
input,textarea,select{caret-color:var(--lime)}

/* ---- The panel: a cut block of ground, not a floating card -------------- */
.card,.panel,.list-group{
  /* Light falls from above: the top of a block catches the canopy, the bottom
     sits in shade. Flat fills were most of why the first pass read muted —
     twenty identical rectangles of one colour is a wall, not a place. */
  background-image:linear-gradient(180deg, var(--canopy-700) 0%, var(--canopy-800) 42%);
  background-color:var(--canopy-800);
  border:var(--edge) solid var(--canopy-600);
  border-radius:0;
  /* Pixel depth: a hard step, then a real shadow underneath it. */
  box-shadow:inset 0 var(--edge) 0 0 rgba(182,255,61,.10),
             0 var(--px) 0 0 var(--canopy-950),
             0 10px 22px -10px rgba(0,0,0,.75);
}
.panel{padding:1.15rem 1.25rem}
@media (max-width:820px){.panel{padding:.85rem .9rem}}
.card .card,.panel .panel,.card .list-group,.panel .list-group{
  background-color:var(--canopy-700);box-shadow:none;border-color:var(--canopy-600);
}
.card-header,.panel > header{
  background-color:var(--canopy-700);
  border-bottom:var(--edge) solid var(--canopy-600);
  color:var(--bone);font-family:"Lilita",sans-serif;letter-spacing:.02em;
}
.card-footer{background-color:var(--canopy-700);border-top:var(--edge) solid var(--canopy-600)}
.list-group-item{background-color:transparent;color:var(--bone);border-color:var(--canopy-700)}
.list-group-item-action:hover{background-color:var(--canopy-700);color:var(--bone)}

/* ---- Controls: pixel blocks that depress ------------------------------- */
.btn{
  font-family:var(--face-display);
  font-size:.9rem;letter-spacing:.04em;text-transform:uppercase;
  border-width:var(--edge);border-radius:0;padding:.7rem .95rem;
  box-shadow:0 var(--px) 0 0 rgba(0,0,0,.55);
  transition:transform .06s steps(2),box-shadow .06s steps(2),background-color .12s;
}
.btn:active{transform:translateY(var(--px));box-shadow:0 0 0 0 rgba(0,0,0,.55)}
.btn-primary{background-color:var(--lime);border-color:var(--lime);color:var(--lime-ink)}
.btn-primary:hover,.btn-primary:focus{background-color:#C8FF7E;border-color:#C8FF7E;color:var(--lime-ink)}
.btn-secondary,.btn-outline-secondary{
  background-color:var(--canopy-700);border-color:var(--canopy-500);color:var(--bone);
}
.btn-secondary:hover,.btn-outline-secondary:hover{
  background-color:var(--canopy-600);border-color:var(--leaf);color:var(--bone);
}
.btn-danger,.btn-outline-danger{background-color:var(--clay);border-color:var(--clay);color:var(--clay-ink)}
.btn-outline-danger{background-color:transparent;color:var(--clay-text);border-color:var(--clay)}
.btn-outline-danger:hover{background-color:var(--clay);color:var(--clay-ink)}
.btn-link{color:var(--lime);box-shadow:none;text-transform:none;font-family:"Chivo",sans-serif;font-size:.95rem}
.btn-link:active{transform:none}
.btn:disabled,.btn.disabled{opacity:.4;box-shadow:none}
.btn-sm{font-size:.52rem;padding:.55rem .7rem}

.form-control,.form-select,.form-check-input{
  background-color:var(--canopy-950);border:var(--edge) solid var(--canopy-600);
  color:var(--bone);border-radius:0;
}
.form-control:focus,.form-select:focus{
  background-color:var(--canopy-950);border-color:var(--leaf);color:var(--bone);
}
.form-control::placeholder{color:var(--bone-dim);opacity:1}
.form-check-input{border-radius:0}
.form-check-input:checked{background-color:var(--lime);border-color:var(--lime)}
.form-control.is-locked{background-color:var(--canopy-850);color:var(--bone-dim);cursor:default}
.form-label{color:var(--bone);font-weight:600}
.form-text{color:var(--bone-dim)}
.input-group-text{background-color:var(--canopy-700);border-color:var(--canopy-600);color:var(--bone-dim);border-radius:0}

/* ---- Filter rows -------------------------------------------------------
   Bootstrap's grid with align-items-end lines up the BOTTOM of each column, so
   a field carrying help text pushes its own label out of line. These align the
   label row and the control row instead. */
.filters{display:flex;flex-wrap:wrap;gap:.9rem 1rem;align-items:flex-start;margin-bottom:1.15rem}
.filters .field{display:flex;flex-direction:column;min-width:0;flex:0 1 auto}
.filters .field>.form-label{margin-bottom:.35rem}
.filters .field-grow{flex:1 1 15rem}
.filters .field-nolabel::before{content:"";display:block;height:1.55em;margin-bottom:.35rem}
.filters .form-text{margin-top:.3rem}
.filters .form-check{margin:0}
.filters .form-check+.form-check{margin-top:.3rem}
/* The fold is a phone affordance and nothing else. On a desktop the filters are
   one row across the top and cost nothing, so there is nothing to fold away. */
.filter-toggle{display:none}

@media (max-width:575.98px){
  .filters{flex-direction:column;align-items:stretch;gap:.8rem}
  .filters .field-nolabel::before{display:none}

  .filters.is-folded{display:none}

  .filter-toggle{
    display:flex;align-items:center;gap:.55rem;width:100%;
    font-family:var(--face-display);
    font-size:.88rem;letter-spacing:.05em;text-transform:uppercase;
    padding:.65rem .7rem;text-align:left;
    background-color:var(--canopy-700);color:var(--bone);
    border:var(--edge) solid var(--canopy-600);border-radius:0;
    box-shadow:0 var(--px) 0 0 var(--canopy-950);
  }
  .filter-toggle .filter-toggle-label{flex:1 1 auto}
  .filter-toggle[aria-expanded="true"] .sprite-caret{transform:rotate(90deg);opacity:1}
  /* The count sits at its natural size, not the arcade face's, or a two-digit
     number would be wider than the word beside it. */
  .filter-toggle .chip{font-size:.52rem}

  /* flex-basis follows the main axis, and the main axis just turned vertical.
     field-grow is `flex:1 1 15rem`, which is a 15rem *width* in a row and a
     15rem *height* here — plus flex-grow:1 to stretch taller still. Every
     search box on a phone was 240px of empty panel, on all ten pages that
     filter something. Column layout wants intrinsic height, nothing else. */
  .filters .field,.filters .field-grow{flex:0 0 auto}
}

/* ---- Tables: density is the material ------------------------------------
   Borrowed discipline: hairlines and tabular figures at full density, not rows
   padded out to look calm. */
.table{--bs-table-color:var(--bone);--bs-table-bg:transparent;
  --bs-table-border-color:var(--canopy-700);
  --bs-table-hover-bg:var(--canopy-700);--bs-table-hover-color:var(--bone);
  --bs-table-striped-bg:rgba(0,0,0,.16);--bs-table-striped-color:var(--bone)}
.table>thead{
  font-family:var(--face-display);
  letter-spacing:.05em;text-transform:uppercase;
  font-size:.78rem;color:var(--lime);
}
.table>thead th{border-bottom:var(--edge) solid var(--canopy-600);padding-bottom:.7rem;vertical-align:bottom}
.table>tbody>tr>*{padding-top:.5rem;padding-bottom:.5rem}
.col-fit{width:1%;white-space:nowrap}
.phone-only{display:none}
@media (max-width:820px){.phone-only{display:inline}}

/* ---- Sortable headers ---- */
.sort-link{display:inline-flex;align-items:center;gap:.3rem;color:inherit;text-decoration:none;white-space:nowrap}
.sort-link:hover,.sort-link:focus-visible,.sort-link.is-sorted{color:var(--bone)}
.sort-caret{width:11px;height:11px;flex:0 0 auto}
.sort-caret-idle{opacity:.35}
.sort-link:hover .sort-caret-idle,.sort-link:focus-visible .sort-caret-idle{opacity:.7}

/* ---- The same table, as a list, on a phone ---- */
@media (max-width:820px){
  .stack-rows>thead{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
    overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
  .stack-rows,.stack-rows>tbody,.stack-rows>tbody>tr{display:block;width:100%}
  .stack-rows>tbody>tr{padding:.75rem .1rem;border-bottom:var(--edge) solid var(--canopy-700)}
  .stack-rows>tbody>tr:last-child{border-bottom:0}
  .stack-rows>tbody>tr>td{display:block;border:0;padding:0}
  .stack-rows .cell-lead{font-size:1.04rem;line-height:1.3}
  .stack-rows .cell-sub{color:var(--bone-dim);font-size:.9rem;margin-top:.1rem}
  .stack-rows .cell-meta{display:inline;font-size:.85rem}
  .stack-rows .cell-meta+.cell-meta::before{content:"·";margin:0 .45rem;color:var(--canopy-500)}
  /* A cell with more than one line in it cannot join the inline run. Marking it
     block keeps it out, and stops the run's separator appearing to lead the
     line beneath it — which is what a stray "·" before the first fact was. */
  .stack-rows .cell-block{display:block;margin-top:.15rem}
  .stack-rows .cell-block+.cell-meta::before{content:none}
  .stack-rows .cell-act{width:auto;margin-top:.55rem;text-align:right}
  .stack-rows tbody tr:hover{background:transparent}
}

/* ---- State: a colour family each, never one accent doing every job ----- */
.alert{border-width:var(--edge);border-radius:0;color:var(--bone);background:var(--canopy-800)}
.alert-success{border-color:var(--leaf);background:linear-gradient(180deg,rgba(69,217,107,.14),transparent 70%),var(--canopy-800)}
.alert-warning{border-color:var(--sun);background:linear-gradient(180deg,rgba(255,208,41,.14),transparent 70%),var(--canopy-800)}
.alert-danger{border-color:var(--clay);background:linear-gradient(180deg,rgba(255,92,51,.14),transparent 70%),var(--canopy-800)}
.alert-info,.alert-secondary{border-color:var(--canopy-500)}

.badge{font-family:var(--face-display);font-size:.76rem;letter-spacing:.04em;border-radius:0;font-weight:400;padding:.35em .55em}
.text-bg-success,.bg-success{background-color:var(--leaf)!important;color:var(--lime-ink)!important}
.text-bg-warning,.bg-warning{background-color:var(--sun)!important;color:var(--sun-ink)!important}
.text-bg-danger,.bg-danger{background-color:var(--clay)!important;color:var(--clay-ink)!important}
.text-bg-primary,.bg-primary{background-color:var(--leaf)!important;color:var(--lime-ink)!important}
.text-bg-light,.text-bg-secondary,.bg-secondary{background-color:var(--canopy-700)!important;color:var(--bone)!important;box-shadow:inset 0 0 0 1px var(--canopy-500)}
.text-bg-dark,.bg-dark{background-color:var(--canopy-950)!important;color:var(--bone)!important}
.text-muted,.text-body-secondary{color:var(--bone-dim)!important}
.text-warning{color:var(--sun)!important}
.text-danger,.invalid-feedback,.validation-summary-errors{color:var(--clay-text)!important}
.border{border-color:var(--canopy-600)!important}
hr{border-color:var(--canopy-600);opacity:1}

.state{display:inline-flex;align-items:center;gap:.35rem;white-space:nowrap}
.state .tick{width:15px;height:15px;flex:0 0 auto;color:var(--leaf)}
.state-off{color:var(--bone-dim)}
.row-retired{color:var(--bone-dim)}
.row-retired em{font-style:italic}
.tag-quiet{font-family:var(--face-display);text-transform:uppercase;
  letter-spacing:.05em;font-size:.72rem;color:var(--bone-dim)}

.chip{
  display:inline-block;font-family:var(--face-display);text-transform:uppercase;
  letter-spacing:.05em;font-size:.76rem;line-height:1.4;
  padding:.3rem .45rem;border-radius:0;white-space:nowrap;
  background-color:var(--canopy-700);color:var(--bone);
  box-shadow:inset 0 0 0 1px var(--canopy-500);
}
.chip-live{background-color:var(--leaf);color:var(--lime-ink);box-shadow:none}
.chip-ready{background-color:var(--lime);color:var(--lime-ink);box-shadow:none}
.chip-warn{background-color:var(--sun);color:var(--sun-ink);box-shadow:none}
.chip-late{background-color:var(--clay);color:var(--clay-ink);box-shadow:none}
.chip-off{background-color:transparent;color:var(--bone-dim)}

.dropdown-menu{
  background-color:var(--canopy-800);border:var(--edge) solid var(--canopy-600);
  border-radius:0;box-shadow:0 var(--px) 0 0 var(--canopy-950),0 12px 28px -12px rgba(0,0,0,.8);
  --bs-dropdown-link-color:var(--bone);
  --bs-dropdown-link-hover-bg:var(--canopy-700);
  --bs-dropdown-link-hover-color:var(--bone);
  --bs-dropdown-link-active-bg:var(--leaf);
  --bs-dropdown-link-active-color:var(--lime-ink);
}

/* =========================================================================
   The rail. Navigation is somewhere you go, not something you look at.
   ========================================================================= */
.shell{display:flex;min-height:100vh}

.rail{
  position:fixed;left:0;top:0;bottom:0;z-index:1040;
  width:60px;
  display:flex;flex-direction:column;gap:2px;
  padding:8px 6px calc(8px + env(safe-area-inset-bottom));
  /* The dark edge of the clearing, with the canopy light catching its inner
     edge. A lit rule rather than a plain border is what stops a 60px black
     column reading as a browser chrome artefact. */
  background-image:linear-gradient(180deg, #061B0D 0%, var(--canopy-950) 60%);
  background-color:var(--canopy-950);
  border-right:var(--edge) solid var(--canopy-700);
  /* The open rail lies over the work rather than pushing it aside — that is
     what a pop-out is, and .work keeps a constant 60px margin so the page never
     reflows. But at 6px the shadow was invisible, so the overlap read as a
     layout fault instead of as something floating above the page. This is the
     line that makes it obvious the content is underneath, not broken. */
  box-shadow:inset -3px 0 0 -1px rgba(182,255,61,.16), 14px 0 40px -6px rgba(0,0,0,.75);
  /* Clipped across, scrollable down. It was overflow:hidden on both axes, which
     is right for the labels sliding in and wrong for everything else: on a
     phone the drawer is full height and six expanded groups do not fit, so the
     bottom of the menu was simply unreachable and dragging on it scrolled the
     page underneath instead. overscroll-behavior stops that chaining even when
     the rail has hit its own end. */
  overflow-x:hidden;
  overflow-y:auto;
  overscroll-behavior:contain;
  /* Stepped, not eased. Two reasons and they agree: this world does not tween —
     the buttons and the sprite idle are already on steps() — and a smooth width
     ease is the one animation here that costs a layout pass per frame. Three
     steps is three layouts instead of a dozen, and it reads like a menu
     snapping open rather than a drawer sighing. The phone rail below animates
     transform instead, which costs no layout at all.

     The reason a width animation is affordable at all here: the rail is fixed
     and .work carries a constant 60px margin, so the page never reflows when
     it opens — nothing outside this element depends on its width. `contain`
     states that to the engine rather than leaving it to infer it, holding the
     three layout passes inside the rail's own handful of nav items. */
  contain:layout paint;
  transition:width .12s steps(3);
}
/* Opens on intent — hover, keyboard focus, or the toggle. Shuts itself when
   intent ends, so the work gets the glass back without anyone tidying up. */
.rail:hover,.rail:focus-within,.rail.is-open{width:216px}

.rail .brand{
  display:flex;align-items:center;gap:10px;text-decoration:none;
  /* The mark takes the canopy's own green rather than the text colour, so the
     rail's top is a leaf and not a bullet. */
  color:var(--leaf);
  padding:6px 6px 14px;min-height:44px;
}
.rail .brand .plate{
  /* The brand keeps the arcade face — it is the one word in the chrome that is
     display type rather than a label, and 16px lands it on the grid. */
  font-family:var(--face-arcade);font-size:16px;letter-spacing:0;
  white-space:nowrap;opacity:0;transition:opacity .14s;
  /* The one place orchid appears. It was declared for delight and then never
     spent, which is how a palette ends up looking smaller than it is. */
  color:var(--orchid);
  text-shadow:0 2px 0 rgba(0,0,0,.6);
}
.rail:hover .plate,.rail:focus-within .plate,.rail.is-open .plate{opacity:1}

.rail .domain,.rail .sub{
  display:flex;align-items:center;gap:12px;
  padding:9px 8px;text-decoration:none;color:var(--bone-dim);
  border:var(--edge) solid transparent;white-space:nowrap;
  font-family:var(--face-display);font-size:.86rem;letter-spacing:.05em;
  text-transform:uppercase;background:none;width:100%;text-align:left;
}
.rail .sub{font-family:"Chivo",sans-serif;font-size:.85rem;text-transform:none;
  letter-spacing:0;padding-left:14px;color:var(--bone-dim)}
.rail .domain:hover,.rail .sub:hover,
.rail .domain:focus-visible,.rail .sub:focus-visible{color:var(--bone);background-color:var(--canopy-800)}
.rail .domain.active{color:var(--lime-ink);background-color:var(--lime);border-color:var(--lime)}
.rail .domain.active .sprite{color:var(--lime-ink)}

.rail .label{opacity:0;transition:opacity .14s}
.rail:hover .label,.rail:focus-within .label,.rail.is-open .label{opacity:1}

/* ---- Groups fold ---------------------------------------------------------
   The rail used to list every page in the application the moment it opened —
   six domains and fourteen destinations at once, which is not navigation, it
   is an index. Now only the domain you are in is unfolded. The rest are one
   caret away, and folded is the resting state, so the rail opens calm.

   The domain name and the caret are separate controls on one row. Two wants,
   two targets: "take me to Health" and "what is under Health".
*/
.rail .group{display:flex;flex-direction:column}
/* width:auto beats the 100% the shared .domain rule sets, which inside a flex
   row would push the caret off the edge of a 216px rail. */
.rail .domain-row{display:flex;align-items:center;gap:2px}
.rail .domain-row .domain{flex:1 1 auto;min-width:0;width:auto}

.rail .group-toggle{
  display:none;flex:0 0 auto;
  align-items:center;justify-content:center;
  width:34px;height:34px;padding:0;
  background:none;border:var(--edge) solid transparent;border-radius:0;
  color:var(--bone-dim);
}
/* Hover is guarded: a tap on a touchscreen leaves :hover stuck on whatever was
   last touched, so the caret you just used kept a lit border around it as
   though it were still selected. Focus-visible is not guarded — that one is
   supposed to persist. */
@media (hover:hover){
  .rail .group-toggle:hover{color:var(--bone);border-color:var(--canopy-500)}
}
.rail .group-toggle:focus-visible{color:var(--bone);border-color:var(--canopy-500)}
.rail .group.is-expanded .group-toggle .sprite-caret{transform:rotate(90deg);opacity:1}

/* Both the caret and the pages it controls only exist while the rail is open.
   At 60px there is no room for either, and a control you cannot see is one a
   keyboard should not land on. */
.rail .group .sub{display:none}
.rail:hover .group-toggle,
.rail:focus-within .group-toggle,
.rail.is-open .group-toggle{display:flex}
.rail:hover .group.is-expanded .sub,
.rail:focus-within .group.is-expanded .sub,
.rail.is-open .group.is-expanded .sub{display:flex}

/* Nothing in the rail may be squeezed. A flex column shrinks its children
   before it overflows, so without this the nav items compressed to fit and the
   rail never became scrollable — it just got progressively more cramped until
   the labels collided. Let it overflow; that is what the scrolling is for. */
.rail>*,.rail .group>*{flex:0 0 auto}

/* The one exception, and it is the reason the spacer exists: it takes up slack
   when the menu is short and collapses to nothing when it is long. */
.rail-spacer{flex:1 1 auto;min-height:.5rem}
.rail .who{
  font-family:"Chivo",sans-serif;font-size:.78rem;color:var(--bone-dim);
  padding:6px 8px;white-space:nowrap;text-decoration:none;
}

.work{flex:1 1 auto;min-width:0;margin-left:60px}

/* ---- Sprites: authored on a 24px grid, never smoothed ------------------
   Rendered at 24px, which is 1:1 with the grid they are drawn on. The rail
   used to show them at 26px for presence, and the 1.625x scale off a 16 grid
   was the whole reason nobody could tell a crate from a coin: at a fractional
   scale one authored pixel lands across one-and-a-bit device pixels, and
   crispEdges has to round it one way or the other. Presence comes from the
   drawing now. Any size here must stay a whole multiple of 24. */
.sprite{
  width:24px;height:24px;flex:0 0 auto;
  image-rendering:pixelated;shape-rendering:crispEdges;
}
/* The caret stays at 24px like everything else and is small because it is
   drawn small inside its grid — scaling it down would put it back on the
   fractional grid this whole block exists to get off. */
.sprite-caret{opacity:.6;transition:transform .1s steps(2)}

/* ---- Phone: the rail becomes a drawer under the thumb ------------------- */
.rail-toggle{
  /* Above the offline bar's 1060. The bar is anchored clear of it, but this is
     the only way into navigation on a phone and it does not get to depend on
     one other rule staying correct. */
  display:none;position:fixed;left:12px;bottom:12px;z-index:1070;
  width:52px;height:52px;padding:0;
  align-items:center;justify-content:center;
  background-color:var(--lime);color:var(--lime-ink);
  border:var(--edge) solid var(--lime);border-radius:0;
  box-shadow:0 var(--px) 0 0 rgba(0,0,0,.6);
}
.rail-scrim{display:none}

@media (max-width:900px){
  /* Nothing permanent on a phone: every pixel is work until asked otherwise.
     The toggle sits bottom-left, inside a thumb's reach, because this is used
     one-handed at the racks with the other hand full. */
  /* The menu button is fixed at bottom-left, over the drawer. Without this the
     rail's last two items — the signed-in name and Sign out — sat underneath a
     52px lime square, so on a phone there was no way to sign out at all. The
     padding is the button's 52px plus its 12px inset, plus room to breathe. */
  .rail{transform:translateX(-100%);width:216px;transition:transform .2s cubic-bezier(.16,1,.3,1);
        padding-bottom:calc(80px + env(safe-area-inset-bottom))}
  .rail.is-open{transform:translateX(0)}
  .rail:hover:not(.is-open){width:216px;transform:translateX(-100%)}
  .rail .plate,.rail .label{opacity:1}

  /* No `.rail .group .sub{display:flex}` here, deliberately.
     There used to be, and it predated the fold: a phone has no hover to open
     the rail with, so the drawer simply showed every page it had. Once the
     groups learned to fold, that line quietly outranked them — same specificity
     as the `display:none` above it and further down the file — so on a phone
     every group stayed open and the caret rotated over nothing.
     The unmediated pair does the work here too: folded by default, unfolded on
     .rail.is-open .group.is-expanded, which is exactly the drawer's state. */
  .work{margin-left:0}
  .rail-toggle{display:flex}
  .rail-scrim.is-open{display:block;position:fixed;inset:0;z-index:1035;background:rgba(0,0,0,.6)}

  /* The page holds still while the drawer is open. overscroll-behavior on the
     rail stops the chaining once it is scrollable; this covers the other case,
     where the menu is shorter than the screen and a drag on it has nothing of
     its own to move. Scoped to the phone: on a desktop the rail opens on hover
     and locking the page behind it would be hostile. */
  body.rail-open{overflow:hidden}
  body{padding-bottom:4.5rem}
}

/* ---- Offline state ---- */
.offline-bar{
  position:fixed;left:0;right:0;bottom:0;z-index:1060;
  display:none;padding:.6rem .9rem;
  gap:.75rem;align-items:flex-start;
  background-color:var(--canopy-800);border-top:var(--edge) solid var(--sun);
  color:var(--bone);font-size:.88rem;line-height:1.35;
}
.offline-bar.is-shown{display:flex}
.offline-bar.is-broken{border-top-color:var(--clay)}
.offline-bar .bar-text{flex:1 1 auto;min-width:0}

/* Dismissable, and big enough to hit with a thumb in a glasshouse. The bar
   reports a real fault, so this is only a dismissal of the message, not of the
   condition — see offline-queue.js: a different message earns the space back. */
.offline-bar .bar-close{
  flex:0 0 auto;display:flex;align-items:center;justify-content:center;
  width:36px;height:36px;margin:-.3rem -.3rem 0 0;padding:0;
  background:none;border:var(--edge) solid transparent;border-radius:0;
  color:var(--bone-dim);
}
.offline-bar .bar-close:hover,.offline-bar .bar-close:focus-visible{
  color:var(--bone);border-color:var(--canopy-500);
}

@media (max-width:900px){
  /* Clear of the menu button rather than over it. This used to be left:0 with
     76px of padding, which moved the text but left the bar's own body — and
     its 1060 stacking — lying across the toggle. On a phone that is the only
     way into navigation, so the bar made the app unusable the moment it
     appeared. Padding cannot uncover a button; only geometry can. */
  .offline-bar{left:76px}
}

/* =========================================================================
   Motion. One authored moment, plus the sprites' own idle.
   ========================================================================= */
@media (prefers-reduced-motion:no-preference){
  main > :first-child{animation:rise .34s cubic-bezier(.16,1,.3,1) both}
  @keyframes rise{from{opacity:.4;transform:translateY(6px)}to{opacity:1;transform:none}}

  /* The world's own breath: leaves in the rail drift, two frames, slowly.
     Stepped rather than eased — the chrome of this world does not tween. */
  .sprite-idle{animation:bob 1.6s steps(2,jump-none) infinite}
  @keyframes bob{from{transform:translateY(0)}to{transform:translateY(2px)}}

  /* The canopy moves in air, and air is the one thing here that is not
     pixellated — so this is the single eased motion in the build, and it is
     deliberate. Every frond gets its own period and delay, because leaves that
     share a phase read as one printed sheet rocking.

     Rotation only, about the petiole: transform composites, so eight moving
     elements cost no layout and no repaint. Two and a half degrees over eight
     seconds is beneath notice until you look at it, which is the intent. */
  .frond{animation:sway 9s ease-in-out infinite alternate}
  .f1{animation-duration:11s;animation-delay:-2s}
  .f2{animation-duration:8s; animation-delay:-5s}
  .f3{animation-duration:13s;animation-delay:-1s}
  .f4{animation-duration:7s; animation-delay:-4s}
  .f5{animation-duration:10s;animation-delay:-7s}
  .f6{animation-duration:12s;animation-delay:-3s}
  .f7{animation-duration:9s; animation-delay:-6s}
  .f8{animation-duration:14s;animation-delay:-8s}

  @keyframes sway{
    from{transform:rotate(-1.4deg)}
    to{transform:rotate(2.2deg)}
  }

  .btn,.rail .domain,.rail .sub,.card,.list-group-item-action{
    transition:background-color .14s,border-color .14s,color .14s;
  }
}

/* =========================================================================
   Print. A plant passport is a legal document that leaves this building on a
   label. It is never rendered in the world above: white stock, black ink, no
   ornament, no arcade lettering, nothing that costs toner or credibility.
   ========================================================================= */
/* ---- Print: the boundary the contract draws ------------------------------
   Plant passports and printed labels stay sober; a statutory record may not
   read as decoration. But the boundary is wider than the label page — the
   treatment register and the pest check register are the documents produced on
   request by an inspector, and they print from ordinary pages.

   The previous version of this block was not sufficient, and its failure mode
   was invisible ink: .text-muted resolves to --bone-dim (#C8D9C2), which was
   never overridden here, so every muted cell, every explanatory line and every
   "—" printed near-white on white paper. A record with holes in it is worse
   than no record.

   Belt and braces below: reset everything to black, then strip the ornament.
*/
@media print{
  /* Every colour token that carries text is forced to ink, at the root, so a
     surface added later inherits a printable colour instead of a jungle one. */
  :root{
    color-scheme:light;
    --bone:#000;--bone-dim:#000;
    --lime:#000;--lime-ink:#fff;--leaf:#000;
    --sun:#000;--sun-ink:#fff;--clay:#000;--clay-ink:#fff;--clay-text:#000;
    --orchid:#000;--parakeet:#000;
    --bs-body-color:#000;--bs-secondary-color:#000;--bs-tertiary-color:#000;
    --bs-emphasis-color:#000;--bs-heading-color:#000;--bs-code-color:#000;
  }

  body{background:#fff!important;color:#000!important;padding:0!important}
  body{background-image:none!important}

  /* The scenery, and everything that is a control rather than a record. The
     canopy is fixed-position, so without this it repeats on every printed page
     wherever background graphics are enabled. */
  .canopy,.rail,.rail-toggle,.rail-scrim,.offline-bar,.btn,.form-text,
  .filter-toggle,.filters,nav{display:none!important}
  .work{margin-left:0!important}

  .card,.panel,.list-group,.alert,.notice{
    background:#fff!important;background-image:none!important;color:#000!important;
    border:1px solid #000!important;box-shadow:none!important;
  }

  /* Muted, warning and danger text all resolved to pale tokens meant for a dark
     ground. On paper they were the invisible half of the record. */
  .text-muted,.text-body-secondary,.text-warning,.text-danger,
  .text-success,.text-info,.form-text,.chip-off{color:#000!important}

  /* The arcade face and its tracking, everywhere it appears. The old list
     stopped at headings and missed the money figures outright, so a printed
     tax-year page came out in Press Start 2P. */
  h1,h2,h3,h4,h5,h6,.plate,.table>thead,.chip,.badge,.tag-quiet,
  .headline,.headline-caption,.section-rule,.ledger-figure,.ledger-label,
  .ledger-margin,.filter-toggle-label{
    color:#000!important;font-family:ui-sans-serif,system-ui,sans-serif!important;
    text-transform:none!important;letter-spacing:normal!important;
    text-shadow:none!important;background:none!important;
  }
  .section-rule::after{background:#000!important}
  .headline{font-size:1.6rem!important}

  /* Chips carry meaning — "9 d late" has to survive the trip to paper — so they
     keep their outline and lose their fill rather than disappearing. */
  .chip{border:1px solid #000!important}

  .table{--bs-table-color:#000;--bs-table-border-color:#000}
  a{color:#000!important;text-decoration:none!important}
}
