/*!
 * ZazaDesk — customer portal chrome
 *
 * Scope: the CHROME ONLY (top bar, sidebar, content offset, footer). The 46
 * stock body views are untouched and still depend on bootstrap-css,
 * tailwind-css, theme-css and the fontawesome handles, so none of those are
 * stripped. See the strip list in zazadesk.php for why.
 *
 * ── CASCADE ─────────────────────────────────────────────────────────────────
 * theme/head.php emits this file in this order:
 *
 *   1. compile_theme_css()   stock sheets (bootstrap, tailwind, theme-css, …)
 *   2. THIS FILE             layout + fallback colours
 *   3. app_customers_head()  theme_style's inline <style> = the BUYER's colours
 *
 * So this file beats the stock sheets, and the buyer's colour picker beats this
 * file. That is deliberate. theme_style targets `.customers .navbar-default`
 * (specificity 0,2,0) and `.customers .navbar-default .navbar-nav>li>a`, so
 * every colour declaration here is kept at single-class specificity (0,1,0) and
 * never uses !important. Raising specificity on a colour here would silently
 * disable Setup → Settings → Customers → theme colours.
 *
 * Layout properties have no such constraint — theme_style only ever writes
 * background and color.
 *
 * ── RTL ─────────────────────────────────────────────────────────────────────
 * Logical properties throughout (inset-inline-start, margin-inline-start,
 * border-inline-end). Perfex resolves direction per contact and head.php sets
 * dir="rtl" on <body>, so the layout mirrors with no extra rules. Never
 * reintroduce left/right here.
 *
 * ── NOT FIXABLE ─────────────────────────────────────────────────────────────
 * assets/css/custom.css is emitted unconditionally by the core, outside the
 * asset registry, after everything above (themes_helper.php:150-152). A buyer's
 * leftover customisations land on top of this file and cannot be suppressed.
 */

:root {
  --zd-sidebar-width: 230px;
  --zd-topbar-height: 57px;

  /*
   * THE CHROME, AND IT DEFAULTS TO THE ADMIN'S OWN COLOURS.
   *
   * Fallbacks only — theme_style overrides both surfaces at once via
   * `.customers .navbar-default`, which the topbar and the sidebar both carry
   * (template_parts/navigation.php:36, :54).
   *
   * These are not invented values. They are what Perfex's admin renders with
   * nothing configured, read off a running install: the topbar #ffffff, the
   * sidebar #f9f9f9, text #111827, the divider #cbd5e1. The point of an
   * admin-shaped portal is that a buyer who has set no colours anywhere sees
   * one product rather than two — and a dark navy chrome beside a white admin
   * reads as a different application, not as a theme.
   *
   * Two surface tokens rather than one because the admin really does use two:
   * the topbar is white and the sidebar a half-step off it. A buyer's Theme
   * Style colour still paints both the same, because that override targets the
   * shared .navbar-default class rather than these.
   */
  --zd-topbar-bg: #ffffff;
  --zd-sidebar-bg: #f9f9f9;
  --zd-chrome-fg: #111827;
  --zd-chrome-fg-muted: rgba(17, 24, 39, 0.62);
  --zd-chrome-hover-bg: rgba(17, 24, 39, 0.05);
  /*
     * Active sidebar item. Overridden per-request by the inline token block in
     * theme/head.php, which resolves the buyer's Theme Style setting (Customers
     * tab, added by this module) and falls back to their ADMIN active-item
     * colour. These literals are only the last-resort default, and they are the
     * admin's: white on the off-white sidebar, carried by weight rather than by
     * a colour the buyer never chose.
     */
  --zd-active-bg: #ffffff;
  --zd-active-fg: #111827;

  --zd-accent: #ff3366;

  --zd-content-bg: #f7f8fa;
  /*
     * Horizontal gutter between the chrome and the page content, replacing the
     * one Bootstrap's fixed-width .container used to produce by accident. Same
     * value the admin uses (.content{padding:25px}); drops to 15px below the
     * sidebar breakpoint, again matching the admin.
     */
  --zd-content-gutter: 25px;

  /*
   * Widest a standalone DOCUMENT gets — see the .zd-doc-page rule below for why
   * these four pages are capped when the rest of the portal is not. A token
   * rather than a literal so a buyer can retune it from their own Custom CSS
   * without having to out-specify our selector.
   */
  --zd-document-max: 1600px;

  /* Internal dividers, on the light chrome. */
  --zd-border: rgba(17, 24, 39, 0.1);
  /* The sidebar's outer edge — the same slate the admin sidebar uses. */
  --zd-sidebar-border: #cbd5e1;

  --zd-font:
    Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
  --zd-z-topbar: 1030;
  --zd-z-sidebar: 1020;
  --zd-z-backdrop: 1010;
}

