/* ============================================================================
   TAK DNS — visual system
   ----------------------------------------------------------------------------
   Apple-flavoured: near-black canvas, one accent used sparingly, very large
   display type with tight tracking, generous air, and depth built from soft
   light rather than hard borders.

   Motion uses a single expo-out curve everywhere. Mixing easing curves is the
   fastest way to make an interface feel assembled from parts instead of
   designed, so there is exactly one.
   ========================================================================== */

/* The `hidden` attribute must win over any class that sets `display`.
   ----------------------------------------------------------------------------
   The browser's own rule for `[hidden]` is a bare attribute selector, so any
   class rule beats it — `.btn { display: inline-flex }` among them. That made
   `button.hidden = true` do nothing at all: the plan guide showed «بعدی» and
   «بستن» side by side on its last page, and the order button on every page,
   with the JavaScript setting the property correctly the whole time. Anything
   whose visibility is driven from script relies on this rule. */
[hidden] { display: none !important; }

@font-face {
  font-family: "Vazirmatn";
  src: url("../fonts/Vazirmatn[wght].f6d31671339d.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-display: swap;
  font-style: normal;
}

:root {
  color-scheme: dark;

  /* canvas ------------------------------------------------------------- */
  /* Warmed off pure blue-black. #0A0908 carries a trace of brown, which
     reads as "dim room" rather than "switched-off screen" — the same trick
     good dark themes use to stop long sessions feeling clinical. */
  --bg:        #0B0A09;
  --bg-soft:   #131110;
  --elevated:  rgba(255, 255, 255, .035);
  --elevated-2:rgba(255, 255, 255, .062);
  --hairline:  rgba(255, 255, 255, .085);
  --hairline-2:rgba(255, 255, 255, .15);

  /* type --------------------------------------------------------------- */
  /* Text warmed to match the canvas. Pure #F2F4F8 on a warm background
     looks faintly blue and tires the eye over a long page. */
  --label:     #F6F2EC;
  --label-2:   rgba(246, 242, 236, .64);
  --label-3:   rgba(246, 242, 236, .40);

  /* accent — warm teal into soft amber ----------------------------------
     The old pair was glacier blue and mint: correct, cold, and a little
     clinical for something people sit with. Teal keeps the calm and the
     contrast; amber gives the warmth. They sit opposite each other on the
     wheel, so the gradient between them stays clean rather than muddy. */
  --tint:      #4FC3B0;
  --tint-2:    #F0B67F;
  --tint-soft: rgba(79, 195, 176, .12);
  --tint-line: rgba(79, 195, 176, .28);
  --mint-soft: rgba(240, 182, 127, .12);
  --mint-line: rgba(240, 182, 127, .28);
  --warn:      #F2C57C;
  --warn-soft: rgba(242, 197, 124, .12);
  --bad:       #E8877F;
  --bad-soft:  rgba(232, 135, 127, .12);

  /* The connected/disconnected pair on the landing card. Deliberately more
     saturated than --tint and --bad: those are interface colours chosen to sit
     quietly, and this one switch is meant to be read across the room. */
  --on:        #22C55E;
  --off:       #EF4444;

  /* «somebody is waiting» — not an error, not routine. Used by the unread
     ticket counters.

     ⚠️ TWO VALUES, and the reason is written out at length further down for
     the trial accent: #F97316 is 7.06:1 on this canvas and 2.65:1 on the cream
     one, which is not legible text by any standard. The light theme runs the
     darker orange at 4.89:1. `--alert-ink` is the colour of an orange SURFACE's
     label and is near-black in both themes, because the surface itself does
     not change. */
  --alert:      #F97316;
  --alert-ink:  #1C0F02;
  --alert-soft: rgba(249, 115, 22, .09);
  --alert-line: rgba(249, 115, 22, .35);

  /* form --------------------------------------------------------------- */
  --r-sm: .75rem;
  --r:    1.25rem;
  --r-lg: 2rem;
  --r-pill: 999px;

  --lift:   0 .75rem 2.5rem -.75rem rgba(0, 0, 0, .55);
  --lift-2: 0 2rem 5rem -1.5rem rgba(0, 0, 0, .7);
  --glow:   0 0 0 1px var(--tint-line), 0 1rem 3rem -1rem var(--tint-soft);

  --ease: cubic-bezier(.16, 1, .3, 1);
  --font: "Vazirmatn", -apple-system, BlinkMacSystemFont, "SF Pro Display",
          "Segoe UI", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;

  --wrap: 62rem;
}

:root[data-theme="light"] {
  color-scheme: light;

  /* Not white. A hint of warmth off the page takes the glare out of a
     bright room without reading as "cream". */
  --bg:        #FBF8F4;
  --bg-soft:   #FFFDFA;
  --elevated:  rgba(60, 42, 26, .028);
  --elevated-2:rgba(60, 42, 26, .055);
  --hairline:  rgba(60, 42, 26, .09);
  --hairline-2:rgba(60, 42, 26, .17);

  --label:     #1C1917;
  --label-2:   rgba(28, 25, 23, .66);
  --label-3:   rgba(28, 25, 23, .44);

  /* Darker than the dark-theme pair: the same hues need more depth to hold
     contrast on a light page. Measured, not guessed — both clear 4.5:1
     against the canvas, which is the bar for normal-size text. The service
     address in the guide is set in --tint at small size, so "good enough for
     headings" was not good enough here. */
  --tint:      #0D7A6F;
  --tint-2:    #A85F24;
  --tint-soft: rgba(13, 122, 111, .10);
  --tint-line: rgba(13, 122, 111, .24);
  --mint-soft: rgba(168, 95, 36, .10);
  --mint-line: rgba(168, 95, 36, .24);
  --warn:      #9A6410;
  --warn-soft: rgba(154, 100, 16, .10);
  --bad:       #C4453C;
  --bad-soft:  rgba(196, 69, 60, .10);

  /* Deeper on the light canvas, same reason every other pair is. */
  --on:        #15803D;
  --off:       #DC2626;
  /* 4.89:1 here, against #F97316's 2.65:1. The soft/line washes stay in the
     bright hue: a wash is a surface, and darkening it would make it read as
     brown rather than as a highlight. */
  --alert:      #C2410C;
  --alert-soft: rgba(249, 115, 22, .08);
  --alert-line: rgba(249, 115, 22, .32);

  /* Shadows tinted with the page rather than neutral grey, so cards look
     lit by the same light as everything around them. */
  --lift:   0 .75rem 2.5rem -1rem rgba(60, 42, 26, .13);
  --lift-2: 0 2rem 4rem -1.5rem rgba(60, 42, 26, .17);
}

/* ------------------------------------------------- trial accent: orange
   Opt-in, so it changes nothing for anyone who has not asked for it. Set
   with ?accent=orange and cleared with ?accent=off; the choice is kept in
   this browser only, which is what makes it safe to try on the live site.

   It overrides ONLY the accent variables. Canvas, text and shadows are the
   theme's job, and repainting those from a logo is how a site ends up
   orange-on-orange.

   ⚠️ The two themes do NOT get the same orange, and this is the whole reason
   the trial is not one line. The logo's #F97316 measures 7.06:1 on the dark
   canvas — comfortable — and 2.65:1 on the light one, which is not legible
   body text by any standard. The light theme therefore runs a darker orange
   at 4.89:1, which is where the teal it replaces already sat (4.92:1). Every
   number here was measured against the actual canvas, not chosen by eye. */
:root[data-accent="orange"] {
  --tint:      #F97316;   /* 7.06:1 on --bg  */
  --tint-2:    #FDBA5C;   /* 11.64:1         */
  --tint-soft: rgba(249, 115, 22, .12);
  --tint-line: rgba(249, 115, 22, .28);
  --mint-soft: rgba(253, 186, 92, .12);
  --mint-line: rgba(253, 186, 92, .28);
}
:root[data-accent="orange"][data-theme="light"] {
  --tint:      #C2410C;   /* 4.89:1 on the light canvas */
  --tint-2:    #9A3412;   /* 6.90:1                      */
  --tint-soft: rgba(194, 65, 12, .10);
  --tint-line: rgba(194, 65, 12, .24);
  --mint-soft: rgba(154, 52, 18, .10);
  --mint-line: rgba(154, 52, 18, .24);
}
/* The drifting background lights are teal and mint by default; left alone
   they keep a cool cast on a warm page. */
:root[data-accent="orange"] .aurora::before {
  background: radial-gradient(circle, rgba(249, 115, 22, .38), transparent 68%);
}
:root[data-accent="orange"] .aurora::after {
  background: radial-gradient(circle, rgba(253, 186, 92, .30), transparent 68%);
}

/* ---------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

body {
  font-family: var(--font);
  font-size: 1.0625rem;
  line-height: 1.65;
  letter-spacing: -.005em;
  color: var(--label);
  background: var(--bg);
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Two slow-drifting light sources. Fixed so the page seems to move through
   them rather than carry them, and behind everything so they never eat a tap. */
.aurora {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  overflow: hidden;
}
.aurora::before,
.aurora::after {
  content: "";
  position: absolute;
  width: 46rem; height: 46rem;
  border-radius: 50%;
  filter: blur(90px);
  opacity: .55;
}
.aurora::before {
  background: radial-gradient(circle, rgba(90,200,250,.45), transparent 68%);
  top: -18rem; inset-inline-end: -14rem;
  animation: drift-a 26s var(--ease) infinite alternate;
}
.aurora::after {
  background: radial-gradient(circle, rgba(100,228,192,.34), transparent 68%);
  bottom: -22rem; inset-inline-start: -16rem;
  animation: drift-b 32s var(--ease) infinite alternate;
}
:root[data-theme="light"] .aurora::before,
:root[data-theme="light"] .aurora::after { opacity: .4; }

@keyframes drift-a { to { transform: translate3d(-6rem, 5rem, 0) scale(1.12); } }
@keyframes drift-b { to { transform: translate3d(5rem, -4rem, 0) scale(1.08); } }

/* A whisper of grain. Flat gradients band on cheap panels; noise hides it. */
.grain {
  position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .022;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

img, svg { display: block; max-width: 100%; height: auto; }
a { color: var(--tint); text-decoration: none; transition: opacity .25s var(--ease); }
a:hover { opacity: .78; }
:focus-visible { outline: 2px solid var(--tint); outline-offset: 3px; border-radius: .5rem; }
::selection { background: var(--tint-soft); }

/* ---------------------------------------------------------------- layout */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: 1.5rem; }
.narrow { max-width: 44rem; }
main { flex: 1; }

.section { padding-block: clamp(4rem, 11vw, 8rem); }
.section-head { text-align: center; margin-bottom: clamp(2.5rem, 6vw, 4rem); }

h1, h2, h3 { font-weight: 700; letter-spacing: -.028em; line-height: 1.12; }
h1 { font-size: clamp(2.6rem, 7.5vw, 4.75rem); letter-spacing: -.04em; line-height: 1.04; }
h2 { font-size: clamp(1.9rem, 4.6vw, 3rem); }
h3 { font-size: 1.125rem; letter-spacing: -.02em; line-height: 1.35; }

/* Persian headings need more leading than Latin ones once they wrap.
   ----------------------------------------------------------------------------
   1.12 is a tight, deliberate figure for a one-line display heading, and on a
   wide screen every heading here is one line. On a phone they wrap, and Persian
   wraps badly at that value: descenders like ی and ج reach into the line below
   and the ascender of the next line comes up to meet them, so two lines read as
   one dense block. «چه سرویس‌هایی در دسترس قرار می‌گیرند» was the one that made
   it obvious.

   Applied by width rather than by line count, because CSS cannot ask how many
   lines there are — and at these widths a single-line heading is short enough
   that the extra leading costs nothing. */
@media (max-width: 48rem) {
  h1 { line-height: 1.25; }
  h2 { line-height: 1.35; }
  h3 { line-height: 1.5; }
}

.lead {
  font-size: clamp(1.0625rem, 2.1vw, 1.3125rem);
  line-height: 1.55; color: var(--label-2); letter-spacing: -.012em;
  max-width: 36rem; margin-inline: auto;
}
/* Must come AFTER the rule above, not with the heading overrides further up:
   the two selectors are equally specific, so the later one wins and a mobile
   override written above `.lead` does exactly nothing. Measured — it was
   still reporting 1.55 on a 375px screen. */
@media (max-width: 48rem) {
  .lead { line-height: 1.85; }
}
.eyebrow {
  /* .8125rem × 1.35 × 1.35 = 1.4808rem — 35% asked for twice, on two separate
     readings of the live page, so the running total is written out rather than
     left as a number nobody can retrace.

     One rule, not a modifier on the two labels that were named: «راهنما»,
     «ویندوز», «سرور و شبکه» and «ویدیو» are the same kind of label doing the
     same job, and two of four at a different size reads as a mistake rather
     than as emphasis. */
  font-size: 1.4808rem; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; color: var(--tint);
}
/* A heading that stands in for the eyebrow-plus-title pair: heading size,
   eyebrow colour. Used where a section needs one line, not two. */
.section-head h2.h2-accent { color: var(--tint); }

/* ---------------------------------------------- floating Telegram button
   Bottom-left on every page. `left`, not `inset-inline-start`: the site is
   RTL, so the logical property lands bottom-right, which is the opposite
   corner from the one that was asked for.

   Telegram's own blue rather than --tint. A brand mark repainted in the site
   accent stops being recognisable at 44px, and recognisable is the entire
   reason to put an icon there instead of the word «تلگرام». It therefore also
   ignores the operator palette, deliberately.

   `env(safe-area-inset-*)` keeps it clear of the home indicator on a phone,
   where the bottom 34px is not really the bottom. */
.tg-fab {
  position: fixed;
  left: max(1.1rem, env(safe-area-inset-left));
  bottom: max(1.1rem, env(safe-area-inset-bottom));
  z-index: 55;                       /* under the header (60), over content */
  width: 3rem; height: 3rem;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: #229ED9; color: #fff;
  box-shadow: 0 .5rem 1.5rem -.4rem rgba(34, 158, 217, .55);
  transition: transform .35s var(--ease), box-shadow .35s var(--ease);
}
/* The glyph is optically off-centre in Telegram's own mark — the tail of the
   plane hangs left — so it is nudged rather than left looking misaligned. */
.tg-fab svg { transform: translateX(-1px); }
.tg-fab:hover {
  transform: translateY(-2px);
  box-shadow: 0 .75rem 2rem -.4rem rgba(34, 158, 217, .7);
}
.tg-fab:focus-visible { outline: 2px solid var(--tint); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
  .tg-fab, .tg-fab:hover { transition: none; transform: none; }
  .tg-fab svg { transform: translateX(-1px); }
}
/* The room the footer leaves for it is set with the footer's own rule further
   down this file, not here. `padding-block` there is a shorthand and would
   overwrite a `padding-bottom` written above it — the same source-order trap
   that has already cost this stylesheet two bugs. */
.tint-text {
  background: linear-gradient(102deg, var(--cta-ink) 8%, var(--cta-ink-2) 92%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}

.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 15.5rem), 1fr)); }

