/* ============================================================================
   layout.css - the JACRA Backoffice app shell
   ----------------------------------------------------------------------------
   Sidebar + topbar + content on desktop; a centred column + a fixed tab bar on
   mobile. This is the ONLY file that owns shell geometry, and each shell class
   is declared exactly once. The single media query at the bottom INVERTS the
   scroll model rather than re-declaring the boxes.

   SCROLL MODEL - the load-bearing part of this file.

   Desktop (> 860px)                      Mobile (<= 860px)
   ------------------------------------   ------------------------------------
   window             locked, no scroll   window             IS the scroller
   .app               100dvh, hidden      .app               auto height, visible
   .main              hidden              .main              visible
   #screens           hidden              #screens           visible
   .screen.is-active  flex column         .screen.is-active  block
   .view-desktop      flex column         .view-desktop      display: none
   .view-desktop
     > .content       THE SCROLLER        .view-mobile       block, centred
                      overflow: auto        > .pbody         overflow: visible

   Consequence: every flex ancestor of .content needs min-height: 0 (and
   min-width: 0). The flex default of min-height: auto lets a pane grow past the
   viewport, so the inner scroller never engages and the pane just clips. The
   full chain is  body > .app > .main > #screens > .screen > .view-desktop.

   Every viewport height here uses the DYNAMIC unit, dvh. The static one leaves
   the fixed tab bar sitting under mobile browser chrome on first paint, which
   makes the last row of every screen unreachable.

   WHAT IS RENDERED, NOT HIDDEN. The sidebar and the tab bar arrive empty in
   index.html and are filled by app.js from the permission filtered module
   list. There is deliberately no rule in this file that hides a nav entry: a
   destination the signed in user may not open never reaches the DOM at all.

   SAFE AREA. Contract section 4.3. Every piece of fixed chrome in this file -
   the sidebar, the tab bar, the busy bar and the overlay sheets - pays back the
   inset it sits under, using the --safe-* tokens. On a desktop and on an
   Android without a cutout all four resolve to 0px, so the arithmetic is free.

   App-wide there are exactly two breakpoints: 1180px (dashboard grid reflow,
   which belongs to the screen stylesheets) and 860px (the shell switch, here).
   There is deliberately no matching min-width query one pixel above the switch:
   the base rules above ALREADY are the desktop case, so it could only be a no-op.
   ============================================================================ */


/* ---------------------------------------------------------------------------
   Derived shell metrics.
   tokens.css owns the primitives (--bottombar-h, --s-2, --safe-b, ...). These
   are arithmetic on top of them, declared once so the fixed tab bar's clearance
   and the busy bar's timing each have a single source of truth instead of being
   hand-typed at every call site.
   --------------------------------------------------------------------------- */
:root{
  /* Room to leave under mobile content so the tab bar never covers it. */
  --bottombar-clear: calc(var(--bottombar-h) + var(--s-2));
  /* The same, plus the iOS home-indicator inset (0px on every other device). */
  --bottombar-safe: calc(var(--bottombar-clear) + var(--safe-b));

  /* How long a request must run before the busy bar is worth showing. The
     notification poll finishes well inside this, so a bar never blinks once a
     minute for a background check nobody asked about. */
  --busy-delay: 260ms;
  /* One sweep of the busy bar, and one breath of a loading skeleton. */
  --busy-cycle: var(--t-loop);
}


/* ---------------------------------------------------------------------------
   Document. On desktop the window is locked and .content is the only scroller.
   --------------------------------------------------------------------------- */
html,
body{
  height: 100%;
}

body{
  overflow: hidden;
}


/* ---------------------------------------------------------------------------
   .app - the shell grid
   --------------------------------------------------------------------------- */
.app{
  display: grid;
  grid-template-columns: var(--side-w) 1fr;
  height: 100dvh;
  overflow: hidden;
  background: var(--gray-50);
}


/* ---------------------------------------------------------------------------
   .side - sidebar (desktop only), rendered by app.js
   --------------------------------------------------------------------------- */
.side{
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding-top: calc(var(--s-5) + var(--safe-t));
  padding-right: var(--s-4);
  padding-bottom: calc(var(--s-4) + var(--safe-b));
  padding-left: calc(var(--s-4) + var(--safe-l));
  background: var(--navy-900);
  color: var(--gray-400);
}