/* ───────────────────────────── shell ───────────────────────────── */

body.zd-portal,
body.zazadesk-portal {
  background: var(--zd-content-bg);
  font-family: var(--zd-font);
}

/*
 * Bootstrap's .navbar-default carries a background, border, radius, min-height
 * and margin-bottom. The chrome elements borrow that class purely so
 * theme_style can colour them, so its box model has to be neutralised.
 * Specificity stays at 0,1,0 + element so the buyer's colour rule still wins.
 */
.zd-topbar.navbar-default,
.zd-sidebar.navbar-default {
  border: 0;
  border-radius: 0;
  margin-bottom: 0;
  min-height: 0;
}

/*
 * The sidebar's outer edge, re-applied AFTER the reset above and at the same
 * [0,2,0] specificity. It has to live here: `border: 0` in that reset beats the
 * declaration in the .zd-sidebar block ([0,1,0]) and silently erased it.
 * The .zd-sidebar rule keeps its own copy as a fallback for the case where the
 * .navbar-default compatibility class is ever dropped.
 */
.zd-sidebar.navbar-default {
  border-inline-end: 1px solid var(--zd-sidebar-border);
}

/* ───────────────────────────── top bar ───────────────────────────── */

.zd-topbar {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  height: var(--zd-topbar-height);
  z-index: var(--zd-z-topbar);

  display: flex;
  align-items: center;
  gap: 12px;
  padding-inline: 16px;

  background: var(--zd-topbar-bg);
  color: var(--zd-chrome-fg);
  /*
     * The admin's own header shadow, copied value for value. On the dark chrome
     * the bar separated itself; white on a #f7f8fa page does not, and without
     * an edge the topbar and the content read as one surface.
     */
  box-shadow:
    rgba(0, 0, 0, 0.1) 0 1px 3px 0,
    rgba(0, 0, 0, 0.1) 0 1px 2px -1px;
}

.zd-brand {
  display: flex;
  align-items: center;
  min-width: 0;
}

.zd-brand .zd-brand-logo,
.zd-brand img {
  max-height: 34px;
  width: auto;
}

/* Hamburger. Visible only below the breakpoint; the sidebar is permanent above it. */
.zd-menu-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 4px;

  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
}

.zd-menu-toggle:hover,
.zd-menu-toggle:focus-visible {
  background: var(--zd-chrome-hover-bg);
}

.zd-menu-toggle-bar {
  display: block;
  width: 20px;
  height: 2px;
  margin-inline: auto;
  border-radius: 2px;
  background: currentColor;
}

/* ───────────────────────────── sidebar ───────────────────────────── */

/*
 * position:fixed, not absolute.
 *
 * The admin sidebar is position:absolute with top:0;bottom:0, which resolves
 * against the initial containing block and therefore ends at the viewport
 * bottom. Admin papers over it at runtime with mainWrapperHeightFix() in
 * main.js, which sets #menu min-height to the document height on every resize.
 * That script does not run in the client area, so copying the absolute approach
 * would need us to reimplement it. Fixed positioning gets the same result with
 * no JavaScript and scrolls independently on long menus.
 */