/* Three short readings — load, memory, disk — always on one row, labels above
   values. They were on `.grid-3`, which is a *card* grid: it demands 15.5rem a
   column and reflows with auto-fit when that will not fit. Inside a machine
   card it never fits, so the three stats broke across lines in whatever
   arrangement the width happened to allow — «دیسک» dropped to its own row and
   the load figure ended up beside the memory figure rather than under its own
   label. These values are a handful of characters and always fit; they should
   not reflow at all. */
.stat-trio {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .75rem;
}
/* The same row when there are more than three figures and their number is not
   fixed. `auto-fit` rather than a count, because seven does not divide into a
   phone and hard-coding four columns would only move the problem to the next
   figure somebody adds. The minimum is the width of a grouped Toman number
   plus its label — below that the digits wrap mid-number, which reads as a
   different amount. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.9rem, 1fr));
  gap: 1rem .55rem;
}
/* ⚠️ These sizes are what keeps seven figures on ONE line, and they were
   measured rather than guessed. The card sits in a 62rem wrap: minus the page
   padding and the card's own `clamp(1.75rem, 5vw, 3rem)` there are about 53rem
   inside it, and seven columns plus six gaps need 7 × 6.4 + 6 × 0.6 ≈ 48rem.
   The widest value is «2,200,000» — nine grouped digits — which needs roughly
   6.1rem at 1.0625rem and would need 7.4rem at the 1.375rem this started at.
   That is the whole reason the row was wrapping into 5 + 2.
   Auto-fitting here means the type gets smaller, never the information. */
.stat-row .stat-num { font-size: 1.17rem; line-height: 1.3; }
.stat-row .stat-label { font-size: .8125rem; }
.stat-row .faint { font-size: .8125rem; }

/* ---------------------------------------------------------------- header */
.site-header {
  position: sticky; top: 0; z-index: 60;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: saturate(180%) blur(22px);
  -webkit-backdrop-filter: saturate(180%) blur(22px);
  border-bottom: 1px solid transparent;
  transition: border-color .35s var(--ease), background .35s var(--ease);
}
.site-header.is-stuck { border-bottom-color: var(--hairline); }
.site-header .wrap { display: flex; align-items: center; gap: .75rem; height: 3.25rem; }

.brand {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: .9375rem; font-weight: 600; letter-spacing: -.015em; color: var(--label);
}
.brand:hover { opacity: 1; }
/* The TAK DNS orange, taken from the logo. Scoped to the mark rather than
   swapped into --tint, because --tint is the accent the whole interface is
   built on — links, focus rings, chart series, the light-theme contrast pair
   that was measured to clear 4.5:1. Repainting all of that from a logo is a
   design decision of its own, not a side effect of changing a name. */
:root {
  --brand:   #F97316;
  --brand-2: #FDBA5C;
}
/* The logo is transparent, so the page shows through and it needs no plate
   behind it in either theme. Height is fixed and the width follows, because
   the artwork is wide (609x291) and constraining the wrong axis would let it
   grow out of the header on a narrow screen. */
.brand img {
  height: 1.75rem;
  width: auto;
  display: block;
}
@media (max-width: 30rem) { .brand img { height: 1.5rem; } }

.nav { margin-inline-start: auto; display: flex; align-items: center; gap: .15rem; }
.nav a {
  padding: .4rem .7rem; border-radius: var(--r-sm);
  font-size: .8125rem; font-weight: 500; color: var(--label-2);
  transition: color .25s var(--ease), background .25s var(--ease);
}
/* Plain links only — never the buttons sitting in the same row.
   ----------------------------------------------------------------------------
   `background` is a shorthand, so this rule was resetting `background-image` to
   none. On «شروع» that wiped the `--cta` gradient and left
   `rgba(255,255,255,.035)`, while the near-black label stayed put because
   `.nav a.btn-primary` is the more specific colour rule. Hovering the main call
   to action in dark mode turned it into black text on a black bar — and only in
   dark mode, because the same wash over the cream canvas is still readable,
   which is why it survived. Each button already has its own hover. */
