/* ===========================================================================
   JACRA Backoffice - base
   ---------------------------------------------------------------------------
   Reset, document defaults, the heading scale, the native-shell behaviour every
   screen inherits, the shared entrance keyframes, and the global utilities
   (.ic, the focus ring, .sr-only, .stagger, reduced motion).

   Nothing here knows about the app shell or any individual component. If a rule
   only matters to one screen it belongs in that screen's stylesheet, not here.

   Load order: tokens.css -> base.css -> layout.css -> components.css ->
   mobile.css -> screens/*.css, with print.css last (media="print").
   Fonts arrive via the single <link> in index.html.

   This file is also where the app stops being a web page. Contract section 4:
   the site ships wrapped by Median.co as an Android and iOS app, so the rubber
   band, the tap flash, the 300ms click delay and the long-press text callout
   all have to go, and the notch and home-indicator insets have to be paid back.
   Those rules live here rather than in layout.css because they are document
   behaviour, not shell geometry, and because a rule that only exists inside a
   media query would leave the desktop browser feeling different from the app.
   =========================================================================== */


/* ---------------------------------------------------------------------------
   Reset
   --------------------------------------------------------------------------- */

*,
*::before,
*::after{
  box-sizing: border-box;
}

html,
body{
  margin: 0;
  padding: 0;
}

/* Headings and text blocks arrive flush; spacing is the layout's decision, not
   the element's. The app uses h1-h3 only - see the heading scale below. */
h1, h2, h3,
p, figure, blockquote, dl, dd{
  margin: 0;
}

/* Every list in this app is a semantic grouping (nav, roster, recipients), not
   a bulleted document list. */