/* Brand lockup: mark + wordmark. Rendered as a link back to the first screen
   this account may open, which is not always Home. */
.side-brand{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-1) var(--s-2) var(--s-4);
  color: var(--white);
  text-decoration: none;
}

/* The Jacra mark.

   Drawn from the company logo file rather than set as a letter in a box. The
   real mark is a calligraphic J whose stroke thickens and thins along its
   length, with a small curl at the top and a loop that crosses the stem at the
   bottom. No typeface reproduces that, and the gold tile with a bold J that
   used to sit here was a stand in, not the logo.

   Two layers, because the logo itself is two layers: a gold copy offset down
   and to the right, and the light copy on top. That offset is what makes it
   read as the Jacra mark instead of a single letter.

   The artwork is carried as a mask, not as an image, so the colour comes from
   the tokens and one file serves the dark sidebar and any light surface it may
   sit on later. */
.side-mark{
  position: relative;
  display: block;
  flex: none;
  width: 27px;
  height: 34px;
  /* The glyph that markup still carries as a fallback is hidden: the shape is
     painted by the two layers below. */
  font-size: 0;
  color: transparent;
}

.side-mark::before,
.side-mark::after{
  content: '';
  position: absolute;
  inset: 0;
  -webkit-mask: url(../icons/jacra-j.png) center / contain no-repeat;
  mask: url(../icons/jacra-j.png) center / contain no-repeat;
}

.side-mark::before{
  background: var(--gold);
  transform: translate(2px, 2px);
}

.side-mark::after{
  background: var(--gray-50);
}

.side-name{
  display: flex;
  align-items: baseline;
  gap: var(--s-2);
  min-width: 0;
  font-family: var(--f-disp);
  font-size: 17px;
  font-weight: 700;
  letter-spacing: .02em;
  color: var(--white);
}

.side-name small{
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  color: var(--gold);
}


/* ---------------------------------------------------------------------------
   .side-tools - search and the notification bell, above the nav list.
   The bell lives here rather than in a screen topbar because the topbars belong
   to the screen partials: shell chrome that moved house on every navigation
   would lose its open panel every time.
   --------------------------------------------------------------------------- */
.side-tools{
  display: flex;
  flex: none;
  flex-direction: column;
  gap: var(--s-1);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--navy);
}

.side-tool{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  min-height: 40px;
  padding: var(--s-2) var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--gray-300);
  font-family: var(--f-ui);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.side-tool:hover{
  background: var(--navy-800);
  color: var(--white);
}

.side-tool .ic{
  flex: none;
  color: var(--gray-400);
  transition: color var(--t-fast);
}

.side-tool:hover .ic{
  color: var(--gold);
}

.side-tool[aria-expanded="true"]{
  background: var(--navy-800);
  color: var(--white);
}

.side-tool-l{
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.side-kbd{
  flex: none;
  padding-inline: var(--s-2);
  border: 1px solid var(--navy-600);
  border-radius: var(--r-sm);
  background: var(--navy-800);
  color: var(--gray-400);
  font-family: var(--f-mono);
  font-size: 10px;
  line-height: 18px;
}

/* Unread count on the bell. Hidden through the `hidden` attribute app.js
   toggles, never through a class, so the accessible tree agrees with the pixels. */
.nav-badge{
  flex: none;
  min-width: 20px;
  padding-inline: var(--s-1);
  border-radius: 999px;
  background: var(--red);
  color: var(--white);
  font-family: var(--f-ui);
  font-size: 10.5px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}


/* ---------------------------------------------------------------------------
   .nav - the permission filtered destination list
   --------------------------------------------------------------------------- */

/* Group eyebrow. Lives inside .nav now, because the groups are rendered and a
   heading with nothing under it would be a lie. */
.nav-label{
  padding: var(--s-4) var(--s-3) var(--s-1);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gray-400);
}

.nav-label:first-child{
  padding-top: var(--s-2);
}

.nav{
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--s-1);
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* .nav-item is used on <a href="#/id"> in the nav. Never on a div. */
.nav-item{
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  min-height: 40px;
  padding: var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--gray-300);
  font-family: var(--f-ui);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.2;
  text-align: left;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.nav-item .ic{
  flex: none;
}