.zd-sidebar {
  position: fixed;
  inset-block-start: var(--zd-topbar-height);
  inset-block-end: 0;
  inset-inline-start: 0;
  width: var(--zd-sidebar-width);
  z-index: var(--zd-z-sidebar);

  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;

  /*
     * Matches the admin sidebar exactly (style.css:511-521):
     *   padding-left: 6px; padding-right: 7px;
     *   border-right: 1px solid #cbd5e1;
     *
     * Written logically so RTL mirrors instead of putting the border on the
     * wrong edge. In LTR these compute to precisely the physical values above.
     *
     * Bootstrap sets * { box-sizing: border-box }, so the 6 + 7 + 1 comes out of
     * the 230px rather than adding to it — leaving a 216px content column, the
     * same width the admin's #side-menu measures.
     */
  padding-inline-start: 6px;
  padding-inline-end: 7px;

  background: var(--zd-sidebar-bg);
  border-inline-end: 1px solid var(--zd-sidebar-border);
}

.zd-user {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px;
  border-block-end: 1px solid var(--zd-border);
}

.zd-user-avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

.zd-user-meta {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.zd-user-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--zd-chrome-fg);
}

.zd-user-email,
.zd-nav-heading {
  font-size: 11px;
  color: var(--zd-chrome-fg-muted);
}

.zd-user-name,
.zd-user-email {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.zd-nav-wrap {
  display: flex;
  flex-direction: column;
  padding-block: 8px 24px;
}

/*
 * .navbar-nav is float:left + margin in Bootstrap 3 — has to go for a vertical
 * list. Reset it on the element, keeping specificity low.
 */
.zd-nav {
  list-style: none;
  margin: 0;
  padding: 0;
  float: none;
  width: 100%;
}

.zd-nav > li {
  float: none;
  display: block;
}

/*
 * TWO rules on purpose, at two specificities. Do not merge them.
 *
 * The stock theme sheet (theme-css, still registered) styles the horizontal
 * navbar with
 *   .navbar-default .navbar-nav>li>a { line-height:85px; display:inline;
 *                                     font-size:14.5px; margin-right:3px }
 * at [0,2,2]. Because the chrome keeps .navbar-default/.navbar-nav so the
 * buyer's theme_style colours still apply, it inherits that too — an 85px line
 * box designed for a top bar, which is what makes each sidebar row ~85px tall,
 * and display:inline, which kills the flex layout. A [0,1,2] rule cannot beat it
 * whatever the source order.
 *
 * So LAYOUT goes in the [0,2,2] rule below (ties with stock, wins on order),
 * and COLOUR stays in the [0,1,2] rule so theme_style's
 * `.customers .navbar-default .navbar-nav>li>a` at [0,3,2] still wins.
 * Moving `color` up into the layout rule would silently disable the buyer's
 * colour picker.
 */
.zd-nav > li > a {
  color: var(--zd-chrome-fg);
}

.zd-sidebar .zd-nav > li > a {
  display: flex;
  align-items: center;
  gap: 10px;

  padding: 10px 16px;
  margin: 0;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.4;
  text-decoration: none;
  white-space: normal;
}

.zd-sidebar .zd-nav > li > a:hover,
.zd-sidebar .zd-nav > li > a:focus-visible {
  background: var(--zd-chrome-hover-bg);
}

/*
 * Active item. Marked by $item['href'] === current_full_url(), same as stock.
 *
 * Background and text colour are buyer-configurable: this module adds
 * "Sidebar Active Item Background/Text Color" to Setup -> Theme Style ->
 * Customers via the get_styling_areas filter, mirroring the admin sidebar's
 * admin-menu-active-item. Nothing here is a hardcoded accent.
 *
 * Background swap only — no leading bar. There is deliberately no [dir="rtl"]
 * counterpart rule: it existed solely to mirror that bar's inset shadow.
 */
.zd-sidebar .zd-nav > li.active > a {
  background: var(--zd-active-bg);
  color: var(--zd-active-fg);
  font-weight: 600;
}

/* Only an entry without a name keeps an icon (template_parts/navigation.php). */
.zd-sidebar .zd-nav > li > a > i {
  width: 18px;
  text-align: center;
  flex-shrink: 0;
}

.zd-badge {
  margin-inline-start: auto;
  background: var(--zd-accent);
  color: #fff;
  border-radius: 10px;
  padding: 1px 7px;
  font-size: 11px;
}

/* Account block */

.zd-nav-account {
  margin-block-start: 12px;
  padding-block-start: 12px;
  border-block-start: 1px solid var(--zd-border);
}

.zd-nav-heading {
  padding: 6px 16px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
}

/*
 * Language selector — native <details>, no JS.
 *
 * The summary is the clickable row: label on the inline-start, the currently
 * selected language on the inline-end, and a chevron that rotates when open.
 */

/*
 * Divider above the selector, separating the account links from the language
 * switcher. Same treatment as .zd-nav-account so the two rules read as one
 * pattern. Deliberately on the <li> rather than a separate divider element:
 * when is_language_disabled() removes the selector the rule goes with it, so
 * there is never a stray rule floating above Logout.
 */
.zd-nav-group {
  margin-block-start: 8px;
  padding-block-start: 8px;
  border-block-start: 1px solid var(--zd-border);
}

.zd-nav-group-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--zd-chrome-fg-muted);
  cursor: pointer;
  list-style: none;
}