ul, ol{
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The sprite is injected as <div hidden aria-hidden="true">, and screens toggle
   panes with el.hidden. Restate the UA rule so a later display declaration
   cannot quietly resurrect a hidden node. */
[hidden]{
  display: none;
}

img,
picture,
video,
canvas{
  max-width: 100%;
  display: block;
}

/* Form controls do not inherit type from their ancestors by default. */
button,
input,
select,
textarea{
  font: inherit;
  color: inherit;
}

/* Every interactive control in this app is a real <button>, so the UA's grey
   bevel has to go before components.css can dress them. .btn, .icon-btn,
   .mini-btn, .fchip and friends all beat this on specificity. */
button{
  background: none;
  border: 0;
  padding: 0;
  text-align: inherit;
  color: inherit;
}

/* Search fields carry a UA clear-button and inner shadow in WebKit that ignore
   our border-radius. */
input[type="search"]{
  -webkit-appearance: none;
  appearance: none;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button{
  -webkit-appearance: none;
}

/* Nav items, tabs and the module launcher are all anchors; none of them want
   the default link treatment. */
a{
  color: inherit;
  text-decoration: none;
}


/* ---------------------------------------------------------------------------
   Document defaults
   --------------------------------------------------------------------------- */

body{
  font-family: var(--f-ui);
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink);

  /* The canvas the cards sit on. The prototype painted the body a dark slate
     grey - the backdrop of the pitch deck the file grew out of, framing a mock
     browser window. Nothing to do with the app itself. */
  background: var(--gray-50);

  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

::selection{
  background: var(--gold-a30);
  color: var(--ink);
}


/* ---------------------------------------------------------------------------
   Native shell behaviour  (contract section 4.4)

   Four browser reflexes that are correct on a web page and wrong inside an app
   wrapper. All four are set on the document rather than per component, because
   the moment one screen forgets them the app stops feeling like one app.
   --------------------------------------------------------------------------- */

html{
  /* Stop iOS Safari inflating type when the phone is turned to landscape. */
  -webkit-text-size-adjust: 100%;

  /* The grey flash Chrome and iOS paint over a tapped link or button before the
     :active rule has a chance to run. Every tappable atom in this app already
     answers a press with its own state, so the flash is only ever a double
     acknowledgement in the wrong colour. Inherited, hence declared once. */
  -webkit-tap-highlight-color: transparent;

  /* Kills the 300ms wait a mobile browser spends deciding whether a tap was the
     first half of a double-tap-to-zoom. touch-action is enforced as the
     intersection of the hit element and every ancestor, so declaring it on the
     root really does reach the whole tree. `manipulation` keeps panning and
     pinch zoom, which accessibility needs; only the double-tap gesture goes. */
  touch-action: manipulation;
}

/* The root declaration above is what actually governs the gesture, but it does
   not show up in a control's own computed style, which makes it invisible to a
   test and easy for a future rule to shadow with `touch-action: auto` on an
   ancestor. Restating it on the controls themselves costs one rule and makes
   the intent local to the thing being tapped. :where() keeps it at 0,0,0 so a
   scroller that needs pan-x, or a canvas that needs none, still wins outright. */
:where(a[href], button, input, select, textarea, label, summary,
       [role="button"], [role="tab"], [role="option"]){
  touch-action: manipulation;
}

/* No rubber band. On a real web page the overscroll glow tells you the document
   ended; inside a wrapped app it tells you that you are looking at a web page.
   Declared on both elements because whichever of the two is the scroller
   changes at the 860px shell switch (layout.css hands scrolling from .content
   back to the window), and the propagation rules differ between the two cases.
   Set on the y axis only: the horizontal scrollers that do exist (the schedule
   grid, the case board lanes) already declare their own containment. */
html,
body{
  overscroll-behavior-y: none;
}

/* Chrome does not get to be selectable. Dragging across the sidebar, a tab bar
   or a badge and watching it turn gold is the single loudest "this is a web
   page" tell there is, and long-pressing a button on iOS raises a copy callout
   over the control you were trying to press.

   :where() keeps this at specificity 0,0,0 so the opt-back-in below wins on
   source order alone, and so any component can override it without a fight. */
:where(.side, .side-brand, .side-foot, .side-user, .nav, .nav-item, .nav-label,
       .topbar, .bottombar, .tab, .phead, .sec-t, .stepper, .step,
       .btn, .icon-btn, .mini-btn, .mbtn, .fchip, .dpill, .seg,
       .pill, .badge, .tag, .av, .sla, label){
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Content the team genuinely copies out of this app: a vault value, a case
   code, a hostname, a description, a note, anything typed into a field. These
   have to stay selectable on a phone or the vault screen is useless, which is
   why the rule above names chrome explicitly instead of blanking the body.

   .selectable and [data-select="text"] are the escape hatches for a screen that
   needs one; the attribute form scores 0,1,0 and so also beats a component that
   turned selection off on itself. */
:where(input, textarea, select, [contenteditable="true"],
       p, td, dd, code, pre, .mono, .note, .desc, .secret, .selectable){
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

[data-select="text"]{
  -webkit-user-select: text;
  user-select: text;
}


/* ---------------------------------------------------------------------------
   Wrapped-app visibility  (contract section 4.2)

   app.js puts .is-native on <html> when the user agent says Median or GoNative,
   or when window.median exists. Two classes ride on it, and they are a pair on
   purpose: anything that only makes sense in a browser tab, and anything that
   only makes sense inside the app.

     .native-hide   real in the browser, gone in the app
                    (install prompts, "open in your browser", print links)
     .native-only   absent in the browser, real in the app
                    (pull-to-refresh hints, app version, native sign-out)

   `display: revert` rather than a fixed display value: the element goes back to
   whatever it would otherwise have been, so a .native-only <tr> stays a row and
   a .native-only <span> stays inline. Nothing has to declare its display twice.
   --------------------------------------------------------------------------- */

.native-only{
  display: none;
}

html.is-native .native-only{
  display: revert;
}

html.is-native .native-hide{
  display: none;
}

/* Inside the wrapper there is no browser chrome to bounce against on either
   axis, and no reason to leave the horizontal band unlocked. */
html.is-native,
html.is-native body{
  overscroll-behavior: none;
}

/* Long-pressing a logo, an icon or a link in an iOS or Android WebView starts a
   system drag with a translucent copy of the element stuck to the finger, and
   it looks exactly like a page coming apart. There is nothing in this app to
   drag anything to, so the gesture is pure downside. Left on in a desktop
   browser, where dragging an image out to a folder is a normal thing to do. */
html.is-native :where(img, svg, a, .av, .side-mark){
  -webkit-user-drag: none;
}

/* Keyboard hints are the first real consumer of the pair: app.js already ships
   the sidebar's Ctrl+K badge as `side-kbd native-hide`, because a WebView has
   no keyboard to press it with. Nothing speculative is listed here: a control
   that should disappear in the app gets .native-hide in its own markup. */


/* ---------------------------------------------------------------------------
   Heading scale

   The app uses exactly three levels and skips none:
     h1  screen title, one per screen, in the topbar / mobile header
     h2  panel or section title
     h3  card title  (these were <h4> in the prototype, under an h2 that did not
         exist - a skipped level that made the screens unnavigable by heading)
   --------------------------------------------------------------------------- */

h1{
  font-family: var(--f-disp);
  font-weight: 600;
  font-size: 21px;
  line-height: 1.2;
  letter-spacing: -.01em;
  color: var(--navy);
}

h2{
  font-family: var(--f-disp);
  font-weight: 600;
  font-size: 15px;
  line-height: 1.3;
  color: var(--navy);
}

h3{
  font-family: var(--f-ui);
  font-weight: 600;
  font-size: 13.5px;
  line-height: 1.3;
  color: var(--navy);
}

/* The router moves focus to the screen's h1 after every navigation so keyboard
   and screen-reader users follow the change. That focus is programmatic, not a
   keyboard action, and a tabindex -1 heading can never be reached with Tab, so
   no ring is lost here. Chrome still matches :focus-visible on the first load,
   which drew the gold ring around the title before anybody touched anything. */
h1[tabindex="-1"]:focus,
h2[tabindex="-1"]:focus{
  outline: none;
}

h1[tabindex="-1"]:focus-visible,
h2[tabindex="-1"]:focus-visible{
  box-shadow: none;
}


/* ---------------------------------------------------------------------------
   Icons

   One <svg class="ic"><use href="#i-name"/></svg> utility, three modifiers.
   The sprite is stroke-based, so size and colour both come from CSS: stroke
   follows currentColor, which means an icon inherits whatever paints its label.
   --------------------------------------------------------------------------- */

.ic{
  width: 18px;
  height: 18px;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
  display: inline-block;
  vertical-align: middle;
}

/* Inline with body copy and inside dense metadata rows. */
.ic--sm{
  width: 15px;
  height: 15px;
}

/* Stat tiles, the mobile tab bar and the empty-state block. */
.ic--lg{
  width: 22px;
  height: 22px;
}

/* Solid glyphs - the badge dot and the on-call marker. The sprite paths for
   these carry fill="currentColor" stroke="none" inline; this modifier makes the
   same flip for the stroke-drawn ones. */
.ic--fill{
  fill: currentColor;
  stroke: none;
}


/* ---------------------------------------------------------------------------
   Focus

   The prototype had no focus style whatsoever - tabbing through it was blind.
   One global ring, drawn as a box-shadow so it follows the element's own
   rounding instead of the outline's square box.

   Rule for every stylesheet in this app: never write outline:none without
   putting a visible replacement in the same rule.
   --------------------------------------------------------------------------- */

/* The ring fades rather than snaps, in both directions. The transition has to
   live on the RESTING state, not inside :focus-visible: a transition declared
   on the focused rule only exists while the element is focused, so the ring
   would ease in and then vanish the instant focus left. Declared here it covers
   the arrival and the departure.

   :where() scores 0,0,0, so any component that already lists box-shadow in its
   own transition keeps its own timing, and one that lists other properties
   still overrides this outright the way the shorthand always has. This rule is
   the floor for the controls nobody thought to time, not a new mechanism. */
:where(a[href], button, input, select, textarea, summary, [tabindex]){
  transition: box-shadow var(--t-fast);
}

:focus-visible{
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--r-sm);
}


/* ---------------------------------------------------------------------------
   Screen-reader-only text

   Carries the accessible name for icon-only controls, the <label> on every
   search field, and the router's aria-live announcements. Stays in the
   accessibility tree; takes no space and never triggers a scroll.
   --------------------------------------------------------------------------- */

.sr-only{
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}


/* ---------------------------------------------------------------------------
   Shared entrance keyframes

   Six primitives, declared once here so every stylesheet in the app can reach
   them. @keyframes are global to the document, not scoped to the file that
   declared them, and the two that used to live at the top of components.css
   were therefore only usable by accident of load order - a screen sheet that
   happened to load first would have found them undefined.

   The distances are deliberately short (6px, 16px, a 4% scale). An entrance
   should read as "settling", never as "flying in", and the retuned --ease-out
   in tokens.css already covers most of its distance early, so a long throw now
   looks like a swoop rather than an arrival.

   FILL MODE, which is the part that bites:

     Opacity-only keyframes (fade-in, veil-in) fill `both`. Holding the last
     frame pins opacity at 1, which is where the element was going anyway, so
     nothing can end up stuck invisible even if JS never removes a class.

     Keyframes that also move (rise-in, scale-in, slide-up, slide-in-right)
     fill `backwards`, and this is not a shortcut. An animated value outranks
     every normal declaration for as long as it is held, so `both` on a moving
     keyframe would pin transform at its resting value for good and the hover
     lift underneath it could never move again. `backwards` still holds the
     FIRST frame through the stagger delay, which is the half that matters:
     an item waiting its turn stays invisible instead of flashing in and out.
     After the run the element simply returns to its own resting styles, so it
     is never left at opacity 0 either.

   slide-up and slide-in-right read their travel from --slide-from so one
   keyframe serves both a 16px nudge and a full bottom sheet: a sheet sets
   --slide-from: 100% on itself and gets the same curve as everything else.
   --------------------------------------------------------------------------- */

@keyframes rise-in{
  from{ opacity: 0; transform: translateY(6px); }
  to  { opacity: 1; transform: translateY(0); }
}

@keyframes fade-in{
  from{ opacity: 0; }
  to  { opacity: 1; }
}

@keyframes scale-in{
  from{ opacity: 0; transform: scale(.96); }
  to  { opacity: 1; transform: scale(1); }
}

@keyframes slide-up{
  from{ opacity: 0; transform: translateY(var(--slide-from, 16px)); }
  to  { opacity: 1; transform: translateY(0); }
}

@keyframes slide-in-right{
  from{ opacity: 0; transform: translateX(var(--slide-from, 16px)); }
  to  { opacity: 1; transform: translateX(0); }
}

/* The backdrop behind a modal, a drawer or the mobile bottom sheet. The blur is
   animated alongside the wash so the app behind does not snap out of focus;
   because the end frame is exactly what the veil element declares at rest,
   `both` holds a value the element already had. */
@keyframes veil-in{
  from{
    opacity: 0;
    -webkit-backdrop-filter: blur(0px);
    backdrop-filter: blur(0px);
  }
  to{
    opacity: 1;
    -webkit-backdrop-filter: blur(var(--blur-veil));
    backdrop-filter: blur(var(--blur-veil));
  }
}


/* ---------------------------------------------------------------------------
   .stagger - a cascading list entrance

   Put .stagger on the container; every direct child rises in one --stagger
   behind the one above it. The index comes from --i, which the screen scripts
   already write with el.style.setProperty('--i', n) - never from an inline
   style attribute, and never as a hand-typed delay.

   Two guards, because a cascade that runs away is worse than no cascade:

     min(var(--i, 0), 12) caps the envelope at twelve steps. At 34ms that is
     408ms from the first item to the last, which is about as long as a list
     can take to arrive before it reads as loading rather than as animating.
     Item 40 of a long case list arrives with item 12, not thirteen seconds
     later, and a screen script does not have to remember to clamp.

     The :where() nth-child rules give the same cascade to markup that never
     ran a script - a static partial, a server-rendered list. They score 0,0,0,
     so a real --i (inline, or from a 0,1,0 rule) always wins, and so the
     reduced-motion net below outranks them.
   --------------------------------------------------------------------------- */

.stagger > *{
  animation: rise-in var(--t-base) backwards;
  animation-delay: calc(min(var(--i, 0), 12) * var(--stagger));
}

/* Opacity-only variant for lists that must not move: table rows, and anything
   whose children carry a hover lift of their own. */
.stagger--fade > *{
  animation: fade-in var(--t-base) both;
}

.stagger > :where(:nth-child(1)) { --i: 0; }
.stagger > :where(:nth-child(2)) { --i: 1; }
.stagger > :where(:nth-child(3)) { --i: 2; }
.stagger > :where(:nth-child(4)) { --i: 3; }
.stagger > :where(:nth-child(5)) { --i: 4; }
.stagger > :where(:nth-child(6)) { --i: 5; }
.stagger > :where(:nth-child(7)) { --i: 6; }
.stagger > :where(:nth-child(8)) { --i: 7; }
.stagger > :where(:nth-child(9)) { --i: 8; }
.stagger > :where(:nth-child(10)){ --i: 9; }
.stagger > :where(:nth-child(11)){ --i: 10; }
.stagger > :where(:nth-child(12)){ --i: 11; }
.stagger > :where(:nth-child(n+13)){ --i: 12; }


/* ---------------------------------------------------------------------------
   Reduced motion

   Two mechanisms, in this order, and both are needed.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce){

  /* The primary one. Every transition, and every animation that plays once, is
     timed with one of these tokens, so collapsing the tokens collapses the
     motion - no priority flag, no specificity fight, and the components stay
     readable because their colours, borders and shadows are untouched.

     The whole ladder is listed, not just --t-fast. That was the bug this block
     used to carry: --t-base and --t-slow were left at full length, so every
     transform, every reveal, every progress rail and the entire stagger
     envelope still played at 260ms and 420ms for a user who had asked the
     operating system for none of it.

     --stagger goes to 0ms so a list arrives as a list rather than as a wave,
     and --lift to 0px so a hover stops moving the card at all: a lift is
     motion whether or not it is timed. 1ms rather than 0s throughout, so
     transitionend and animationend still fire and no script that waits on one
     can hang.

     --t-loop and --t-spin are left alone on purpose. A loop is not made still
     by shortening it: the rule keeps its `infinite`, and a 1ms cycle repeated
     forever paints a random frame every frame, a strobe. Every loop is
     stopped instead, either by the net below or, when its selector outranks
     the net, by a reduced-motion block of its own beside the rule that also
     gives it a still, visible resting state (busy bar in layout.css, sign in
     hairline and spinners in login.css, palette line in commandbar.css). */
  :root{
    --t-fast:   1ms linear;
    --t-base:   1ms linear;
    --t-slow:   1ms linear;
    --t-slower: 1ms linear;
    --stagger:  0ms;
    --lift:     0px;
  }

  /* Safety net for any hand-written duration that slipped past the token, and
     for the delays: a token cannot collapse an `animation-delay` that some rule
     computed from something other than --stagger, and a held delay is exactly
     what leaves an element sitting invisible.

     `html:root *` scores 0,1,1 (0,1,2 on ::before and ::after), which
     outranks the single-class rules that realistically carry a transition,
     without resorting to a priority flag. It also cuts every loop it reaches
     to one pass, so a skeleton shimmer (.ui-skel, .nskel) runs once and
     rests. It reaches only a rule that scores less: every other sheet that
     animates loads after this one, so a tie goes to the loop. What it
     cannot reach is therefore a loop that scores as much or more, such as
     `html.is-loading .busybar-fill` (0,2,1), or one on html itself or on its
     own ::before or ::after, since html is no descendant of anything. Those
     are the ones that carry their own block, as the note above says. */
  html:root *,
  html:root *::before,
  html:root *::after{
    animation-duration: 1ms;
    animation-delay: 0ms;
    animation-iteration-count: 1;
    transition-duration: 1ms;
    transition-delay: 0ms;
  }

  html:root{
    scroll-behavior: auto;
  }

}