.nav-item:hover{
  background: var(--navy-800);
  color: var(--white);
}

/* Router-owned state. aria-current is matched alongside .is-active so the
   visual state can never drift out of sync with the accessible one. */
.nav-item.is-active,
.nav-item[aria-current="page"]{
  background: var(--gold-a14);
  color: var(--gold);
}

.nav-item.is-active::before,
.nav-item[aria-current="page"]::before{
  content: "";
  position: absolute;
  top: var(--s-2);
  bottom: var(--s-2);
  left: calc(var(--s-4) * -1);
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--gold);
  transition: transform var(--t-base);
}

.side-foot{
  display: flex;
  flex: none;
  flex-direction: column;
  gap: var(--s-1);
  padding-top: var(--s-3);
  border-top: 1px solid var(--navy);
}

/* Signed-in user chip, pinned to the bottom of the sidebar. A real button:
   it opens the account menu. */
.side-user{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  min-height: 52px;
  margin-top: var(--s-2);
  padding: var(--s-2);
  border: 1px solid var(--navy-600);
  border-radius: var(--r-sm);
  background: var(--navy-800);
  color: var(--gray-300);
  font-family: var(--f-ui);
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}

.side-user:hover{
  background: var(--navy);
  border-color: var(--gold-a30);
}

.side-user[aria-expanded="true"]{
  background: var(--navy);
  border-color: var(--gold-a30);
}

.side-user .ic{
  flex: none;
  margin-left: auto;
  color: var(--gray-500);
  transition: transform var(--t-base);
}

.side-user[aria-expanded="true"] .ic{
  transform: rotate(180deg);
}

.su-meta{
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}

.su-meta b{
  overflow: hidden;
  font-size: 13px;
  font-weight: 600;
  color: var(--white);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.su-meta span{
  font-size: 11px;
  color: var(--gray-400);
}


/* ---------------------------------------------------------------------------
   .main / #screens / .screen / .view - the mount chain.
   Every level carries min-height: 0 so the scroller at the end of it works.
   --------------------------------------------------------------------------- */
.main{
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--gray-50);
}

#screens{
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.screen{
  display: none;
  /* The screen transition. Declared on the base rule, not on the state classes,
     so the value is already in place when the router adds .is-entering and the
     transition really fires when it lifts it one frame later. */
  opacity: 1;
  transition: opacity var(--t-base), transform var(--t-base);
}

.screen.is-active{
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

/* ---- screen transition ----------------------------------------------------
   The owner asked for movement that never snaps, so the swap is staged: the
   outgoing screen fades and lifts away on --t-fast, and only once it is gone
   does the incoming one rise into place on the slower --t-base. Sequential
   rather than crossfaded on purpose - two screens in the document at once
   would put two copies of the same element ids there too.

   The direction comes from #screens[data-direction], which router.js stamps
   from the nav order, so going back down the sidebar moves the other way.
   --------------------------------------------------------------------------- */
.screen.is-entering{
  opacity: 0;
  transform: translate3d(0, 12px, 0);
}

.screen.is-leaving{
  opacity: 0;
  transform: translate3d(0, -8px, 0);
  pointer-events: none;
  transition: opacity var(--t-fast), transform var(--t-fast);
}

#screens[data-direction="back"] > .screen.is-entering{
  transform: translate3d(0, -12px, 0);
}

#screens[data-direction="back"] > .screen.is-leaving{
  transform: translate3d(0, 8px, 0);
}

/* Base rule shared by both views. */
.view{
  min-width: 0;
  min-height: 0;
}

.view-desktop{
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.view-mobile{
  display: none;
}


/* ---------------------------------------------------------------------------
   .topbar - fixed-height header inside .view-desktop, outside the scroller.
   It belongs to the screen partials; the shell only supplies the geometry.
   --------------------------------------------------------------------------- */
.topbar{
  display: flex;
  flex: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  height: calc(var(--topbar-h) + var(--safe-t));
  min-width: 0;
  padding: var(--safe-t) var(--content-pad) 0;
  background: var(--white);
  border-bottom: 1px solid var(--gray-200);
}

.tb-title{
  display: flex;
  flex: 1 1 auto;
  align-items: baseline;
  gap: var(--s-3);
  min-width: 0;
}

.tb-title h1{
  margin: 0;
  font-family: var(--f-disp);
  font-size: 21px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--ink);
  white-space: nowrap;
}