/* Both are needed: the pseudo-element for WebKit, list-style for the rest. */
.zd-nav-group-label::-webkit-details-marker {
  display: none;
}

.zd-nav-group-label:hover,
.zd-lang[open] > .zd-nav-group-label {
  color: var(--zd-chrome-fg);
}

.zd-nav-group-label:focus-visible {
  /*
     * --zd-accent, not --zd-active-bg. The active item's background is now
     * white by default, and a white ring on a white sidebar is no ring at all.
     * The other focus rules in this file already use the accent; this one was
     * the odd out.
     */
  outline: 2px solid var(--zd-accent);
  outline-offset: -2px;
}

/* Current language, pushed to the far edge and not shouty like the label. */
.zd-lang-current {
  margin-inline-start: auto;
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
  color: var(--zd-chrome-fg);
}

/* Chevron drawn in CSS so it needs no icon font — fontawesome may be stripped. */
.zd-nav-group-label::after {
  content: '';
  width: 6px;
  height: 6px;
  flex-shrink: 0;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}

.zd-lang[open] > .zd-nav-group-label::after {
  transform: rotate(45deg);
}

@media (prefers-reduced-motion: reduce) {
  .zd-nav-group-label::after {
    transition: none;
  }
}

.zd-nav-sub {
  list-style: none;
  margin: 0;
  padding: 0;
}

.zd-nav-sub > li > a {
  display: block;
  padding: 7px 16px 7px 34px;
  font-size: 13px;
  text-decoration: none;
  color: var(--zd-chrome-fg-muted);
}

[dir='rtl'] .zd-nav-sub > li > a {
  padding: 7px 34px 7px 16px;
}

.zd-nav-sub > li > a:hover,
.zd-nav-sub > li.active > a {
  background: var(--zd-chrome-hover-bg);
  color: var(--zd-chrome-fg);
}

.zd-nav-tail:empty {
  display: none;
}

/* ───────────────────────────── content ───────────────────────────── */

/*
 * The offset. One declaration, mirroring the sidebar width — same idea as the
 * admin's #wrapper { margin-left: 230px }, but logical so RTL needs no
 * counterpart rule.
 */
.zd-shell .zd-main {
  margin-inline-start: var(--zd-sidebar-width);
  padding-block-start: var(--zd-topbar-height);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Pages that called disableNavigation() render with no sidebar and no top bar. */
.zd-shell-bare .zd-main {
  margin-inline-start: 0;
  padding-block-start: 0;
}

.zd-content {
  flex: 1 0 auto;
  padding-block: 20px;
}

/*
 * The gutter is owned by .container alone (below). This rule stops the stock
 * sheet contributing a second one: style.min.css carries
 * `@media (max-width:768px){#content{padding:15px}}`, and #content is [1,0,0],
 * so it beats every class selector in this file — our own padding-block
 * included. Matching on the id here ([1,2,0]) is what takes it back.
 */
.zd-shell #content.zd-content {
  padding-block: 20px;
  padding-inline: 0;
}