.nav a:not(.btn):hover { color: var(--label); background: var(--elevated); opacity: 1; }
.nav form { display: inline-flex; }

/* The header menu on small screens.
   ----------------------------------------------------------------------------
   48rem is measured, not chosen. The widest header this site renders is the
   staff one — nine links, the logo and the theme button — which needs 725px to
   lay out; the reseller's needs 663 and a plain customer's 591. Below 768px the
   row could not fit and simply overflowed, pushing 133px of every page off to
   the side with `flex-wrap: nowrap` and no scroll container to catch it. The
   table on the orders page was blamed for this first; the header was doing it
   everywhere. */
.nav-toggle {
  display: none;
  place-items: center; width: 2.25rem; height: 2.25rem;
  border: 1px solid var(--hairline); border-radius: var(--r-pill);
  background: var(--elevated); color: var(--label-2); cursor: pointer;
  transition: color .25s var(--ease), border-color .25s var(--ease);
}
.nav-toggle:hover { color: var(--label); border-color: var(--hairline-2); }

/* ------------------------------------------- unread markers
   An orange dot on the closed menu, and a count on the link inside it.

   ⚠️ `left` and `top`, not the logical properties. The page is RTL, so
   `inset-inline-start` puts both of these on the right — the corner nobody
   asked for, and on the hamburger the one already occupied by the bars'
   optical centre.

   Orange, not --tint and not --bad: the accent would read as decoration next
   to every other teal thing in the header, and red says «something is wrong»
   when the truth is «somebody wrote to you». */
.nav-toggle { position: relative; }
.nav-toggle.has-unread::after {
  content: ""; position: absolute; top: -2px; left: -2px;
  width: .6rem; height: .6rem; border-radius: var(--r-pill);
  background: var(--alert);
  /* Ringed in the header's own background so the dot stays legible against
     the button's border rather than merging with it. */
  box-shadow: 0 0 0 2px var(--bg);
}
.nav-count { position: relative; }
.nav-count > b {
  position: absolute; top: -.45rem; left: -.7rem;
  min-width: 1.15rem; height: 1.15rem; padding: 0 .25rem;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  background: var(--alert); color: var(--alert-ink);
  font-size: .6875rem; font-weight: 700; line-height: 1;
  box-shadow: 0 0 0 2px var(--bg);
}
/* The wallet dot. Same anchor as .nav-count, but it carries no figure: the
   header answers «is there something new», and the amount is on the page.
   Sized and positioned to match the counter badge so the two sit on one line
   when both are lit. */
.nav-dot { position: relative; }
.nav-dot > i {
  position: absolute; top: -.3rem; left: -.55rem;
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--warn);
  box-shadow: 0 0 0 2px var(--bg);
}

/* Inside the open menu the links are full-width rows, so a badge pinned to
   the text's top-left would float over the row above. It sits at the end of
   the row instead — still visually left, because the row is RTL. */
@media (max-width: 48rem) {
  .site-header.nav-open .nav-count,
  .site-header.nav-open .nav-dot {
    display: flex; align-items: center; justify-content: space-between;
  }
  .site-header.nav-open .nav-count > b { position: static; box-shadow: none; }
  .site-header.nav-open .nav-dot > i { position: static; box-shadow: none; }
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: ""; display: block; width: 1.05rem; height: 1.5px;
  background: currentColor; border-radius: 1px;
  transition: transform .25s var(--ease), background .2s var(--ease);
}
.nav-toggle-bars::before { position: absolute; top: -5px; }
.nav-toggle-bars::after  { position: absolute; top: 5px; }
/* Open: the middle bar goes, the outer two cross. */
.site-header.nav-open .nav-toggle-bars { background: transparent; }
.site-header.nav-open .nav-toggle-bars::before { transform: translateY(5px) rotate(45deg); }
.site-header.nav-open .nav-toggle-bars::after  { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 48rem) {
  .nav-toggle { display: grid; margin-inline-start: auto; }
  .site-header .wrap { position: relative; }
  .nav {
    /* Closed by a missing `display`, not by the `hidden` attribute: an
       attribute set at 375px would still be set at 1400px after a rotate,
       leaving a desktop with no navigation at all. */
    display: none;
    position: absolute; inset-inline: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: .1rem;
    margin: 0; padding: .5rem 1.5rem 1rem;
    background: color-mix(in srgb, var(--bg) 94%, transparent);
    backdrop-filter: saturate(180%) blur(22px);
    -webkit-backdrop-filter: saturate(180%) blur(22px);
    border-bottom: 1px solid var(--hairline);
    max-height: calc(100dvh - 3.25rem); overflow-y: auto;
  }
  .site-header.nav-open .nav { display: flex; }
  /* Full-width rows with a real touch target, rather than the 13px pills that
     make sense in a horizontal bar. */
  .nav a { padding: .7rem .6rem; font-size: .9375rem; }
  .nav a.btn { width: 100%; margin-top: .4rem; }
  .nav form { display: flex; }
  .nav form .btn { width: 100%; margin-top: .4rem; }
  .nav .icon-btn { width: 100%; margin: .5rem 0 0; border-radius: var(--r-sm); }
}

.icon-btn {
  display: grid; place-items: center; width: 1.9rem; height: 1.9rem;
  margin-inline-start: .25rem;
  border: 1px solid var(--hairline); border-radius: var(--r-pill);
  background: var(--elevated); color: var(--label-2);
  font-size: .8rem; cursor: pointer;
  transition: color .25s var(--ease), border-color .25s var(--ease);
}
.icon-btn:hover { color: var(--label); border-color: var(--hairline-2); }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  padding: .8rem 1.6rem; border: 1px solid transparent;
  border-radius: var(--r-pill);
  font: inherit; font-size: .9375rem; font-weight: 600; letter-spacing: -.01em;
  cursor: pointer; white-space: nowrap;
  transition: transform .35s var(--ease), box-shadow .35s var(--ease),
              background .3s var(--ease), opacity .3s var(--ease);
}
.btn:hover { opacity: 1; transform: translateY(-1px); }
.btn:active { transform: translateY(0) scale(.985); }
/* The call to action, in its own colour.
   ------------------------------------------------------------------------
   A short hop inside one hue rather than a two-colour sweep: cyan into sky
   reads as a single fresh surface, where the old teal-to-amber crossed the
   wheel and looked muddy in the middle at button size.

   NOT wired to --tint. That variable is the interface accent — links, focus
   rings, meters, the light-theme pair measured to clear 4.5:1 — and pulling
   all of it toward blue to make one button cheerful is how a palette stops
   meaning anything. The button gets its own two variables and nothing else
   moves.

   The label stays near-black in both themes. Measured against both ends of
   the gradient: 13.1:1 on the light end and 8.9:1 on the dark end, so it is
   legible across the whole sweep rather than only where the sample was
   taken — which is the part that goes wrong when a gradient is chosen by
   eye. */
:root {
  /* Two pairs, because a colour that fills a shape and a colour that draws a
     letter are not the same job.

     --cta        a SURFACE. Pale, so the near-black label on top of it is
                  legible; the button reads as a button through its shadow and
                  its shape rather than by shouting.
     --cta-ink    the same family as TEXT, over the page. Pale would be
                  invisible here, so it goes deep on the light theme and light
                  on the dark one — theme-aware, exactly like --tint is. */
  --cta:   #A5DEEF;
  --cta-2: #8FCFE6;
  --cta-ink:   #7DD3FC;   /* 11.9:1 on the dark canvas */
  --cta-ink-2: #67E8F9;   /* 13.7:1 */
}
:root[data-theme="light"] {
  --cta-ink:   #0E7490;   /* 5.1:1 on the cream canvas */
  --cta-ink-2: #0369A1;   /* 5.6:1 */
}
.btn-primary {
  background: linear-gradient(140deg, var(--cta), var(--cta-2));
  color: #04121B;
  box-shadow: 0 .6rem 1.75rem -.7rem color-mix(in srgb, var(--cta-2) 70%, transparent);
}
.btn-primary:hover {
  box-shadow: 0 .9rem 2.4rem -.7rem color-mix(in srgb, var(--cta-2) 85%, transparent);
}
/* `.nav a` sets a muted link colour and outranks `.btn-primary` on
   specificity, so the one call to action in the header was drawing its label
   at 66% opacity — a faded CTA, which is the opposite of the job. Predates
   this colour change; it was just less visible against the old gradient. */
.nav a.btn-primary { color: #04121B; }
.btn-ghost {
  background: var(--elevated); border-color: var(--hairline); color: var(--label);
  backdrop-filter: blur(12px);
}
.btn-ghost:hover { background: var(--elevated-2); border-color: var(--hairline-2); }
.btn-quiet { background: transparent; color: var(--tint); padding-inline: .6rem; }
.btn-danger { background: var(--bad-soft); border-color: var(--bad); color: var(--bad); }
.btn-block { width: 100%; }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; }
.btn-sm { padding: .45rem 1rem; font-size: .8125rem; }

.actions { display: flex; flex-wrap: wrap; gap: .7rem; justify-content: center; }