.tb-sub{
  margin: 0;
  min-width: 0;
  font-size: 12.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tb-tools{
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
}

/* .tb-search is a <label>: sr-only text, then the icon, then the input. */
.tb-search{
  display: flex;
  flex: 0 1 230px;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--gray-200);
  border-radius: var(--r-sm);
  background: var(--gray-50);
  color: var(--gray-500);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}

.tb-search input{
  width: 100%;
  min-width: 0;
  border: 0;
  background: transparent;
  font-family: var(--f-ui);
  font-size: 13px;
  color: var(--ink);
}

.tb-search input::placeholder{
  color: var(--muted);
}

/* The focus ring moves to the label so the whole field reads as focused. This
   is the replacement that licenses dropping the input's own outline below. */
.tb-search:focus-within{
  border-color: var(--gold-600);
  box-shadow: var(--focus-ring);
}

.tb-search input:focus-visible{
  outline: none;
}

.tb-user{
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
  padding-left: var(--s-1);
}

.tb-user b{
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

.tb-user .ic{
  color: var(--gray-400);
}


/* ---------------------------------------------------------------------------
   .content - THE desktop scroller
   --------------------------------------------------------------------------- */
.content{
  /* Positioned so every absolutely positioned box inside it, .sr-only above
     all, takes its containing block from here and scrolls with it. overflow:
     auto alone does not do that: with no positioned ancestor such a box
     resolves against the window, escapes this scroller and stretches the
     locked window, and scrollIntoView or a screen reader landing on it scrolls
     the whole shell off screen. Measured on 23 October 2026 at 1366x900: seven
     screens did it, the Delivery tab of Email Blast up to a 2400px document. */
  position: relative;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: auto;
  padding: var(--content-pad);
  padding-right: calc(var(--content-pad) + var(--safe-r));
  background: var(--gray-50);
  scrollbar-gutter: stable;
  overscroll-behavior: contain;
}


/* ---------------------------------------------------------------------------
   .bottombar - the mobile tab bar. Hidden on desktop; pinned in the query.
   Rendered by app.js: up to four destinations plus More.
   --------------------------------------------------------------------------- */
.bottombar{
  display: none;
}

.bottombar .tab{
  position: relative;
  display: flex;
  flex: 1;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-1);
  min-width: 0;
  min-height: 44px;
  padding: var(--s-1) 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--muted);
  font-family: var(--f-ui);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.bottombar .tab .ic{
  width: 22px;
  height: 22px;
  transition: transform var(--t-base);
}

.bottombar .tab em{
  max-width: 100%;
  font-style: normal;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bottombar .tab:hover{
  color: var(--ink);
}

.bottombar .tab:active .ic{
  transform: scale(.9);
}

.bottombar .tab.is-active,
.bottombar .tab[aria-current="page"]{
  background: var(--gold-a05);
  color: var(--ink);
}

.bottombar .tab.is-active .ic,
.bottombar .tab[aria-current="page"] .ic{
  color: var(--gold-600);
}

.bottombar .tab[aria-expanded="true"]{
  background: var(--gray-100);
  color: var(--ink);
}

/* Unread pip on More: the bell has no tab of its own at phone width. */
.tab-dot{
  position: absolute;
  top: var(--s-1);
  left: 50%;
  width: 8px;
  height: 8px;
  margin-left: var(--s-2);
  border-radius: 999px;
  background: var(--red);
}


/* ---------------------------------------------------------------------------
   .busybar - the global request indicator.
   api.js puts .is-loading on <html> while any request is on the wire. The
   opacity delay is the whole trick: a request that finishes inside
   --busy-delay never reveals the bar, so the once-a-minute unread poll stays
   invisible while a slow report still gets a progress cue.
   --------------------------------------------------------------------------- */
.busybar{
  position: fixed;
  top: var(--safe-t);
  right: 0;
  left: 0;
  z-index: 90;
  height: 3px;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--t-fast);
}