/*
 * Bootstrap's .container is FIXED-width, not fluid: 750px at >=768, 970px at
 * >=992, 1170px at >=1200 (bootstrap.css:1589-1602). Stock Perfex gets away with
 * that because its portal is full-bleed — the auto margins centre the container
 * and the leftover viewport becomes the gutter.
 *
 * Here the container lives inside .zd-main, which is 230px narrower than the
 * viewport. So on a stock install the fixed width exceeds its parent for a wide
 * band:
 *
 *   viewport   available   .container   result
 *    768px       538px       750px      overflows by 212px
 *    992px       762px       970px      overflows by 208px
 *   1200px       970px      1170px      overflows by 200px
 *   1430px      1200px      1170px      fits — gutters finally appear
 *
 * When an auto-margin box is over-constrained the margins resolve to 0, so the
 * container pins to the inline start and spills off the end: content touching
 * the sidebar on one side and clipped on the other, from 768px all the way to
 * ~1430px. Capping with max-width would only re-centre it — the gutter would
 * still be whatever the viewport happened to leave over.
 *
 * The admin does not use .container at all; its #content is fluid with a flat
 * padding (style.min.css: .content{padding:25px}, 15px below 768px). Matching
 * that is both the fix and the thing this theme is already imitating, so the
 * container is made fluid with an explicit gutter that no longer depends on the
 * viewport.
 *
 * This applies to .zd-shell-bare too — the public invoice, estimate, proposal,
 * contract, subscription, consent and web-to-lead pages, every
 * disableNavigation() caller. It once did not: those pages have no sidebar, so
 * nothing forced the change, and leaving them centred kept a 27" monitor from
 * rendering an invoice one line of numbers wide.
 *
 * Two things settled it the other way. The portal is now full-bleed everywhere
 * else, so a document opening 430px narrower than the page it was linked from
 * reads as a rendering fault rather than a choice. And the documents themselves
 * changed: the section library turned proposal bodies into laid-out documents —
 * hero blocks, two-column rows, images — and those are exactly the content that
 * a fixed 1170px column wastes. Line length stays reasonable regardless, because
 * the document body is .col-md-9 with a summary rail beside it, never the full
 * measure.
 *
 * One deliberate side effect: [0,2,0] here outranks a bare `.container` rule in
 * the buyer's Setup -> Theme Style -> Custom CSS, which renders inline as
 * <style id="theme_style_custom_css"> and would otherwise win on source order.
 * That override is wanted — a hand-rolled
 * `@media (min-width:1200px){.container{width:100%;max-width:100vw}}` is the
 * usual workaround for the very fixed-width container this rule removes, and
 * left to win it would reintroduce the inconsistency at exactly 1200px.
 *
 * No upper cap. It carried max-width: 2000px for a while, which put the portal
 * back into the shape this rule exists to get it out of — content pinned to one
 * side of an ultra-wide screen with the leftover viewport as the gutter, just at
 * a wider breakpoint than Bootstrap's. 100% keeps it flush with .zd-main at
 * every width, matching the admin, whose #content is fluid with a flat padding.
 */
.zd-shell .container {
  width: 100%;
  max-width: 100%;
  padding-inline: var(--zd-content-gutter);
}

/*
 * ...EXCEPT ON THE FOUR STANDALONE DOCUMENTS, which are capped.
 *
 * The rule above argues that full-bleed is right for the PORTAL, and it is:
 * every screen the customer navigates between is flush with .zd-main, so a page
 * that suddenly centred itself would read as a rendering fault. That argument
 * does not carry to an invoice, an estimate, a proposal or a contract, because
 * those are not portal screens. They are documents, opened from a link in an
 * email, usually by somebody who never sees the portal at all — there is no
 * neighbouring screen for them to be consistent with, and the thing they most
 * need to resemble is the PDF of themselves.
 *
 * The failure the cap fixes is line length rather than gutters. An invoice body
 * is a full-measure table; on a 27" monitor at 2560px the description column
 * runs past 2000px and a two-word line item sits alone at the far left of the
 * row from its amount. That is the case the earlier `max-width: 2000px` was
 * reaching for and set too wide to catch.
 *
 * 1600px, and the number is the document's own shape rather than a round
 * figure: an A4 page at 96dpi is 794px, so two of them side by side is 1588px
 * — which is what these views render as once the summary rail sits beside the
 * body. Wider stops looking like the document it is a preview of.
 *
 * SCOPED BY CORE'S OWN BODY CLASS, one per page and each set in exactly one
 * place — Invoice.php:73, Estimate.php:113, Proposal.php:128, Contract.php:57.
 * Not by .zd-shell-bare, which would be the tempting shortcut and is wrong: it
 * also covers the subscription, consent and web-to-lead pages, which are forms
 * rather than documents and were not asked for.
 *
 * The nested .container in the preview bar is matched too, and deliberately.
 * When sticky.js pins that bar, stock CSS stretches it to the viewport
 * (style.css:1694-1703), so its inner container is measured against the screen
 * rather than the capped one below it — capping both is what keeps the document
 * number above the document it labels. The auto margins Bootstrap already puts
 * on .container do the centring; nothing here needs to restate them.
 */