/* ---------------------------------------------------------------- surfaces */
.card {
  position: relative;
  padding: 1.75rem;
  background: var(--elevated);
  border: 1px solid var(--hairline);
  border-radius: var(--r);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
}
/* A card whose body is paged: the pager sits on the card's bottom edge rather
   than floating up under a short last page.

   Grid children already stretch to the tallest card in their row, so the card
   is the right height on its own — what was missing was somewhere for the
   pager to go. `.paged-body` takes the slack with `flex: 1`, which is also why
   a last page holding one row still puts its pager where the previous page
   did. Without this the control moved up the card as the page emptied, and a
   control that changes position between clicks is read as the page having
   jumped. */
.card-paged { display: flex; flex-direction: column; }
.card-paged .paged-body { flex: 1 1 auto; }
.card-paged .pager { margin-top: 1.25rem; }

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap;
  padding-top: 1rem; border-top: 1px solid var(--hairline);
}

/* An `<a>` with no `href` is already inert — this only makes it look it.
   Kept as an anchor rather than swapped for a `<button>` so the row does not
   change shape at the ends of the list. */
.is-disabled {
  opacity: .4; pointer-events: none; cursor: default;
}

/* The body of a SitePage — «دربارهٔ ما», «قوانین», «تماس با ما».
   ------------------------------------------------------------------------
   Justified, at the operator's request. Two things make that safe here and
   would not elsewhere:

   `text-align-last` is left at its default, so the FINAL line of each
   paragraph still starts at the right edge like ordinary RTL text. Forcing it
   to justify would stretch a three-word closing line across the whole column,
   which is the thing that makes justified text look broken.

   Persian does not hyphenate, so justification widens word gaps rather than
   splitting words. On a narrow column that produces rivers; `.narrow` here is
   wide enough that it does not, and these pages are prose read once, not
   figures scanned. Deliberately scoped to this page's card rather than made a
   global rule — a justified table cell or a justified form hint is worse than
   an unjustified one. */
.page-body p {
  text-align: justify;
  text-justify: inter-word;
}

/* Choosing how to pay.
   ------------------------------------------------------------------------
   This was a class name that had never existed in the stylesheet, so the
   chooser rendered as a bare row of small buttons with the current method as
   an inert span — visible, but not recognisable as a choice. A customer who
   does not realise they may pick reads the page as «this is the only way to
   pay» and leaves if it is not their way.

   Now each method is a card the whole of which is the target, the selected
   one is marked by border, ground AND a tick — three signals, because on a
   payment page the cost of a misread is somebody sending money the wrong
   way. */