html.is-loading .busybar{
  opacity: 1;
  transition-delay: var(--busy-delay);
}

.busybar-fill{
  display: block;
  width: 42%;
  height: 100%;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(90deg, var(--gold-a30), var(--gold), var(--gold-600));
  transform: translate3d(-105%, 0, 0);
}

html.is-loading .busybar-fill{
  animation: busy-sweep var(--busy-cycle) var(--ease-in-out) infinite;
}

@keyframes busy-sweep{
  from{ transform: translate3d(-105%, 0, 0); }
  to{ transform: translate3d(260%, 0, 0); }
}

/* Reduced motion: no sweep. The rule above scores 0,2,1 and outranks the
   safety net in base.css, so it has to stop itself. It cannot simply run once
   either, because its resting place is off screen to the left. The fill rests
   across the whole bar instead, one still gold line for as long as a request
   is out, the same answer commandbar.css gives its own progress line. The
   reveal delay stays: it is not motion, and it keeps the background poll from
   blinking the line on and off. */
@media (prefers-reduced-motion: reduce){
  html.is-loading .busybar-fill{
    width: 100%;
    border-radius: 0;
    background: var(--gold);
    transform: none;
    animation: none;
  }
}


/* ---------------------------------------------------------------------------
   Overlay hosts.
   Three empty divs at the end of index.html: #overlays for the shell's own
   panels, #modals for ui.js, #toasts for messages. Each is fixed and inert
   until something is put inside it, so none of them can ever swallow a click
   meant for the app underneath.
   --------------------------------------------------------------------------- */
.overlays,
.modal-root,
.toasts{
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.overlays{
  z-index: 70;
}

.modal-root{
  z-index: 75;
}

.toasts{
  z-index: 80;
}

.overlays:empty,
.modal-root:empty,
.toasts:empty{
  display: none;
}

.overlays > *,
.modal-root > *,
.toasts > *{
  pointer-events: auto;
}

/* .toast declares pointer-events: none in components.css so it never blocks the
   screen it floats over. Its action button is the one part that must be
   clickable. */
.toast .mini-btn{
  pointer-events: auto;
}


/* ---------------------------------------------------------------------------
   .pop - the shell's one overlay primitive.
   The notification panel, the More sheet, the account menu and the change
   password dialog are all this. On desktop it is a panel anchored beside its
   trigger through --pop-x / --pop-y, which app.js measures and sets. At 860px
   and below the media query turns exactly the same markup into a bottom sheet.
   --------------------------------------------------------------------------- */
.pop{
  position: fixed;
  inset: 0;
  pointer-events: auto;
}

/* A closing sheet stays in the document until its slide has finished. Without
   this it would keep swallowing taps for the length of that animation, which
   reads as an app that has frozen. */
.pop:not(.is-on){
  pointer-events: none;
}

/* A real button, because it really does something: it closes the panel. */
.pop-veil{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: default;
  opacity: 0;
  transition: opacity var(--t-base);
}

.pop.is-on .pop-veil{
  opacity: 1;
}

.pop-panel{
  position: fixed;
  top: var(--pop-y, 96px);
  left: var(--pop-x, calc(var(--side-w) + var(--s-3)));
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: min(360px, calc(100vw - var(--s-8)));
  max-height: min(560px, calc(100dvh - var(--s-7)));
  overflow: hidden;
  border: 1px solid var(--gray-200);
  border-radius: var(--r-lg);
  background: var(--white);
  box-shadow: var(--sh-lg);
  opacity: 0;
  transform: translate3d(0, var(--s-2), 0) scale(.98);
  transform-origin: top left;
  transition: opacity var(--t-base), transform var(--t-base);
}

.pop.is-on .pop-panel{
  opacity: 1;
  transform: translate3d(0, 0, 0) scale(1);
}

.pop-grip{
  display: none;
}

.pop-head{
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-4) var(--s-3);
  border-bottom: 1px solid var(--gray-200);
}

.pop-t{
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-family: var(--f-disp);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}

.pop-t:focus-visible{
  outline: none;
  box-shadow: none;
}

.pop-head-act{
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--s-2);
}