body.viewinvoice .zd-shell .container,
body.viewestimate .zd-shell .container,
body.proposal-view .zd-shell .container,
body.contract-view .zd-shell .container {
  max-width: var(--zd-document-max);
}

/*
 * Several stock views nest a .container of their own inside the one index.php
 * opens — reset_password.php:2, and the .preview-sticky-container in the
 * proposal/estimate/invoice previews. Nested, the gutter would apply twice.
 * The outer one owns it.
 */
.zd-shell .container .container {
  padding-inline: 0;
}

/*
 * ...except while the preview bar is stuck.
 *
 * The document previews hand .top to sticky.js, which on scroll sets
 * position:fixed and leaves the element where it is in the DOM. Stock CSS then
 * pins it edge to edge (style.css:1694-1703, `width:100%!important;left:0`), so
 * its nested .container is suddenly measured against the VIEWPORT rather than
 * against the outer container that was paying the gutter — and the proposal
 * number jumps 25px left of the document it belongs to, on every scroll.
 *
 * Giving the gutter back for exactly the stuck state keeps the bar aligned with
 * the content under it. data-sticky-class is added and removed on the element
 * itself (sticky.js:275-291), so this needs no JS of ours.
 */
.zd-shell .preview-sticky-header > .container {
  padding-inline: var(--zd-content-gutter);
}

/*
 * Below 768px the stuck bar pads ITSELF — 20px in stock (style.css:1705-1710),
 * 15px again for .mobile (:1720) — which stacks on the gutter above and walks
 * the proposal number 20px right of the document it labels. Stock has the same
 * offset; it is simply easier to see now that the two are otherwise flush.
 * Dropping the bar's own inline padding leaves the gutter as the only source.
 */
@media (max-width: 767.98px) {
  .zd-shell .preview-sticky-header {
    padding-inline: 0;
  }
}

/*
 * The wrapper rows index.php opens are layout scaffolding, not grid rows, so
 * their Bootstrap -15px margin is dropped.
 *
 * In a well-formed BS3 grid that negative margin is paid back by the 15px
 * padding on the columns inside it. The client-area views almost never put a
 * column there: of the 46 stock views, exactly one (register.php) opens on a
 * col-*; the rest start with a heading, a .panel_s, or a .row of their own. So
 * the -15px normally goes uncompensated and eats 15 of the 25px gutter — which
 * is why the panels sat 10px off the sidebar rather than 25.
 *
 * The child combinators are load-bearing: this must hit ONLY the two rows in
 * index.php. A view's own nested .row keeps its negative margin, so its columns
 * still pay it back and land on the same 25px line. Both dominant patterns end
 * up flush; the register.php outlier is a centred, offset form where the extra
 * 15px is invisible.
 *
 * The footer keeps its margin for the same reason — .zd-footer > .container >
 * .row > .col-md-12 is a real grid row.
 */
.zd-shell .zd-content > .container > .row {
  margin-inline: 0;
}

.zd-submenu {
  list-style: none;
  display: flex;
  gap: 18px;
  justify-content: flex-end;
  width: 100%;
  margin: 0 0 8px;
  padding: 0;
}