.pay-methods { margin-top: 1rem; }
.pay-methods .pay-options {
  display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .6rem;
}
.pay-option {
  flex: 1 1 12rem; min-width: 0;
  display: flex; align-items: center; gap: .6rem;
  padding: .85rem 1rem; text-align: start;
  font: inherit; color: inherit; cursor: pointer;
  background: var(--elevated); border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.pay-option:hover { border-color: var(--tint-line); background: var(--elevated-2); }
.pay-option.is-current {
  border-color: var(--tint); background: var(--tint-soft); cursor: default;
}
.pay-option-mark {
  flex: 0 0 auto; width: 1.15rem; height: 1.15rem; border-radius: 50%;
  display: grid; place-items: center; font-size: .7rem; line-height: 1;
  border: 1px solid var(--hairline-2); color: transparent;
}
.pay-option.is-current .pay-option-mark {
  border-color: var(--tint); background: var(--tint); color: var(--cta-ink);
}
.pay-option-body { min-width: 0; }
.pay-option-title { font-weight: 600; font-size: .9375rem; }
.pay-option-note { font-size: .75rem; color: var(--label-3); margin-top: .1rem; }

/* The page size currently in force. It stays a link, so the row keeps one
   shape and the selected one is not a hole in it — marked by weight and a
   filled ground rather than by removing it. */
.is-current {
  background: var(--tint-soft);
  border-color: var(--tint-line);
  color: var(--tint-2);
  font-weight: 600;
}

.card-lg { border-radius: var(--r-lg); padding: clamp(1.75rem, 5vw, 3rem); }
.card p { color: var(--label-2); font-size: .9375rem; margin-top: .4rem; }

/* Light catching the top edge — the trick that makes a flat rectangle read as
   a physical surface without adding a visible border. */
.card::before {
  content: ""; position: absolute; inset: 0 0 auto; height: 1px;
  border-radius: inherit;
  background: linear-gradient(90deg, transparent, var(--hairline-2) 22%,
                              var(--hairline-2) 78%, transparent);
  pointer-events: none;
}
.card-hover { transition: transform .45s var(--ease), box-shadow .45s var(--ease),
                          border-color .45s var(--ease); }
.card-hover:hover {
  transform: translateY(-4px);
  border-color: var(--tint-line);
  box-shadow: var(--lift);
}

.feature-icon {
  display: grid; place-items: center;
  width: 2.6rem; height: 2.6rem; margin-bottom: 1rem;
  border-radius: .85rem; font-size: 1.15rem;
  background: var(--tint-soft); border: 1px solid var(--tint-line);
}

/* ---------------------------------------------------------------- hero */
.hero {
  position: relative;
  padding-block: clamp(5rem, 14vw, 9rem) clamp(2rem, 6vw, 4rem);
  text-align: center;
}

/* Decorative tiles drifting behind the headline. They sit behind the text and
   ignore pointer events, so they can never steal a tap from the CTA. */
.hero-art {
  position: absolute; inset: -4rem 0 0;
  z-index: 0; pointer-events: none;
  overflow: hidden;
  /* Faded at the edges so the tiles dissolve rather than stop at a hard line. */
  -webkit-mask-image: radial-gradient(70% 60% at 50% 42%, transparent 38%, #000 88%);
          mask-image: radial-gradient(70% 60% at 50% 42%, transparent 38%, #000 88%);
}
.hero > *:not(.hero-art) { position: relative; z-index: 1; }

.art-tile {
  position: absolute;
  inset-inline-start: var(--x); top: var(--y);
  display: grid; place-items: center;
  width: calc(4.25rem * var(--s, 1)); aspect-ratio: 1;
  border-radius: calc(1.35rem * var(--s, 1));
  color: var(--tint);
  background: linear-gradient(150deg, rgba(255,255,255,.07), rgba(255,255,255,.02));
  border: 1px solid var(--hairline);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 1rem 2.5rem -1rem rgba(0,0,0,.55);
  opacity: .55;
  animation: float-tile 20s var(--ease) infinite alternate;
  animation-delay: var(--d, 0s);
}
.art-tile svg { width: 46%; height: 46%; opacity: .85; }
.art-tile:nth-child(even) { color: var(--tint-2); }

:root[data-theme="light"] .art-tile {
  background: linear-gradient(150deg, rgba(255,255,255,.9), rgba(255,255,255,.55));
  box-shadow: 0 1rem 2rem -1rem rgba(16,19,26,.16);
  opacity: .8;
}

@keyframes float-tile {
  from { transform: translate3d(0, 0, 0) rotate(-4deg); }
  to   { transform: translate3d(0, -1.6rem, 0) rotate(4deg); }
}

/* Below the fold they are just noise on a small screen, and the mask leaves
   too little room for them to read as anything. */
@media (max-width: 48rem) {
  .art-tile { opacity: .32; }
  .art-tile:nth-child(n + 6) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .art-tile { animation: none; }
}
.hero h1 { margin-block: 1.25rem 1.5rem; }
.hero .actions { margin-top: 2.25rem; }

.pill {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .35rem .9rem .35rem .5rem;
  border-radius: var(--r-pill); font-size: .8125rem; font-weight: 500;
  color: var(--label-2);
  background: var(--elevated); border: 1px solid var(--hairline);
  backdrop-filter: blur(12px);
}
/* ⚠️ This chip was unreadable on the light theme and had been since the theme
   existed. It paints a near-black label on `--tint`→`--tint-2`, and on the
   light theme those two are DARK — #0D7A6F and #A85F24 — so the measured
   contrast was 3.64:1 and 3.91:1 against a label that needs 4.5:1 at this
   size. Dark on dark. Moving it onto the pale surface takes it to 12.9:1. */
.pill b {
  padding: .1rem .55rem; border-radius: var(--r-pill);
  font-size: .75rem; font-weight: 600; color: #04121B;
  background: linear-gradient(140deg, var(--cta), var(--cta-2));
}

/* The device: a settings panel rendered in CSS. Showing the one field the
   user has to change explains the product faster than a paragraph does. */
.device {
  max-width: 24rem; margin: clamp(3rem, 8vw, 5rem) auto 0;
  padding: 1.5rem 1.35rem;
  background: linear-gradient(165deg, var(--elevated-2), var(--elevated));
  border: 1px solid var(--hairline); border-radius: var(--r-lg);
  box-shadow: var(--lift-2);
  backdrop-filter: blur(24px);
  text-align: start;
}
.device-bar { display: flex; gap: .35rem; margin-bottom: 1.1rem; }
.device-bar i { width: .5rem; height: .5rem; border-radius: 50%; background: var(--hairline-2); }
.device-row {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding-block: .7rem;
}
.device-row + .device-row { border-top: 1px solid var(--hairline); }
.device-row span { font-size: .875rem; color: var(--label-2); }
.device-row b { font-size: .9375rem; font-weight: 600; }
.device-row .set {
  font-family: var(--mono); font-size: .9375rem; color: var(--tint);
  letter-spacing: .01em;
}
/* One value rolling up to the next, inside a window one line tall.
   ----------------------------------------------------------------------------
   Four scenes on one 12s clock. Every row advances on the same clock rather
   than three independent timers: rows rolling at their own pace read as broken,
   not as animated.

   Each item is absolutely placed and takes its turn — no wrapper element, and
   only opacity and transform move, so the whole card stays on one layer. */
.cycle {
  position: relative; display: block;
  height: 1.5rem; line-height: 1.5rem; min-width: 8.5rem;
  overflow: hidden; text-align: end;
}
.cycle i {
  position: absolute; inset: 0; font-style: normal;
  opacity: 0; transform: translateY(-100%);
  animation: device-cycle 12s cubic-bezier(.7, 0, .2, 1) infinite;
}
/* The status word runs on its own two-state clock, not the four-slot one.
   Scene 1 is disconnected; scenes 2, 3 and 4 are connected and «متصل» sits
   perfectly still across all three while the rows above it keep changing.

   Two things have to be undone from the generic .cycle rules, and missing
   either leaves the word invisible rather than merely mistimed:
     • the transform. .cycle i parks every item at translateY(-100%) and lets
       its own keyframes bring it down. These keyframes only touch opacity, so
       without resetting it the word stays parked 24px above a window that is
       24px tall and clips it.
     • the delay. .cycle i:nth-child(2) carries animation-delay: 3s, and a
       two-class selector only *ties* with it — the tie goes to whichever comes
       last in the file, and that is the nth-child block below. So «قطع», being
       second, started three seconds late and was never drawn at all in the one
       scene it exists for. .cycle.cycle-word is three classes and wins
       outright; do not simplify it back down.  */
.cycle-word { min-width: 3.25rem; font-weight: 600; }
.cycle.cycle-word i { animation: none; transform: none; }
.cycle.cycle-word i.is-on {
  color: var(--on);
  animation: state-on 12s linear infinite;
  animation-delay: 0s;
}
.cycle.cycle-word i.is-off {
  color: var(--off);
  animation: state-off 12s linear infinite;
  animation-delay: 0s;
}

/* The mode row turns green the moment the card connects and stays green for
   the three connected scenes, on the same clock as the switch. Set on the
   container so all four items inherit it — colouring each one separately would
   mean four more animations doing one job. */
.cycle-mode { animation: mode-colour 12s linear infinite; }
@keyframes mode-colour {
  0%,  22%   { color: var(--label); }
  25%, 100%  { color: var(--on); }
}

@keyframes state-on  { 0%, 22% { opacity: 0; } 25%, 100% { opacity: 1; } }
@keyframes state-off { 0%, 22% { opacity: 1; } 25%, 100% { opacity: 0; } }
.device-state { display: flex; align-items: center; gap: .6rem; }

/* The line under the card. It is what turns four rotating addresses from
   decoration into the reason to buy. */
.device-note {
  max-width: 24rem; margin: .9rem auto 0;
  font-size: .875rem; line-height: 1.9; color: var(--label-2); text-align: center;
}

/* Stopped while the card is off screen. The rotation is decorative and there
   is no reason to spend a phone's battery animating something nobody is
   looking at; the class is added by the observer in base.html. */
.device.is-paused .cycle i,
.device.is-paused .switch.is-cycling,
.device.is-paused .switch.is-cycling::after { animation-play-state: paused; }
.cycle i:nth-child(1) { animation-delay: 0s; }
.cycle i:nth-child(2) { animation-delay: 3s; }
.cycle i:nth-child(3) { animation-delay: 6s; }
.cycle i:nth-child(4) { animation-delay: 9s; }

@keyframes device-cycle {
  0%              { opacity: 0; transform: translateY(-100%); }
  3%,  22%        { opacity: 1; transform: translateY(0); }
  25%, 100%       { opacity: 0; transform: translateY(100%); }
}

/* ⚠️ The global reduced-motion rule collapses every animation to its final
   frame, and the final frame here is opacity 0 — so without this the three
   rows would simply be blank for anyone who asks for less motion. They get the
   first scene, held still, which is what the card said before it moved. */
@media (prefers-reduced-motion: reduce) {
  .cycle i { animation: none !important; opacity: 0; transform: none; }
  .cycle i:first-child { opacity: 1; }
  .cycle-word .is-off { opacity: 0; }
  .cycle-word .is-on { opacity: 1; }
  /* Held on the connected state, so the mode row has to be green to match the
     switch and the word beside it — a still card that says «متصل» in green
     next to a grey «دستی» looks like the colour failed to load. */
  .cycle-mode { animation: none !important; color: var(--on); }
  .switch.is-cycling,
  .switch.is-cycling::after { animation: none !important; }
  .switch.is-cycling { background: var(--on); }
  .switch.is-cycling::after { inset-inline-end: calc(100% - 1.25rem); }
}

.switch {
  width: 2.35rem; height: 1.4rem; border-radius: var(--r-pill);
  background: linear-gradient(140deg, var(--tint), var(--tint-2));
  position: relative; flex: none;
}
.switch::after {
  content: ""; position: absolute; inset-block: .15rem; inset-inline-end: .15rem;
  width: 1.1rem; border-radius: 50%; background: #fff;
  box-shadow: 0 .1rem .3rem rgba(0,0,0,.3);
}
/* The switch joins the same 12s clock: off and red for the first scene, then
   on and green for the rest, so the card ends every cycle in the state a
   working customer is actually in. */
.switch.is-cycling { animation: device-switch 12s ease-in-out infinite; }
.switch.is-cycling::after { animation: device-knob 12s cubic-bezier(.7,0,.2,1) infinite; }

@keyframes device-switch {
  0%,  22%   { background: var(--off); }
  25%, 100%  { background: var(--on); }
}
@keyframes device-knob {
  /* Knob left = disconnected, knob right = connected, as asked. Written with
     inset-inline-end because the card is RTL: in this direction "end" is the
     LEFT edge, so .15rem parks the knob on the left and the long value pushes
     it to the right. Using `right:` here would read correctly and behave
     backwards the moment anything flips direction. */
  0%,  22%   { inset-inline-end: .15rem; }
  25%, 100%  { inset-inline-end: calc(100% - 1.25rem); }
}

/* ---------------------------------------------------------------- bits */
.badge {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .25rem .8rem; border-radius: var(--r-pill);
  font-size: .8125rem; font-weight: 500;
  background: var(--elevated); border: 1px solid var(--hairline); color: var(--label-2);
}
.badge-ok   { background: var(--mint-soft); border-color: var(--mint-line); color: var(--tint-2); }
.badge-warn { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.badge-bad  { background: var(--bad-soft);  border-color: var(--bad);  color: var(--bad); }
.badge-info { background: var(--tint-soft); border-color: var(--tint-line); color: var(--tint); }

.mono { font-family: var(--mono); letter-spacing: .01em; }
.muted { color: var(--label-2); }
.faint { color: var(--label-3); }
.center { text-align: center; }

.alert {
  padding: .9rem 1.15rem; border-radius: var(--r-sm);
  border: 1px solid var(--hairline); background: var(--elevated);
  font-size: .9375rem; margin-bottom: 1rem;
}
.alert-success { background: var(--mint-soft); border-color: var(--mint-line); color: var(--tint-2); }
.alert-error   { background: var(--bad-soft);  border-color: var(--bad);  color: var(--bad); }
.alert-warning { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.alert-info    { background: var(--tint-soft); border-color: var(--tint-line); color: var(--tint); }

.meter { height: .375rem; border-radius: var(--r-pill); background: var(--elevated-2); overflow: hidden; }
.meter > span {
  display: block; height: 100%; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--tint), var(--tint-2));
  transition: width .8s var(--ease);
}
.meter.is-high > span { background: linear-gradient(90deg, var(--warn), var(--bad)); }

.divider { height: 1px; background: var(--hairline); margin-block: 1.75rem; }

/* ---------------------------------------------------------------- forms */
.field { margin-bottom: 1.15rem; }
.field > label {
  display: block; font-size: .8125rem; font-weight: 500;
  color: var(--label-2); margin-bottom: .45rem;
}
/* Every control, wherever it sits.
   ----------------------------------------------------------------------------
   This block used to be scoped to `.field`, so a <select> in a toolbar — the
   ticket status and priority pickers, for two — fell back to the browser's own
   control font and sat in a Persian page rendering «پیام کاربر» in Segoe UI at
   a different size from everything around it. A form control does not stop
   being part of the design because it is not in a form row. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
select, textarea {
  font: inherit; font-size: .9375rem; color: var(--label);
  background: var(--bg-soft); border: 1px solid var(--hairline);
  border-radius: var(--r-sm); padding: .5rem .8rem;
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease);
}
select { cursor: pointer; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--tint);
  box-shadow: 0 0 0 4px var(--tint-soft);
}
/* The full-width variant. Only the things that differ. */
.field input, .field select, .field textarea {
  width: 100%; padding: .8rem 1rem; font-size: 1rem;
}
.field input::placeholder { color: var(--label-3); }
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--tint);
  box-shadow: 0 0 0 4px var(--tint-soft);
}
.field .help  { font-size: .8125rem; color: var(--label-3); margin-top: .4rem; line-height: 1.55; }
.field .error { font-size: .8125rem; color: var(--bad); margin-top: .4rem; }
input[type="file"] { padding: .6rem; }

/* ---------------------------------------------------------------- plans */
.plan { display: flex; flex-direction: column; }
.plan.is-featured {
  border-color: var(--tint-line);
  background: linear-gradient(165deg, var(--tint-soft), var(--mint-soft));
  box-shadow: var(--lift);
}
.plan .ribbon {
  position: absolute; top: -.7rem; inset-inline-start: 50%;
  transform: translateX(50%);
  padding: .15rem .8rem; border-radius: var(--r-pill);
  font-size: .75rem; font-weight: 600; color: #04121B;
  background: linear-gradient(140deg, var(--tint), var(--tint-2));
  white-space: nowrap;
}
.plan .price {
  font-size: 2.1rem; font-weight: 700; letter-spacing: -.04em;
  margin-block: .5rem 1rem; line-height: 1.1;
}
.plan .price small { font-size: .875rem; font-weight: 400; color: var(--label-2); letter-spacing: 0; }
.plan ul { list-style: none; padding: 0; margin-block: 0 1.5rem; font-size: .9375rem; }
.plan li {
  display: flex; align-items: center; gap: .55rem;
  padding-block: .35rem; color: var(--label-2);
}
.plan li::before { content: "✓"; color: var(--tint-2); font-weight: 700; flex: none; }
.plan form { margin-top: auto; }
.plan .btn { margin-top: auto; }

/* ---------------------------------------------------------------- payment */
.pay-grid {
  display: grid; gap: 1.5rem;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 46rem) {
  .pay-grid { grid-template-columns: 13rem minmax(0, 1fr); align-items: start; }
}
.qr {
  padding: .85rem; border-radius: var(--r);
  background: #fff; color: #000;      /* scanners want maximum contrast */
  box-shadow: var(--lift);
}
.qr svg { width: 100%; height: auto; }

.copy-row {
  display: flex; align-items: center; gap: .5rem;
  padding: .7rem .9rem; border-radius: var(--r-sm);
  background: var(--bg-soft); border: 1px solid var(--hairline);
}
.copy-row code {
  flex: 1; min-width: 0; font-family: var(--mono); font-size: .875rem;
  word-break: break-all; direction: ltr; text-align: start;
}

.steps { counter-reset: step; list-style: none; padding: 0; }
.steps li {
  counter-increment: step; position: relative;
  padding-inline-start: 2.1rem; padding-block: .45rem;
  font-size: .9375rem; color: var(--label-2);
}
.steps li::before {
  content: counter(step); position: absolute; inset-inline-start: 0; top: .5rem;
  display: grid; place-items: center; width: 1.4rem; height: 1.4rem;
  border-radius: 50%; font-size: .75rem; font-weight: 600;
  color: var(--tint); background: var(--tint-soft); border: 1px solid var(--tint-line);
}

.timeline { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.timeline .step {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .8rem; border-radius: var(--r-pill);
  font-size: .8125rem; color: var(--label-3);
  background: var(--elevated); border: 1px solid var(--hairline);
}
.timeline .step.is-done { color: var(--tint-2); border-color: var(--mint-line); background: var(--mint-soft); }
.timeline .step.is-current { color: var(--tint); border-color: var(--tint-line); background: var(--tint-soft); }
.timeline .arrow { color: var(--label-3); font-size: .75rem; }

/* ---------------------------------------------------------------- stats */
/* The heading that names a band of the page — «فروش», «کاربران», «در انتظار
   اقدام», «خروجی‌ها». One rule for every panel, staff and reseller and
   customer alike, because the operator reads all three and a kicker that is
   one size here and another there reads as two different kinds of thing.

   ⚠️ 1.1rem, not the .8125rem it was. At the old size these were being missed
   on a wide screen: uppercase, letterspaced and grey is a lot of styling for
   something you have to lean in to read. Raised at the operator's measurement
   — a third again — and the two sizes above it moved with it so the ladder
   still descends: page title 1.75, group 1.225, this 1.1.

   Anything that overrides this inline is overriding it on purpose and should
   say why. */
.section-title {
  font-size: 1.1rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--label-3);
  margin-block: 2.25rem .9rem;
}
.stat-num {
  font-size: 1.75rem; font-weight: 700; letter-spacing: -.03em;
  line-height: 1.15; margin-block: .15rem;
}
.stat-num small { font-size: .8125rem; font-weight: 400; color: var(--label-2); letter-spacing: 0; }
.stat-label { font-size: .8125rem; color: var(--label-2); }
a.card { color: inherit; }
a.card:hover { text-decoration: none; opacity: 1; }

/* ---------------------------------------------------------------- thread */
.thread { display: flex; flex-direction: column; gap: 1rem; margin-block: 1.5rem; }
.msg {
  max-width: 90%; padding: 1rem 1.15rem; border-radius: var(--r);
  border: 1px solid var(--hairline); background: var(--elevated);
}
.msg.from-user  { align-self: flex-start; border-start-start-radius: .35rem; }
.msg.from-staff {
  align-self: flex-end; border-start-end-radius: .35rem;
  background: var(--tint-soft); border-color: var(--tint-line);
}
.msg-head {
  display: flex; gap: .6rem; align-items: baseline; justify-content: space-between;
  font-size: .8125rem; color: var(--label-3); margin-bottom: .45rem;
}
.msg-head b { color: var(--label-2); font-weight: 600; }
.msg-body { font-size: .9375rem; line-height: 1.8; white-space: pre-wrap; }

/* ---------------------------------------------------------------- usage chart */
.chart {
  display: flex; align-items: flex-end; gap: .35rem;
  height: 7rem; margin-top: 1.25rem;
}
.chart .bar {
  position: relative; flex: 1;
  display: flex; align-items: flex-end;
  height: 100%; border-radius: .3rem;
  background: var(--elevated-2);
}
/* The bar's colour carries its size as well as its height does.
   ----------------------------------------------------------------------------
   Teal-into-amber was the site's accent gradient pressed into service, and it
   gave a 2% day the same saturated warmth as the peak — the eye read a row of
   equally loud stubs. The scale now runs from a quiet wash to the bright cyan
   the headline «بدون لگ» is set in, so height and colour say the same thing
   twice and the peak is findable without reading a single number.

   Four steps and not a continuous ramp because CSS cannot read an element's
   own height. The level is decided in `build_series`, where the number is. */
.chart .bar > i {
  display: block; width: 100%; min-height: 2px; border-radius: .3rem;
  background: linear-gradient(180deg, var(--cta-ink), var(--cta-ink-2));
  transition: height .7s var(--ease), opacity .7s var(--ease);
}
.chart .bar.lv-1 > i { opacity: .42; }
.chart .bar.lv-2 > i { opacity: .60; }
.chart .bar.lv-3 > i { opacity: .80; }
.chart .bar.lv-4 > i { opacity: 1; }
/* A single bar — the «امروز» range — should not become a block the width of
   the card. */
.chart .bar { max-width: 5rem; }
/* Empty days are drawn as an explicit flat mark rather than nothing, so a
   quiet day cannot be mistaken for missing data. */
.chart .bar.lv-0 > i { background: var(--hairline); opacity: 1; }
.chart .bar::after {
  content: attr(data-label);
  position: absolute; inset-block-end: calc(100% + .4rem); inset-inline: 0;
  text-align: center; font-size: .7rem; color: var(--label);
  background: var(--bg-soft); border: 1px solid var(--hairline);
  border-radius: .4rem; padding: .15rem .3rem;
  opacity: 0; pointer-events: none; transition: opacity .25s var(--ease);
  white-space: nowrap;
}
.chart .bar:hover::after { opacity: 1; }
/* The tooltip is dropped where it cannot fit or cannot be used.
   ----------------------------------------------------------------------------
   `opacity: 0` hides the box but keeps it in the layout, so every 14px bar was
   trailing an invisible ~110px `nowrap` label centred on it — and the outermost
   bars hung theirs off the side of the page. Measured on the dashboard: 55px of
   sideways drag at 375px and 28px at 753px, from an element that is invisible
   until hovered.

   Both conditions are needed. `hover: none` alone leaves a tablet with a mouse
   dragging, and a width rule alone leaves a wide touch screen carrying a
   tooltip that can never open. The dates are on the axis underneath either
   way. */
@media (hover: none), (max-width: 48rem) {
  .chart .bar::after { display: none; }
}
.chart-axis {
  display: flex; justify-content: space-between;
  margin-top: .5rem; font-size: .75rem; color: var(--label-3);
}

/* ---------------------------------------------------------------- tables */
.table-wrap {
  overflow-x: auto; border: 1px solid var(--hairline);
  border-radius: var(--r); background: var(--elevated);
}
table { width: 100%; border-collapse: collapse; font-size: .9375rem; }
th, td { padding: .8rem 1.1rem; text-align: start; white-space: nowrap; }
thead th { font-size: .8125rem; font-weight: 500; color: var(--label-3); }
tbody tr { border-top: 1px solid var(--hairline); }

/* Tunnel rows inside a machine card.
   The page-wide `td { padding: .8rem 1.1rem; white-space: nowrap }` is right
   for a full-width table and wrong inside a 19rem card: three columns of it is
   6.6rem of padding alone, and with nowrap the connection counts rendered
   outside the card border. Scoped here rather than loosening the global rule,
   which the wide tables still want. */
/* One row per tunnel: endpoint at the start, state at the end, never wrapped.
   This was a fixed-layout table whose first column had `word-break:break-word`,
   which split «به 212.80.9.35» after «به» and left the address on its own line,
   one cell away from its port. */
.tunnels { font-size: .8125rem; width: 100%; }
.tunnels .tunnel-row {
  display: flex; align-items: baseline;
  justify-content: space-between; gap: .5rem;
  padding: .3rem .35rem;
}
/* Only rows whose endpoint is a bare address are flipped. The page is RTL, so
   the first child sits on the right by default — which is where a Persian
   country name belongs, alongside every other label on the card. A Latin
   address reads better on the left with the state opposite it. Reversing the
   axis rather than reordering the markup keeps the endpoint first in the DOM,
   which is the order it should be read and announced in. */
.tunnels .tunnel-row-addr { flex-direction: row-reverse; }
/* The endpoint is one fact and is never broken up; if the card is too narrow
   it scrolls rather than folding the address away from its port. */
.tunnels .tunnel-row > * { white-space: nowrap; }
.tunnels .tunnel-row > .mono { overflow-x: auto; }
.tunnels .tunnel-endpoint { display: inline-flex; gap: .35rem; align-items: baseline; }

/* ---------------------------------------------------------------- faq */
details.faq {
  border-bottom: 1px solid var(--hairline);
  padding-block: 1.15rem;
}
details.faq summary {
  cursor: pointer; list-style: none;
  font-size: 1rem; font-weight: 500; letter-spacing: -.015em;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
details.faq summary::-webkit-details-marker { display: none; }
details.faq summary::after {
  content: "+"; color: var(--tint); font-size: 1.35rem; font-weight: 300;
  line-height: 1; flex: none; transition: transform .35s var(--ease);
}
details.faq[open] summary::after { transform: rotate(45deg); }
details.faq p { color: var(--label-2); font-size: .9375rem; margin-top: .85rem; max-width: 46rem; }

/* ---------------------------------------------------------------- footer */
.site-footer {
  border-top: 1px solid var(--hairline);
  margin-top: 4rem;
  /* Extra room at the bottom for the floating Telegram button, so the page
     ends clear of it rather than the button dodging content as you scroll —
     a control that moves is harder to hit than one that never does. Written
     as two properties, not `padding-block`, because the bottom value is not
     the top one. */
  padding-top: 2.25rem;
  /* 5.5rem = the button's 3rem plus its 1.1rem inset plus 1.4rem of air. At
     4.75rem the arithmetic still cleared, by ten pixels — true but too close
     to call safe once the footer links wrap on a narrow phone. */
  padding-bottom: max(5.5rem, 2.25rem + env(safe-area-inset-bottom));
  font-size: .8125rem; color: var(--label-3);
}

/* Four columns on one row: brand, contact, links, seals.
   ⚠️ `repeat(auto-fit, minmax(13rem, 1fr))` was wrong here and looked right in
   code. At this container width it resolved to THREE tracks, so the fourth
   column — the seals — wrapped underneath the brand instead of sitting beside
   the links. Named tracks, because the number of columns is fixed at four and
   pretending otherwise is what caused it.

   The brand gets the widest track: it carries the wordmark, the legal name and
   a paragraph, while the other three are a list each. */
.footer-grid {
  display: grid;
  grid-template-columns: 1.7fr 1.2fr 1fr 1fr;
  gap: 2rem 2.5rem;
  align-items: start;
  text-align: start;
}
@media (max-width: 62rem) {
  /* Two by two before anything is allowed to squeeze. */
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
.footer-col h3 {
  font-size: .9375rem; font-weight: 700; color: var(--label);
  margin: 0 0 .9rem;
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li { margin-bottom: .55rem; line-height: 1.9; }
.footer-col a { color: var(--label-3); text-decoration: none; }
.footer-col a:hover { color: var(--label); }
.footer-contact b { color: var(--label-2); font-weight: 600; }

.footer-brand { grid-column: auto; }
.footer-logo { font-size: 1.5rem; font-weight: 800; color: var(--label); letter-spacing: -.02em; }
/* The dot carries the accent, the way the wordmark does in the header. */
.footer-logo span { color: var(--tint); }
.footer-legal { color: var(--label-2); margin: .35rem 0 0; font-size: .875rem; }
.footer-blurb { margin: .8rem 0 0; line-height: 1.9; max-width: 34ch; }
.footer-social { margin-top: 1rem; display: flex; gap: .5rem; }
.footer-social a {
  width: 2rem; height: 2rem; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--elevated); border: 1px solid var(--hairline);
  color: var(--label-2); text-decoration: none;
  transition: color .2s var(--ease), border-color .2s var(--ease);
}
.footer-social a:hover { color: var(--tint); border-color: var(--tint-line); }

/* The badge images arrive at whatever size the issuer serves them, so they are
   constrained here rather than in their own markup — which must not be
   touched. A white plate under each one because both eNamad and Samandehi
   ship art drawn for a light background, and on this dark footer they would
   otherwise be a smudge. */
.footer-seals .seal {
  background: #fff; border-radius: .5rem; padding: .5rem;
  width: 6.5rem; margin-bottom: .75rem;
  display: grid; place-items: center;
}
.footer-seals .seal img,
.footer-seals .seal a > img { max-width: 100%; height: auto; display: block; }

/* A seal the issuer has not activated yet answers 403, and the plate would
   otherwise collapse to a white sliver around a broken image. `is-pending`
   holds the space with a quiet frame instead, and JS takes the class off the
   moment the image loads. Nothing here claims anything — no text, no logo —
   it is a frame waiting for its picture. */
.footer-seals .seal.is-pending {
  background: transparent;
  border: 1px dashed var(--hairline-2);
  min-height: 5.5rem;
}
.footer-seals .seal.is-pending img { display: none; }
.footer-seals .seal.is-pending::after {
  content: "";
  width: 1.6rem; height: 1.6rem; border-radius: .3rem;
  border: 1px dashed var(--hairline-2);
  opacity: .7;
}

.footer-base {
  margin-top: 2.5rem; padding-top: 1.25rem;
  border-top: 1px solid var(--hairline);
  display: flex; flex-wrap: wrap; gap: .5rem 1.25rem;
  align-items: center; justify-content: space-between;
}

@media (max-width: 40rem) {
  .footer-grid { grid-template-columns: 1fr; gap: 1.75rem; }
  .footer-blurb { max-width: none; }
}

/* ---------------------------------------------------------------- auth */
.auth-shell { display: grid; place-items: center; padding-block: clamp(3rem, 9vw, 5rem); }
.auth-card { width: 100%; max-width: 25rem; }
.auth-card h1 { font-size: 1.75rem; letter-spacing: -.03em; margin-bottom: .4rem; }
.auth-card > p { color: var(--label-2); font-size: .9375rem; margin-bottom: 1.75rem; }

/* ---------------------------------------------------------------- reveal */
.reveal {
  opacity: 0; transform: translateY(1.5rem);
  transition: opacity .9s var(--ease), transform .9s var(--ease);
}
.reveal.is-visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }

/* ------------------------------------------------- ticket state badges
   One class per state, named for the state and not for a colour, so a
   template never has to decide what «در حال بررسی» looks like.

   Grouped by whose move it is, which is the only thing somebody scanning a
   queue is asking: orange means it is ours, green means we have answered,
   amber means we are waiting on them, teal means we are working on it. That
   is also why «پیام کاربر» and «پاسخ کاربر» share a colour — they differ in
   what happened, not in what we have to do next. */
.tstate {
  font-size: .75rem; font-weight: 600; line-height: 1;
  padding: .3rem .6rem; border-radius: var(--r-pill);
  border: 1px solid currentColor; white-space: nowrap;
}
.tstate.s-open,
.tstate.s-user_reply    { color: var(--alert); }
.tstate.s-answered      { color: var(--on); }
.tstate.s-investigating { color: var(--tint); }
.tstate.s-waiting       { color: var(--warn); }
.tstate.s-closed        { color: var(--label-3); }

/* The same word after the subject, as «موضوع — وضعیت». An em dash and not a
   hyphen: a hyphen between a Persian phrase and another Persian phrase reads
   as part of a compound word. */
.tstate-inline {
  font-weight: 500; font-size: .875rem;
}
/* Non-breaking spaces, not ordinary ones: leading and trailing whitespace in
   `content` collapses against the adjacent text, so `" — "` rendered as an em
   dash with nothing around it — and in the queue, where the span sits directly
   after the subject with no whitespace in the markup, as
   «سرعت کمهپیام کاربر». */
/* The dash gets real margin instead of spaces inside `content`: leading and
   trailing whitespace there collapses against the adjacent text, so `" — "`
   rendered with nothing around it — and in the queue, where the span sits
   directly after the subject with no whitespace in the markup, the two ran
   together as «سرعت کمهپیام کاربر». A margin cannot collapse away. */
.tstate-inline::before { content: "—"; margin-inline: .45rem; color: var(--label-3); }
.tstate-inline.s-open,
.tstate-inline.s-user_reply    { color: var(--alert); }
.tstate-inline.s-answered      { color: var(--on); }
.tstate-inline.s-investigating { color: var(--tint); }
.tstate-inline.s-waiting       { color: var(--warn); }
.tstate-inline.s-closed        { color: var(--label-3); }

/* ------------------------------------------- the operator's user table
   Every value centred under its own heading. See the comment in
   staff/status.html for why the columns are fixed rather than automatic. */
.users-table { table-layout: fixed; width: 100%; }
.users-table th,
.users-table td { text-align: center; vertical-align: middle; padding: .7rem .5rem; }
/* The name column is a name over a plan, two lines and left-anchored; centring
   it makes the whole column look ragged. */
.users-table th:first-child,
.users-table td:first-child { text-align: start; }
/* An address is LTR text in an RTL cell. `dir` fixes the order; `text-align`
   has to be re-stated or the dir would drag it back to the cell's edge and
   undo the centring. */
.users-table td .mono { text-align: center; }

/* ------------------------------------------------------------ tidy tables
   Same shape as `.users-table` above and for the same reasons: fixed columns
   so the widths come from the `<colgroup>` rather than from whichever string
   happens to be longest, and centred cells so every heading sits over the
   middle of its own column instead of against the edge of the value below it.

   The page-wide `th, td { text-align: start }` is what put «طرح» hard against
   «محقق»; a heading is only readable as a heading when it is over its column,
   not beside the next one.

   ⚠️ One class, not one per table. It was written as `.customers-table` and
   the second table that needed it — the customer's own address list — would
   have been a copy of the same six rules, which is how two tables that are
   meant to look alike stop looking alike. The widths stay per table, in each
   `<colgroup>`, because that is the one part that really does differ. */
.tidy-table { table-layout: fixed; width: 100%; }
.tidy-table th,
.tidy-table td {
  text-align: center; vertical-align: middle; padding: .75rem .5rem;
}
.tidy-table td form { display: flex; justify-content: center; }
/* On a phone these stop being tables and become one card per row.
   ----------------------------------------------------------------------------
   Measured before deciding: at 375px the seven columns get 16 to 65 pixels
   each while their contents need 40 to 99. The username wanted 79px in a 35px
   column and the date 88px in the same. Tightening the padding moved the
   overflow from 15px to 8px and left every column still too narrow to read —
   it was treating the symptom. A seven-column table does not fit a phone, and
   no amount of squeezing makes it fit.

   So each row becomes a small card and each cell a labelled line. The heading
   row is hidden because every cell now carries its own label in `data-label`,
   which is also why that attribute is not optional: without it the values
   would stack with nothing to say which is which. */
@media (max-width: 40rem) {
  .tidy-table,
  .tidy-table tbody,
  .tidy-table tr,
  .tidy-table td { display: block; width: auto; }
  .tidy-table thead,
  .tidy-table colgroup { display: none; }
  .tidy-table tr {
    border: 1px solid var(--hairline); border-radius: var(--r-sm);
    padding: .5rem .75rem; margin-bottom: .6rem;
  }
  .tidy-table tr:last-child { margin-bottom: 0; }
  .tidy-table td {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem; padding: .4rem 0; text-align: end;
    border-block-start: 1px solid var(--hairline-2);
  }
  .tidy-table tr td:first-child { border-block-start: 0; }
  .tidy-table td::before {
    content: attr(data-label);
    color: var(--label-3); font-size: .8125rem; text-align: start;
    flex: 0 0 auto;
  }
  /* A cell whose label is empty — the action column — has nothing to sit
     opposite, so the control takes the whole line. */
  .tidy-table td[data-label=""]::before { content: none; }
  .tidy-table td[data-label=""] { justify-content: stretch; }
  .renew-cell { width: 100%; }
}
/* The plan name is a phrase, not a token — «محقق — یک ماهه» is wider than its
   column and the page-wide `white-space: nowrap` would push it out of the
   cell rather than let it wrap. Only this column: everywhere else nowrap is
   what keeps a figure from breaking across two lines. */
.customers-table td:nth-child(2) { white-space: normal; }
/* Seven columns of real content in 62rem. At the body size the username column
   is 13% ≈ 7rem while «BacheReseller2» needs more, and the page-wide `nowrap`
   holds it on one line — so a fixed column had nowhere to put the overflow and
   the name was simply cut. Smaller type, and a break allowed on the one column
   that carries a free-form string. */
.customers-table { font-size: .8125rem; }
.customers-table th { font-size: .8125rem; letter-spacing: 0; }
.customers-table td:first-child { white-space: normal; overflow-wrap: anywhere; }
.customers-table .renew-cell select {
  font-size: .75rem; padding-inline: .4rem; text-overflow: ellipsis;
}
.customers-table .renew-cell .btn { font-size: .75rem; padding-inline: .6rem; }
/* An LTR run inside a centred RTL cell: `dir` fixes the character order and
   `text-align` has to be re-stated, or the direction drags the text back to
   the cell's edge and undoes the centring. Same note as `.users-table`. */
.tidy-table td .mono,
.tidy-table td [dir="ltr"] { text-align: center; }

/* Select and button on one line. `.actions` wraps on purpose — it is built for
   a row of buttons under a card — and inside a table cell that wrapping is
   what dropped «تمدید» onto its own line beneath the plan box. */
.renew-cell {
  display: flex; flex-wrap: nowrap; align-items: center;
  gap: .4rem; justify-content: center;
}
.renew-cell select { min-width: 0; flex: 1 1 auto; }
.renew-cell .btn { flex: 0 0 auto; }

/* ------------------------------------------------- reference tables in the guide
   These hold settings paths and shell commands, not figures, and the page-wide
   `th, td { white-space: nowrap }` is wrong for both: a string that cannot
   break makes `.table-wrap` grow a horizontal scrollbar under every one of
   them. A scrollbar under a seven-row list of browser settings reads as a
   broken box, and the half of the line that scrolled out of view is the half
   that matters — «→ Off».

   So: wrap instead of scroll, break inside a long token when there is nowhere
   else to break, and a fixed first column so the browser and OS names line up
   down the left instead of each row finding its own width. */
/* ------------------------------------------------- dashboard queue tables
   A queue inside a card on the management dashboard: three or four short
   columns in a third of the page. It keeps `.tidy-table`'s phone behaviour —
   one small card per row, each cell labelled — and drops its equal widths,
   because these columns are genuinely unequal: «#12» needs three characters
   and a ticket subject needs twenty-four.

   ⚠️ It also replaces what used to be an `overflow-x` scrollbar inside the
   card. An email address in a third-width column did not fit, and the answer
   was a scrollbar the operator had to spot, drag, and drag back. Wrapping is
   the answer; the row is three lines tall and all of it is readable. */
.queue-table { table-layout: auto; font-size: .8125rem; }
.queue-table td { text-align: start; padding: .4rem .35rem; }
.queue-table tr + tr td { border-block-start: 1px solid var(--hairline-2); }

.guide-table { table-layout: fixed; width: 100%; }
.guide-table td {
  white-space: normal; overflow-wrap: anywhere;
  padding: .6rem .8rem; vertical-align: middle;
}
/* Between the body text and a caption: big enough to read a command from
   without leaning in, small enough that a settings path fits on one line at
   the width this column actually gets. */
.guide-table td.mono { font-size: .82rem; line-height: 1.75; }
/* The label column — «Chrome», «Windows 11». Narrow, and never broken across
   two lines, because it is what the eye scans down. */
.guide-table.has-labels td:first-child {
  width: 7.5rem; white-space: nowrap; font-size: .875rem;
}
/* A block of shell lines keeps its own newlines but may still wrap a long one
   rather than push the card sideways. */
.guide-table td.block { white-space: pre-wrap; }

/* A guide table whose every string is Latin — field names, a hostname, a port,
   a secret. In the page's own direction the label column lands on the RIGHT,
   which is the opposite side from the dialog the reader is copying into, so
   this one table is turned around. Scoped to a class rather than to the page:
   the tables above it are Persian labels with Latin values and belong RTL. */
.guide-table.is-ltr { direction: ltr; }
.guide-table.is-ltr td:first-child { text-align: left; }
/* Centred, and this is the reason the box looked untidy: three values of very
   different lengths — a hostname, three digits, and a forty-character secret —
   each started at the edge and each ended somewhere else. Centred, they read
   as one column. */
.guide-table.is-ltr td:last-child { text-align: center; }

/* A paragraph in a callout is prose and reads better justified — the ragged
   edge in these narrow boxes was leaving one word alone on the last line. */
.alert p, .alert.is-prose { text-align: justify; text-justify: inter-word; }

/* ------------------------------------------------------- chart tick row
   One slot per bar, aligned to the bars above by using the same flex gap.
   Most slots are empty — which bar prints its date is decided in Python,
   where the number of bars is known. An empty slot still takes its width, so
   the labels that do print sit exactly under their own bar. */
.chart-ticks {
  display: flex; gap: .35rem; margin-top: .4rem;
  font-size: .65rem; color: var(--label-3); letter-spacing: 0;
}
.chart-ticks > span {
  flex: 1; min-width: 0; text-align: center; white-space: nowrap;
}

/* --------------------------------------------------- page-wide range bar */
.range-bar {
  display: inline-flex; flex-wrap: wrap; gap: .25rem;
  padding: .25rem; border-radius: var(--r-pill);
  border: 1px solid var(--hairline); background: var(--elevated);
}
.range-chip {
  padding: .35rem .95rem; border-radius: var(--r-pill);
  font-size: .8125rem; font-weight: 600; color: var(--label-2);
  text-decoration: none; white-space: nowrap;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.range-chip:hover { background: var(--elevated-2); color: var(--label); }
.range-chip.is-active { background: var(--tint); color: var(--bg); }

/* A section heading with its control on the other end of the line. */
.section-head-row {
  display: flex; flex-wrap: wrap; gap: .75rem 1rem;
  align-items: center; justify-content: space-between;
  margin: 2rem 0 1rem;
}

/* A dense tick row on a narrow screen prints one label in five, counted back
   from today so the newest bar always keeps its date. `visibility` and not
   `display`, so the hidden slots still hold their width and the labels that
   do print stay under their own bars. */
@media (max-width: 48rem) {
  .chart-ticks[data-dense="1"] > span:not(:nth-last-child(5n+1)) { visibility: hidden; }
}