.pop-head-act:empty{
  display: none;
}

/* The sprite has no cross, and a second glyph that means the same thing as the
   plus would be a glyph to keep in sync. Rotating the plus is the whole close
   icon. */
.pop-x .ic{
  transform: rotate(45deg);
}

.pop-body{
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-3) var(--s-4) var(--s-4);
}

/* The change password dialog: centred rather than anchored, and behind a real
   veil, because it is a decision rather than a glance. */
.pop--center .pop-panel{
  top: 50%;
  left: 50%;
  width: min(420px, calc(100vw - var(--s-7)));
  transform: translate3d(-50%, calc(-50% + var(--s-3)), 0) scale(.98);
  transform-origin: center;
}

.pop--center.is-on .pop-panel{
  transform: translate3d(-50%, -50%, 0) scale(1);
}

.pop--center .pop-veil{
  background: var(--veil);
  backdrop-filter: blur(var(--blur-veil));
  -webkit-backdrop-filter: blur(var(--blur-veil));
}


/* ---------------------------------------------------------------------------
   .nlist - the notification feed inside the panel
   --------------------------------------------------------------------------- */
.nlist{
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.nrow{
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: start;
  gap: var(--s-3);
  min-height: 44px;
  padding: var(--s-3);
  border-radius: var(--r-sm);
  color: var(--ink);
  text-decoration: none;
  transition: background var(--t-fast);
}

.nrow:hover{
  background: var(--gray-50);
}

.nrow.is-unread{
  background: var(--gold-a05);
}

.nrow.is-unread:hover{
  background: var(--gold-a14);
}

.nrow-ic{
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  background: var(--gray-100);
  color: var(--gray-600);
}

.nrow.is-unread .nrow-ic{
  background: var(--gold-100);
  color: var(--gold-600);
}

.nrow-b{
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  min-width: 0;
}

.nrow-t{
  font-size: 13px;
  font-weight: 600;
  line-height: 1.3;
}

.nrow-d{
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted);
}

.nrow-m{
  font-size: 11px;
  color: var(--gray-500);
}

.nrow-dot{
  flex: none;
  width: 8px;
  height: 8px;
  margin-top: var(--s-2);
  border-radius: 999px;
  background: var(--gold);
}

.nlist--loading{
  gap: var(--s-2);
}

.nskel{
  display: block;
  height: 52px;
  border-radius: var(--r-sm);
  background: var(--gray-100);
  animation: nskel-breathe var(--busy-cycle) var(--ease-in-out) infinite;
}

@keyframes nskel-breathe{
  0%, 100%{ opacity: .5; }
  50%{ opacity: 1; }
}


/* ---------------------------------------------------------------------------
   .msheet - the More sheet, and .umenu - the account menu.
   They share the row primitive: one control shape, two hosts.
   --------------------------------------------------------------------------- */
.msheet-l{
  margin: var(--s-4) 0 var(--s-2);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gray-500);
}

.msheet-l:first-child{
  margin-top: 0;
}

.msheet-grid{
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(94px, 1fr));
  gap: var(--s-2);
}

.msheet-tile{
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 78px;
  padding: var(--s-3);
  border: 1px solid var(--gray-200);
  border-radius: var(--r);
  background: var(--white);
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-base);
}

.msheet-tile:hover{
  background: var(--gray-50);
  border-color: var(--gray-300);
}

.msheet-tile:active{
  transform: scale(.97);
}

.msheet-tile .ic{
  width: 20px;
  height: 20px;
  color: var(--gold-600);
}

.msheet-list,
.umenu-list{
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.msheet-row,
.umenu-item,
.lang-row{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink);
  font-family: var(--f-ui);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.2;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}

.msheet-row:hover,
.umenu-item:hover,
.lang-row:hover{
  background: var(--gray-50);
}

.msheet-row .ic,
.umenu-item .ic{
  flex: none;
  color: var(--gray-500);
}

.msheet-row > span:not(.pill),
.umenu-item > span,
.lang-row > span:not(.lang-dot){
  flex: 1 1 auto;
  min-width: 0;
}

.msheet-row--danger,
.umenu-item--danger{
  color: var(--red-700);
}