/*
 * padding-inline: 0 for the same reason as #content above — stock .footer
 * carries `padding: 15px`, which would stack a second gutter on top of the
 * container's and push the copyright line 15px past the content column. The
 * footer keeps its .row margin, though: unlike index.php's wrapper rows, this
 * one really is a grid row with a .col-md-12 inside paying the -15px back.
 */
.zd-footer {
  flex-shrink: 0;
  padding-block: 18px;
  padding-inline: 0;
  font-size: 12px;
}

/* ───────────────────────────── responsive ───────────────────────────── */

/*
 * Off-canvas below 768px, matching the admin's breakpoint. The admin drives
 * this with body classes set by its own main.js; the client area has no such
 * script, so assets/js/theme.js toggles .zd-sidebar-open on <body>.
 *
 * Sidebar state is CSS-only from here down — the JS only flips one class.
 */
@media (max-width: 767.98px) {
  /* Narrower gutter on phones, as the admin does. */
  :root {
    --zd-content-gutter: 15px;
  }

  .zd-menu-toggle {
    display: flex;
  }

  .zd-sidebar {
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    width: min(var(--zd-sidebar-width), 82vw);
  }

  [dir='rtl'] .zd-sidebar {
    transform: translateX(100%);
  }

  body.zd-sidebar-open .zd-sidebar {
    transform: translateX(0);
  }

  .zd-shell .zd-main {
    margin-inline-start: 0;
  }

  .zd-sidebar-backdrop {
    position: fixed;
    inset: 0;
    z-index: var(--zd-z-backdrop);
    background: rgba(0, 0, 0, 0.45);
  }

  body.zd-sidebar-open {
    overflow: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  .zd-sidebar {
    transition: none;
  }
}

/* ───────────────────────────── a11y ───────────────────────────── */

/*
 * Perfex's stock portal has no focus-visible styling and no skip link. Keeping
 * a visible focus ring is the minimum; a reset that removes outlines without
 * replacing them is the single most common way a theme becomes unusable by
 * keyboard.
 */
.zd-sidebar a:focus-visible,
.zd-topbar button:focus-visible {
  outline: 2px solid var(--zd-accent);
  outline-offset: -2px;
}

@media print {
  .zd-topbar,
  .zd-sidebar,
  .zd-sidebar-backdrop {
    display: none;
  }

  .zd-shell .zd-main {
    margin-inline-start: 0;
    padding-block-start: 0;
  }
}

/*
 * ===========================================================================
 * THE SIGN-IN CARD
 * ===========================================================================
 *
 * Login, Forgot password and Reset password. Stock lays these out as
 * col-md-4 col-md-offset-4 — a PERCENTAGE of the container — which was fine
 * while .container was Bootstrap's fixed 1170px. This theme made the container
 * full-bleed, so a third of it is a third of the monitor: measured 380px at
 * 1280, 593px at 1920 and 807px at 2560. A login form should not grow with the
 * display; past a point it stops reading as a form at all.
 *
 * So the column is capped rather than re-columned. Overriding stock's login.php
 * to change the class would fork core markup this module does not otherwise
 * touch, and a Perfex release adding a field to that form — a consent
 * checkbox, a different captcha — would then never reach the buyer's screen.
 *
 * Both children of the row are capped: the heading sits in its own col-md-4
 * beside the form's, so leaving it uncapped would centre a wide heading over a
 * narrow card.
 *
 * float and margin-inline undo .col-md-offset-4's 33.33% margin-left, which
 * would otherwise push the now-narrow column off-centre. Our selector is four
 * classes deep against Bootstrap's one, so it wins without !important.
 *
 * Not applied to register: that form is col-md-10 with col-md-6 pairs inside,
 * and this cap would stack it into a ladder. zazadesk_theme_signin_page()
 * excludes it.
 */
.zd-signin {
  /* The visible card. The column adds the two 15px gutters on top. */
  --zd-signin-width: 360px;
}

.zd-signin .zd-content .container .col-md-4 {
  max-width: calc(var(--zd-signin-width) + 30px);
  float: none;
  margin-inline: auto;
}