.msheet-row--danger .ic,
.umenu-item--danger .ic{
  color: var(--red);
}

.msheet-row--danger:hover,
.umenu-item--danger:hover{
  background: var(--red-100);
}

.msheet-id{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3) var(--s-3);
}

.msheet-who,
.umenu-who{
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.3;
}

.msheet-who b,
.umenu-who b{
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}

.msheet-who span{
  font-size: 11.5px;
  color: var(--muted);
}

.umenu-id{
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--gray-200);
}

.umenu-role{
  font-size: 11.5px;
  color: var(--muted);
}

.umenu-mail{
  overflow: hidden;
  font-size: 11px;
  color: var(--gray-500);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.umenu-list{
  padding-top: var(--s-2);
}


/* ---------------------------------------------------------------------------
   .lang-group - the language picker, inside the account menu and inside the
   More sheet. app.js builds it once and drops the same block into both, so
   what follows has to sit correctly inside either list.

   Why a ring rather than an icon. Every neighbouring row opens with an 18px
   glyph from the sprite, and the sprite holds no globe and no flag; borrowing
   an unrelated one would be a lie, and leaving the leading column empty would
   push these two labels out of line with every row around them. The ring is
   18px, so it lands in that column exactly, and it says what a tick cannot:
   one of these is chosen, always, and choosing the other unchooses this one.
   --------------------------------------------------------------------------- */
.lang-group,
.lang-opts{
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

/* The group caption. Same voice as .msheet-l, less air: that one sits above a
   list and this one sits inside one. */
.lang-l{
  margin: var(--s-3) 0 var(--s-1);
  padding-inline: var(--s-3);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gray-500);
}

.lang-dot{
  flex: none;
  width: 18px;
  height: 18px;
  border: 2px solid var(--gray-300);
  border-radius: 999px;
  transition: border-color var(--t-fast), background var(--t-fast), box-shadow var(--t-fast);
}

/* .is-on, not .is-active: the router owns that one, and this is UI state. */
.lang-row.is-on{
  color: var(--gold-700);
  font-weight: 600;
}

/* The inset white ring is the gap between the border and the fill, drawn with
   a shadow so the dot stays one element and one box. */
.lang-row.is-on .lang-dot{
  border-color: var(--gold-600);
  background: var(--gold);
  box-shadow: inset 0 0 0 2px var(--white);
}

.lang-row:disabled{
  cursor: progress;
  opacity: .6;
}


/* ---------------------------------------------------------------------------
   .pform - the change password form.
   Kept in the shell rather than in ui.js's form builder because signing in
   again must never depend on a module that may still be loading.
   --------------------------------------------------------------------------- */
.pform{
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.pform > .note{
  margin: 0;
}

.pfield{
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.pfield-l{
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
}

.pfield-in{
  width: 100%;
  min-height: 44px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  background: var(--white);
  font-family: var(--f-ui);
  font-size: 14px;
  color: var(--ink);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}

.pfield-in:focus-visible{
  outline: none;
  border-color: var(--gold-600);
  box-shadow: var(--focus-ring);
}

.pfield-in.is-invalid{
  border-color: var(--red);
}

.pfield-err{
  font-size: 11.5px;
  color: var(--red-700);
}

.pform-err{
  margin: 0;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-sm);
  background: var(--red-100);
  color: var(--red-700);
  font-size: 12.5px;
  line-height: 1.45;
}

.pform-act{
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
  padding-top: var(--s-1);
}


/* ---------------------------------------------------------------------------
   .app-error - the boot and route failure banner.
   It is prepended to <body>, ahead of .app, because .app is a 100dvh box in an
   overflow:hidden body and anything after it would be clipped out of reach.
   --------------------------------------------------------------------------- */
.app-error{
  position: relative;
  z-index: 95;
  margin: var(--s-4);
}


/* ============================================================================
   <= 860px - the shell switch.
   Scrolling goes back to the window, the sidebar and the desktop view go away,
   the mobile column is centred, the tab bar is pinned to the viewport and every
   .pop becomes a bottom sheet.
   ============================================================================ */
@media (max-width: 860px){

  /* Hand scrolling back to the window. */
  html,
  body{
    height: auto;
    min-height: 100%;
    overflow: visible;
  }

  .app{
    display: block;
    height: auto;
    min-height: 100dvh;
    overflow: visible;
  }

  .side{
    display: none;
  }

  .main{
    display: block;
    min-height: 100dvh;
    overflow: visible;

    /* JARAK NOTCH TIDAK DIBAYAR DI SINI, dan itu keputusan yang sengaja.
       Kepala tiap layar yang membayarnya sendiri, lewat dua aturan di
       mobile.css dan satu di screens/home.css.

       Sebelumnya jarak itu dibayar dua kali: sekali di sini dan sekali lagi di
       .phead. Yang dibayar di sini berwarna krem, warna dasar aplikasi,
       sedangkan kepala layar berwarna putih dan hero Beranda berwarna navy.
       Hasilnya pita kosong selebar layar di bawah jam ponsel, berbeda warna
       dari apa pun di bawahnya, dan di Beranda bedanya mencolok. Di peramban
       biasa safe area selalu nol, jadi tidak satu pun audit bisa melihatnya;
       yang menemukannya pemiliknya sendiri di ponselnya.

       Membiarkan kepala layar yang membayar berarti jaraknya diwarnai oleh
       elemen yang memang ada di bawah jam itu, bukan oleh dasar halaman. */
    padding-bottom: var(--bottombar-safe);
  }

  #screens{
    display: block;
  }

  .screen.is-active{
    display: block;
  }

  .view-desktop{
    display: none;
  }

  .view-mobile{
    display: block;
    max-width: var(--mobile-max);
    /* Tanpa mengurangi --safe-t lagi: sejak jarak notch berpindah ke kepala
       tiap layar, kolom ini memang setinggi layar dikurangi tab bar saja. */
    min-height: calc(100dvh - var(--bottombar-safe));
    margin-inline: auto;
    background: var(--gray-50);
  }

  /* No inner pane keeps its own scrollbar: the window is the one scroller. */
  .view-mobile,
  .view-mobile .content,
  .view-mobile .pbody{
    height: auto;
    overflow: visible;
  }

  .bottombar{
    position: fixed;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 50;
    display: flex;
    align-items: stretch;
    gap: var(--s-1);
    width: 100%;
    max-width: var(--mobile-max);
    height: calc(var(--bottombar-h) + var(--safe-b));
    margin-inline: auto;
    padding-top: var(--s-2);
    padding-right: calc(var(--s-2) + var(--safe-r));
    padding-bottom: calc(var(--s-2) + var(--safe-b));
    padding-left: calc(var(--s-2) + var(--safe-l));
    background: var(--white);
    border-top: 1px solid var(--gray-200);
  }

  /* ---- every .pop becomes a bottom sheet --------------------------------- */
  .pop-veil{
    background: var(--veil);
    backdrop-filter: blur(var(--blur-veil));
    -webkit-backdrop-filter: blur(var(--blur-veil));
  }

  .pop-panel,
  .pop--center .pop-panel{
    top: auto;
    right: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    max-width: var(--mobile-max);
    max-height: 84dvh;
    margin-inline: auto;
    border-right: 0;
    border-bottom: 0;
    border-left: 0;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    transform: translate3d(0, 100%, 0);
    transform-origin: bottom center;
    transition: opacity var(--t-fast), transform var(--t-slower);
  }

  .pop.is-on .pop-panel,
  .pop--center.is-on .pop-panel{
    transform: translate3d(0, 0, 0);
  }

  /* The grab handle every native sheet has. Decorative: the close button and
     the veil are the real controls. */
  .pop-grip{
    display: block;
    flex: none;
    width: 40px;
    height: 4px;
    margin: var(--s-2) auto 0;
    border-radius: 999px;
    background: var(--gray-300);
  }

  .pop-head{
    padding-top: var(--s-3);
  }

  .pop-body{
    padding-bottom: calc(var(--s-5) + var(--safe-b));
  }

  /* Contract section 4.5: 44px minimum for anything a thumb has to hit. */
  .pop-panel .pop-x{
    width: 44px;
    height: 44px;
  }

  .pform-act{
    flex-direction: column-reverse;
  }

  .pform-act .btn{
    width: 100%;
    min-height: 44px;
  }
}
