/* Ravaan business portal — component layer.
 *
 * Everything positional lives in Tailwind utilities in the templates. What is
 * left here is the set of elements DJANGO renders, which therefore arrive with
 * no classes for a utility to attach to: form controls, mainly. Tailwind's
 * preflight strips their native styling, so without this file every input on
 * the portal is an unstyled box.
 *
 * Palette is docs/design/ravaan-dashboards-v1.html. Keep it in step with the
 * `colors` block in portal/base.html — the two are the same system written
 * twice, once for utilities and once for element selectors.
 */

:root {
  --navy: #1B2A41;
  --navy-soft: #2C3E58;

  /* docs/portal-design-spec-v1.md §1 names these two by number. --navy-900 is
     the same colour as --navy above and is kept as its own name so the auth
     backdrop reads the way the spec is written; --navy-800 is genuinely new
     (a touch darker than --navy-soft) and exists to be the dot colour. */
  --navy-900: #1B2A41;
  --navy-800: #22334D;
  --brand: #2E8FE6;
  --brand-dark: #1F74C2;
  --brand-wash: #EAF2FC;
  --canvas: #F5F7FA;
  --surface: #FFFFFF;
  --line: #E3E8EE;
  --line-strong: #C6D3E0;
  --muted: #66768A;
  --ok: #1E6B3A;        --ok-wash: #E7F5EC;
  --warn: #8A5A00;      --warn-wash: #FEF3D6;
  --bad: #9A3B12;       --bad-wash: #FBEAE5;

  /* Layered shadows. One blurred box reads as a grey halo; three offsets at
     decreasing opacity read as an object above a surface, which is the whole
     point of giving a card depth rather than a border. */
  --shadow-1: 0 1px 2px rgba(27,42,65,.06), 0 2px 6px rgba(27,42,65,.04);
  --shadow-2: 0 1px 3px rgba(27,42,65,.07), 0 4px 12px rgba(27,42,65,.06),
              0 12px 32px rgba(27,42,65,.05);
  --shadow-3: 0 2px 4px rgba(27,42,65,.08), 0 8px 20px rgba(27,42,65,.08),
              0 20px 48px rgba(27,42,65,.07);

  /* One easing for everything that moves, so the portal has a single feel.
     Decelerating: fast to start, settles gently. */
  --ease: cubic-bezier(.16, .84, .44, 1);

  /* Gradients. The sidebar and the primary button carry the only two in the
     portal. A gradient on every surface reads as decoration; on exactly two
     it reads as hierarchy. Both stay inside the approved navy/brand pair —
     these are lighter and darker stops of colours the gallery already uses,
     not new hues. */
  --grad-brand: linear-gradient(180deg, #3D9BEE 0%, #2E8FE6 52%, #2680D6 100%);
  --grad-brand-hi: linear-gradient(180deg, #2E8FE6 0%, #2179C9 52%, #1F74C2 100%);
  --grad-navy: linear-gradient(168deg, #24374F 0%, #1B2A41 44%, #15223A 100%);

  /* A hairline of white on the top edge. This one line is most of the
     difference between a panel printed on the page and a panel lit from
     above; it pairs with the shadows below it, which imply the same light. */
  --edge-top: inset 0 1px 0 rgba(255, 255, 255, .72);

  /* Brand-tinted glow, for the things that should look energised rather than
     merely raised: the primary button and live status badges. */
  --glow-brand: 0 2px 10px rgba(46, 143, 230, .30);

  /* Inter is fetched from Google Fonts by base.html, and every fallback below
     is a system face — a blocked or slow font request costs the typeface, not
     the layout. */
  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica,
               Arial, 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono',
               monospace;

  /* Tailwind's two breakpoints, kept as a comment because custom properties
     cannot be used in media queries: sm = 640px, lg = 1024px. */
}

/* --- reset ------------------------------------------------------------ */
/* This replaces Tailwind Preflight, which used to arrive with the Play CDN.
 * It is not a style choice — several rules below are load-bearing for the
 * markup that already exists:
 *
 *   * the universal `border-width: 0; border-style: solid` is what makes a
 *     bare `border` utility mean "1px solid", which is how every card,
 *     divider and input border in the templates is written;
 *   * headings and lists are used as structure throughout (a photo grid is a
 *     <ul>, a spec table is a <dl>), so their default margins and bullets
 *     would put spacing and dots where the design has none;
 *   * form controls do not inherit the page font on their own, and Django
 *     renders every one of them.
 */

*, *::before, *::after {
  box-sizing: border-box;
  border-width: 0;
  border-style: solid;
  border-color: var(--line);
}

html { -webkit-text-size-adjust: 100%; tab-size: 4; }

body {
  margin: 0;
  min-height: 100%;
  line-height: 1.5;
  font-family: var(--font-sans);
}

h1, h2, h3, h4, h5, h6 { margin: 0; font-size: inherit; font-weight: inherit; }
p, figure, blockquote, dl, dd, pre { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
hr { margin: 0; border-top-width: 1px; }

a { color: inherit; text-decoration: inherit; }
strong, b { font-weight: bolder; }
small { font-size: 80%; }

code, kbd, samp, pre { font-family: var(--font-mono); font-size: 1em; }

/* Media is block-level so an image in a card has no mysterious baseline gap
   under it, and never overflows the column it sits in. */
img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
img, video { max-width: 100%; height: auto; }

button, input, optgroup, select, textarea {
  font-family: inherit;
  font-size: 100%;
  font-weight: inherit;
  line-height: inherit;
  color: inherit;
  margin: 0;
  padding: 0;
}
button, select { text-transform: none; }
button, [type="button"], [type="submit"] { -webkit-appearance: button; background-color: transparent; background-image: none; }
:-moz-focusring { outline: auto; }
textarea { resize: vertical; }
::placeholder { opacity: 1; color: var(--muted); }
[hidden] { display: none !important; }

/* --- form controls --------------------------------------------------- */

.pf input[type="text"],
.pf input[type="email"],
.pf input[type="tel"],
.pf input[type="url"],
.pf input[type="number"],
.pf input[type="password"],
.pf input[type="date"],
.pf input[type="datetime-local"],
.pf select,
.pf textarea {
  width: 100%;
  display: block;
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  color: var(--navy);
  /* 16px, not smaller: iOS Safari zooms the viewport on focus below this,
     which on a phone shunts the form sideways mid-entry. */
  font-size: 16px;
  line-height: 1.4;
  font-family: inherit;
  /* A whisper of inset shadow at rest. Without it a white input on a white
     card is only a border; with it the field reads as a recess, which is
     what makes the focus ring feel like it lifts. */
  box-shadow: inset 0 1px 2px rgba(27, 42, 65, .045);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
              background-color .18s var(--ease);
}

.pf textarea { min-height: 92px; resize: vertical; }

.pf select {
  /* Native arrow is removed by preflight on some engines; draw one so the
     control still reads as a dropdown. */
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2366768A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 38px;
}

.pf input:hover, .pf select:hover, .pf textarea:hover { border-color: var(--line-strong); }

.pf input:focus, .pf select:focus, .pf textarea:focus {
  outline: none;
  border-color: var(--brand);
  /* The inset recess is dropped on focus and replaced by a ring, so the
     field visibly comes forward instead of only changing colour. */
  box-shadow: 0 0 0 3px rgba(46, 143, 230, .16), 0 1px 2px rgba(46, 143, 230, .20);
}

.pf input[disabled], .pf select[disabled], .pf textarea[disabled] {
  background: var(--canvas);
  color: var(--muted);
  cursor: not-allowed;
}

/* Error state is set by the field wrapper, so it reaches the control Django
   rendered without needing a class on the control itself. */
.field--error input,
.field--error select,
.field--error textarea { border-color: var(--bad); }
.field--error input:focus,
.field--error select:focus,
.field--error textarea:focus { box-shadow: 0 0 0 3px rgba(154, 59, 18, .15); }

/* Checkboxes and radios keep their native box — restyling them wholesale
   costs more than it returns — but take the brand colour and a matching
   focus ring so they belong to the same set. */
.pf input[type="checkbox"], .pf input[type="radio"] {
  width: 17px; height: 17px;
  accent-color: var(--brand);
  cursor: pointer;
  flex: none;
}
.pf input[type="checkbox"]:focus-visible,
.pf input[type="radio"]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

.pf input[type="file"] {
  width: 100%;
  font-size: 14px;
  color: var(--muted);
}
.pf input[type="file"]::file-selector-button {
  margin-right: 12px;
  padding: 8px 14px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  color: var(--navy);
  font: 500 14px/1 inherit;
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}
.pf input[type="file"]::file-selector-button:hover {
  background: var(--brand-wash);
  border-color: var(--brand);
  color: var(--brand-dark);
}


/* --- buttons ---------------------------------------------------------- */
/* Component classes rather than repeated utility strings: these appear on
   every screen, and a primary button that drifts between pages is the exact
   inconsistency the restyle exists to remove. */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px 18px;
  border-radius: 10px;
  border: 1px solid transparent;
  font: 600 15px/1.2 inherit;
  letter-spacing: -.006em;
  text-decoration: none;
  cursor: pointer;
  transition: background-color .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), box-shadow .18s var(--ease),
              transform .18s var(--ease);
}
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn--block { width: 100%; }

/* 1.5% and one pixel. Enough to answer the cursor, small enough that a
   full-width button does not visibly change width. */
.btn:hover:not([disabled]) { transform: translateY(-1px) scale(1.005); }
.btn:active:not([disabled]) { transform: translateY(0) scale(.997); transition-duration: .07s; }

/* The gradient runs light-to-dark over 100%, which is what makes a filled
   button read as a physical key rather than a coloured rectangle. The top
   inset hairline is the same lit-from-above rule the cards follow. */
.btn--primary {
  background: var(--grad-brand);
  color: #fff;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.22),
              0 1px 2px rgba(27,42,65,.14), var(--glow-brand);
}
.btn--primary:hover {
  background: var(--grad-brand-hi);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.18),
              0 2px 4px rgba(27,42,65,.14), 0 8px 22px rgba(46,143,230,.42);
}
.btn--primary:active { box-shadow: inset 0 1px 3px rgba(16,52,88,.30); }

.btn--secondary { background: var(--surface); color: var(--navy); border-color: var(--line); }
.btn--secondary:hover {
  background: var(--brand-wash); border-color: var(--brand); color: var(--brand-dark);
  box-shadow: var(--shadow-1);
}

.btn--quiet { background: transparent; color: var(--muted); }
.btn--quiet:hover { background: var(--canvas); color: var(--navy); }

.btn--danger { background: transparent; color: var(--bad); border-color: transparent; }
.btn--danger:hover { background: var(--bad-wash); }

.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--line);
  border-color: transparent;
  color: var(--muted);
  cursor: not-allowed;
}
.btn--secondary[disabled]:hover, .btn[disabled]:hover { background: var(--line); color: var(--muted); }

/* A button that should read as a link, used inside dense photo/document rows. */
.linkbtn {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: 500 13px/1 inherit; color: var(--brand); text-decoration: none;
  transition: color .15s ease;
}
.linkbtn:hover { color: var(--brand-dark); text-decoration: underline; }
.linkbtn--bad { color: var(--bad); }
.linkbtn--bad:hover { color: var(--bad); }
.linkbtn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }

/* --- surfaces --------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid rgba(27, 42, 65, .06);
  border-radius: 16px;
  box-shadow: var(--edge-top), var(--shadow-2);
  padding: 22px;
}

/* Opt-in: only for cards that are themselves a link or a target, never for a
   form container, where a panel rising under the cursor is just noise. */
.card--interactive {
  transition: box-shadow .25s var(--ease), transform .25s var(--ease);
}
.card--interactive:hover {
  box-shadow: var(--shadow-3);
  transform: translateY(-2px);
}
@media (min-width: 640px) { .card { padding: 26px; } }

/* An inline note. `--bad` for the ones that report a broken environment. */
.note {
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  background: var(--brand-wash);
  border-radius: 10px;
  padding: 14px 16px;
  font-size: 14px;
}
.note--bad  { border-left-color: var(--bad);  background: var(--bad-wash); }
.note--warn { border-left-color: var(--warn); background: var(--warn-wash); }

/* --- typography ------------------------------------------------------- */
/* Headings tighten as they grow: at 15px, -0.02em is invisible; at 30px it is
   the difference between "set" and "default". Body text goes the other way —
   looser leading, because these screens carry explanatory prose about
   approvals and documents, not just labels. */

.h1 { font-size: 30px; font-weight: 700; letter-spacing: -.022em; line-height: 1.15; }
.h2 { font-size: 21px; font-weight: 650; letter-spacing: -.015em; line-height: 1.25; }
.h3 { font-size: 16px; font-weight: 600; letter-spacing: -.008em; line-height: 1.35; }
@media (max-width: 640px) {
  .h1 { font-size: 25px; }
  .h2 { font-size: 19px; }
}
.prose { line-height: 1.65; }

/* --- the auth backdrop ------------------------------------------------ */
/* docs/portal-design-spec-v1.md §2: a dark navy ground with a dot lattice.
   The signed-out screens are deliberately NOT the product's canvas — the
   spec's reasoning is that the darkness is the signal that says "you are
   outside the main product, at the gate", and the white card is the one lit
   object in it.

   The dots are navy-800 on navy-900: a single step of lightness apart, so the
   texture is felt rather than read. The half-cell background-position offset
   stops a dot landing exactly in the viewport corner, which is the detail
   that makes a lattice look placed rather than tiled. */

.auth-bg {
  position: relative;
  background-color: var(--navy-900);
  background-image: radial-gradient(var(--navy-800) 1px, transparent 1px);
  background-size: 24px 24px;
  background-position: -12px -12px;
}

/* A vignette over the middle, so the lattice fades out behind the card
   instead of running under it at full strength and fighting the form. */
.auth-bg::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 45%, rgba(27, 42, 65, .9) 0%, transparent 55%);
}
/* Content sits above the vignette; without this the card is painted under it. */
.auth-bg > * { position: relative; }

/* --- auth text on the dark ground ------------------------------------- */
/* Only two things sit OUTSIDE the white card: the wordmark above it and the
   foot beneath it. Both inherited colours chosen for a light canvas, so on
   navy the wordmark rendered navy-on-navy and the foot links were barely
   legible. These overrides are scoped to `.auth-outside` rather than applied
   to `.text-muted` globally, because the same utility is used INSIDE the
   white card — where it must stay dark. */

.auth-bg .wordmark { color: #fff; }

.auth-outside .text-muted { color: rgba(255, 255, 255, .64); }
.auth-outside .text-brand { color: #79BDF5; }
.auth-outside .text-brand:hover { color: #A6D4F9; }
.auth-outside .auth-foot { color: rgba(255, 255, 255, .5); }

/* --- entrance --------------------------------------------------------- */
/* One orchestrated arrival on load, staggered so the eye lands on the card
   rather than on three things at once. Nothing animates on scroll. */

@keyframes rv-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
.rv-enter { animation: rv-rise .3s var(--ease) both; }
.rv-enter-1 { animation-delay: .04s; }
.rv-enter-2 { animation-delay: .10s; }
.rv-enter-3 { animation-delay: .17s; }

/* --- submit feedback -------------------------------------------------- */
/* A sliver at the top of the viewport while a form is in flight, so sending
   an OTP or uploading a photo does not look like a frozen page. The bar is
   CSS; a listener adds `is-busy` on submit, because no CSS selector can see
   a form submission. */

.rv-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  overflow: hidden;
  z-index: 60;
  opacity: 0;
  transition: opacity .2s var(--ease);
  pointer-events: none;
}
.rv-progress::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 42%;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(90deg, rgba(46,143,230,0), var(--brand), rgba(46,143,230,0));
  transform: translateX(-100%);
}
body.is-busy .rv-progress { opacity: 1; }
body.is-busy .rv-progress::before { animation: rv-sweep 1.05s var(--ease) infinite; }

@keyframes rv-sweep {
  from { transform: translateX(-100%); }
  to   { transform: translateX(340%); }
}

/* The button that was pressed says so too, and stops taking clicks. */
.btn.is-busy { pointer-events: none; opacity: .82; }
.btn.is-busy::after {
  content: "";
  width: 15px; height: 15px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: rv-spin .6s linear infinite;
}
@keyframes rv-spin { to { transform: rotate(360deg); } }

/* --- the wordmark ----------------------------------------------------- */
/* The full stop after "Ravaan" was a text period taking the brand colour. As
   a drawn dot it can carry the brand gradient and a glow, which gives the
   signed-out screens the one piece of brand furniture they have. Sized in em
   so it tracks whatever size the wordmark is set at. */

.wordmark {
  display: inline-flex;
  align-items: baseline;
  font-weight: 700;
  letter-spacing: -.028em;
  color: var(--navy);
  text-decoration: none;
  transition: opacity .18s var(--ease);
}
.wordmark:hover { opacity: .84; }
.wordmark__dot {
  width: .30em;
  height: .30em;
  margin-left: .09em;
  border-radius: 50%;
  background: var(--grad-brand);
  box-shadow: 0 0 0 3px rgba(46, 143, 230, .13), 0 2px 8px rgba(46, 143, 230, .55);
}

/* --- the auth card ---------------------------------------------------- */
/* Deeper than a content card on purpose: it is the only object on the page,
   so it carries the whole sense of depth rather than sharing it. */

.auth-card {
  border-radius: 18px;
  border-color: rgba(27, 42, 65, .07);
  box-shadow: var(--edge-top), var(--shadow-3);
}

/* A field label with a touch more weight than the body it sits above. The
   weight contrast is the point: label 600 against value 400 lets the eye
   skim a form by its labels without the labels being a different colour. */
.lbl {
  display: block;
  margin-bottom: 7px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -.004em;
  color: var(--navy);
}

/* The reassurance line under an auth card. Muted, small, never a heading. */
.auth-foot {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  color: var(--muted);
}
.auth-foot svg { width: 13px; height: 13px; flex: none; opacity: .75; }

/* --- status badges ---------------------------------------------------- */
/* Pills as a component rather than a utility string, so a badge is identical
   on every screen. The glow is a tinted ring plus a soft drop in the same
   hue — enough that a row of statuses reads at a glance, small enough that
   twelve of them down a table do not vibrate. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
  border-radius: 999px;
  padding: 4px 10px;
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .002em;
  border: 1px solid transparent;
  transition: box-shadow .2s var(--ease);
}
/* The 5px dot is what makes a pill legible to someone who cannot separate
   the green from the amber. Colour is never the only carrier of state. */
.badge::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.badge--ok {
  background: var(--ok-wash); color: var(--ok);
  box-shadow: 0 0 0 1px rgba(30,107,58,.12), 0 1px 6px rgba(30,107,58,.16);
}
.badge--warn {
  background: var(--warn-wash); color: var(--warn);
  box-shadow: 0 0 0 1px rgba(138,90,0,.13), 0 1px 6px rgba(138,90,0,.16);
}
.badge--bad {
  background: var(--bad-wash); color: var(--bad);
  box-shadow: 0 0 0 1px rgba(154,59,18,.13), 0 1px 6px rgba(154,59,18,.16);
}
.badge--info {
  background: var(--brand-wash); color: var(--brand-dark);
  box-shadow: 0 0 0 1px rgba(46,143,230,.14), 0 1px 6px rgba(46,143,230,.18);
}
/* Neutral means "nothing has happened yet", so it deliberately does not
   glow — a state with no glow among states that do is itself a signal. */
.badge--neutral {
  background: var(--canvas); color: var(--muted);
  border-color: var(--line);
}

/* --- filter chips ----------------------------------------------------- */
/* A row of links that narrow a list (Payouts, screen #19). Links rather than
   buttons, so filtering works without JavaScript and each view has a URL.
   Shaped like a badge so the two read as one family, but larger and quiet:
   a badge reports a state, a chip is something to press. The current one is
   marked by aria-current, the same attribute a screen reader announces, so
   the style cannot drift from what is actually selected. */
.chip {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 500;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--line);
  text-decoration: none;
  transition: background-color .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);
}
.chip:hover { color: var(--navy); border-color: var(--line-strong); }
.chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.chip[aria-current="page"] {
  background: var(--navy); color: #fff; border-color: var(--navy);
}
/* The row they sit in: one line, always. Five chips fit a 390px phone at
   the tighter padding below; anything narrower scrolls sideways instead of
   wrapping, because a wrapped filter row reads as two groups of filters. */
.chips {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  overflow-x: auto;
  scrollbar-width: none;
  /* Room for the focus ring, which overflow would otherwise clip. */
  padding: 3px;
  margin: -3px;
}
.chips::-webkit-scrollbar { display: none; }
.chips .chip { flex: none; }
@media (max-width: 640px) {
  .chips { gap: 6px; }
  .chips .chip { padding: 6px 11px; font-size: 12.5px; }
}

/* --- the sidebar ------------------------------------------------------ */
/* Flat #1B2A41 replaced by a gradient down the same navy, plus a lit right
   edge. The edge matters more than the gradient does: it is what stops a
   240px navy column from reading as a hole cut in the page. */

.sidebar {
  background: var(--grad-navy);
  box-shadow: inset -1px 0 0 rgba(255, 255, 255, .07), 4px 0 24px rgba(21, 34, 58, .16);
}

/* Nav items get their active state from a rail on the left edge rather than
   from the fill alone, so the current page survives being looked at quickly. */
.navlink {
  position: relative;
  transition: background-color .18s var(--ease), color .18s var(--ease);
}
.navlink::before {
  content: "";
  position: absolute;
  left: -12px;
  top: 50%;
  width: 3px;
  height: 0;
  border-radius: 0 3px 3px 0;
  background: var(--brand);
  box-shadow: 0 0 10px rgba(46, 143, 230, .8);
  transform: translateY(-50%);
  transition: height .22s var(--ease);
}
.navlink.is-active::before { height: 20px; }

/* --- page transition -------------------------------------------------- */
/* The signed-in equivalent of the auth entrance: main content arrives once,
   on load. Shorter and shallower than the auth stagger, because this fires
   on every navigation rather than once per session. */

@keyframes rv-page-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.rv-page { animation: rv-page-in .3s var(--ease) both; }

/* Figures line up column-wise only with tabular figures; proportional ones
   make a column of rupee amounts ragged. */
.tnum { font-variant-numeric: tabular-nums; }

/* --- password reveal -------------------------------------------------- */
/* The eye control sits INSIDE the input's box. `.pw` is added by the
   enhancement in base.html, not written into any template, so every password
   field on the portal gets one without a template remembering to ask — which
   is what went wrong before, when only the sign-in screen had a toggle and
   the two fields on the signup form had none.

   The padding lives on `.pw input` rather than on the base control, so a
   password field with scripting off is a normal field with no dead gutter. */

.pw { position: relative; display: block; }
.pw input { padding-right: 46px; }

.pw__toggle {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  border-radius: 0 10px 10px 0;
  transition: color .15s var(--ease), background-color .15s var(--ease);
}
.pw__toggle:hover { color: var(--brand-dark); background: var(--brand-wash); }
.pw__toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.pw__toggle svg { width: 18px; height: 18px; display: block; }

/* Exactly one of the two icons is shown, flipped by the button's own
   aria-pressed state — so the icon cannot drift out of step with the
   accessible name the way two independent toggles would. */
.pw__toggle .pw__on { display: none; }
.pw__toggle[aria-pressed="true"] .pw__on  { display: block; }
.pw__toggle[aria-pressed="true"] .pw__off { display: none; }

/* --- the OTP code entry ----------------------------------------------- */
/* docs/portal-design-spec-v1.md §6. Six boxes rather than one field, because
   a 6-digit code is read off a screen in pairs and typed in one run — the
   boxes tell you how many digits are expected before you start, and show you
   where you are without counting.
   
   The markup is built by the enhancement in verify.html, never by the
   template, so with scripting off the page keeps Django's ordinary single
   field and still works. Nothing here styles that fallback: it is already
   covered by the form-control rules at the top of this file.

   Sized in the spec's own numbers (44x52, 6px radius) but drawing on the
   palette already defined above rather than re-declaring near-identical
   greys, so there is one set of colours in this file and not two. */

.otp-row {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin-bottom: 4px;
}

.otp-box {
  /* flex-basis rather than a hard width so six boxes plus their gaps still
     fit inside the card on a 320px phone, where 6x44 + 5x8 would not. */
  flex: 0 1 44px;
  width: 44px;
  min-width: 0;
  height: 52px;
  padding: 0;
  text-align: center;
  /* 20px, which is also comfortably above the 16px below which iOS Safari
     zooms the viewport on focus. */
  font-size: 1.25rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--navy);
  border: 1.5px solid var(--line-strong);
  border-radius: 6px;
  background: var(--canvas);
  transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
              background-color .18s var(--ease);
}

.otp-box:focus {
  outline: none;
  border-color: var(--brand);
  background: var(--surface);
  box-shadow: 0 0 0 4px rgba(46, 143, 230, .18);
}

/* A box that holds a digit keeps a darker edge, so the filled run reads as
   progress at a glance rather than only by the digits themselves. */
.otp-box.is-filled {
  border-color: var(--navy-800);
  background: var(--surface);
}

.field--error .otp-box { border-color: var(--bad); }

/* The live resend countdown. The button is a real submit that posts to
   portal:resend_code — the timer only disables it, so with scripting off the
   server-rendered state still governs. */
.resend-timer { font-variant-numeric: tabular-nums; }

/* --- utilities -------------------------------------------------------- */
/* The layout layer. Every class below was previously generated in the
 * browser by Tailwind's Play CDN — 400KB of JavaScript that downloaded,
 * scanned the DOM, compiled CSS and injected it on every page load. Until
 * that finished, the portal had no centring, no spacing, no page background
 * and no card width, because ALL of that lived in the generated sheet and
 * none of it lived here. That is why the restyle could be fully deployed and
 * still look like it had never been applied.
 *
 * These are the 189 utilities the portal templates actually use, written out
 * once. Tailwind's own scale and naming are kept exactly, so the markup did
 * not have to change and anyone reading a template can still look a class up
 * in Tailwind's docs.
 *
 * Placed AFTER the component layer deliberately: a utility and a component
 * class have identical specificity, so source order is what decides, and
 * `card p-7` has to mean 28px rather than the card's own 22px.
 */

/* display */
.block { display: block; }
.flex { display: flex; }
.grid { display: grid; }
.hidden { display: none; }

/* position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
.inset-0 { inset: 0; }
.inset-y-0 { top: 0; bottom: 0; }
.left-0 { left: 0; }
.top-0 { top: 0; }
.top-3 { top: .75rem; }
.right-3 { right: .75rem; }
.z-20 { z-index: 20; }
.z-30 { z-index: 30; }
.z-40 { z-index: 40; }

/* flex + grid */
.flex-1 { flex: 1 1 0%; }
.flex-col { flex-direction: column; }
.flex-col-reverse { flex-direction: column-reverse; }
.flex-wrap { flex-wrap: wrap; }
.shrink-0 { flex-shrink: 0; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-end { justify-content: flex-end; }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.gap-1\.5 { gap: .375rem; }
.gap-2 { gap: .5rem; }
.gap-1 { gap: .25rem; }
.gap-2\.5 { gap: .625rem; }
.gap-3 { gap: .75rem; }
.gap-4 { gap: 1rem; }
.gap-5 { gap: 1.25rem; }
.gap-x-2 { column-gap: .5rem; }
.gap-x-4 { column-gap: 1rem; }
.gap-x-6 { column-gap: 1.5rem; }
.gap-y-3 { row-gap: .75rem; }

/* sizing */
.h-4 { height: 1rem; }
.h-6 { height: 1.5rem; }
.h-9 { height: 2.25rem; }
.h-14 { height: 3.5rem; }
.h-24 { height: 6rem; }
.h-28 { height: 7rem; }
.h-32 { height: 8rem; }
.h-\[18px\] { height: 18px; }
.h-full { height: 100%; }
.w-4 { width: 1rem; }
.w-6 { width: 1.5rem; }
.w-9 { width: 2.25rem; }
.w-\[18px\] { width: 18px; }
.w-\[240px\] { width: 240px; }
.w-auto { width: auto; }
.w-full { width: 100%; }
.min-w-0 { min-width: 0; }
.min-h-screen { min-height: 100vh; }
.max-w-sm { max-width: 24rem; }
.max-w-2xl { max-width: 42rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-\[420px\] { max-width: 420px; }

/* margin */
.mx-auto { margin-left: auto; margin-right: auto; }
.mt-auto { margin-top: auto; }
.-ml-2 { margin-left: -.5rem; }
.mt-0\.5 { margin-top: .125rem; }
.mt-1 { margin-top: .25rem; }
.mt-1\.5 { margin-top: .375rem; }
.mt-2 { margin-top: .5rem; }
.mt-2\.5 { margin-top: .625rem; }
.mt-3 { margin-top: .75rem; }
.mt-4 { margin-top: 1rem; }
.mt-5 { margin-top: 1.25rem; }
.mt-6 { margin-top: 1.5rem; }
.mt-7 { margin-top: 1.75rem; }
.mt-10 { margin-top: 2.5rem; }
.mb-1\.5 { margin-bottom: .375rem; }
.mb-2 { margin-bottom: .5rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }

/* padding */
.p-2 { padding: .5rem; }
.p-4 { padding: 1rem; }
.p-7 { padding: 1.75rem; }
.pb-2 { padding-bottom: .5rem; }
.pl-4 { padding-left: 1rem; }
.pt-4 { padding-top: 1rem; }
.pt-3 { padding-top: .75rem; }
.pt-5 { padding-top: 1.25rem; }
.pt-6 { padding-top: 1.5rem; }
.px-2\.5 { padding-left: .625rem; padding-right: .625rem; }
.px-2 { padding-left: .5rem; padding-right: .5rem; }
.px-3 { padding-left: .75rem; padding-right: .75rem; }
.px-3\.5 { padding-left: .875rem; padding-right: .875rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.py-2 { padding-top: .5rem; padding-bottom: .5rem; }
.py-0\.5 { padding-top: .125rem; padding-bottom: .125rem; }
.py-2\.5 { padding-top: .625rem; padding-bottom: .625rem; }
.py-3 { padding-top: .75rem; padding-bottom: .75rem; }
.py-7 { padding-top: 1.75rem; padding-bottom: 1.75rem; }
.py-10 { padding-top: 2.5rem; padding-bottom: 2.5rem; }
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }

/* typography */
.font-sans { font-family: var(--font-sans); }
.font-mono { font-family: var(--font-mono); }
.font-normal { font-weight: 400; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.uppercase { text-transform: uppercase; }
.tracking-wide { letter-spacing: .025em; }
.leading-snug { line-height: 1.375; }
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-sm { font-size: .875rem; line-height: 1.25rem; }
.text-\[12px\] { font-size: 12px; }
.text-\[11px\] { font-size: 11px; }
.text-\[13px\] { font-size: 13px; }
.text-\[15px\] { font-size: 15px; }
.text-\[17px\] { font-size: 17px; }
.text-\[19px\] { font-size: 19px; }
.text-\[20px\] { font-size: 20px; }
.text-\[28px\] { font-size: 28px; }
.no-underline { text-decoration-line: none; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.antialiased { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

/* colour — text */
.text-navy { color: var(--navy); }
.text-navy-line { color: var(--line-strong); }
.text-brand { color: var(--brand); }
.text-brand\/40 { color: rgba(46, 143, 230, .4); }
.text-muted { color: var(--muted); }
.text-white { color: #fff; }
.\!text-white { color: #fff !important; }
.text-ok { color: var(--ok); }
.text-warn { color: var(--warn); }
.text-bad { color: var(--bad); }

/* colour — background */
.bg-transparent { background-color: transparent; }
.bg-white { background-color: #fff; }
.bg-brand { background-color: var(--brand); }
.bg-white\/10 { background-color: rgba(255, 255, 255, .10); }
.bg-canvas { background-color: var(--canvas); }
.bg-navy\/50 { background-color: rgba(27, 42, 65, .5); }
.bg-brand-wash { background-color: var(--brand-wash); }
.bg-ok-wash { background-color: var(--ok-wash); }
.bg-bad-wash { background-color: var(--bad-wash); }

/* borders */
.border { border-width: 1px; }
.border-0 { border-width: 0; }
.border-t { border-top-width: 1px; }
.border-b { border-bottom-width: 1px; }
.border-line { border-color: var(--line); }
.border-white\/10 { border-color: rgba(255, 255, 255, .10); }
.border-brand\/15 { border-color: rgba(46, 143, 230, .15); }
.border-ok\/15 { border-color: rgba(30, 107, 58, .15); }
.border-bad\/15 { border-color: rgba(154, 59, 18, .15); }
.rounded-lg { border-radius: .5rem; }
.rounded-xl2 { border-radius: 14px; }
.rounded-full { border-radius: 9999px; }
.rounded-\[10px\] { border-radius: 10px; }
.shadow-card { box-shadow: 0 1px 2px rgba(27,42,65,.06), 0 8px 28px rgba(27,42,65,.08); }

/* misc */
.overflow-hidden { overflow: hidden; }
.overflow-y-auto { overflow-y: auto; }
.object-cover { object-fit: cover; }
.cursor-pointer { cursor: pointer; }
.transition-colors {
  transition-property: color, background-color, border-color, fill, stroke;
  transition-duration: .15s;
  transition-timing-function: var(--ease);
}
.transition-transform {
  transition-property: transform;
  transition-duration: .15s;
  transition-timing-function: var(--ease);
}
.duration-200 { transition-duration: .2s; }
.ease-out { transition-timing-function: cubic-bezier(0, 0, .2, 1); }
.-translate-x-full { transform: translateX(-100%); }

/* Visible to a screen reader, to nobody else. Used for the mobile menu
   button's label, which is an icon on screen. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* The stretched link: the whole listing card is the hit area, while only one
   link is in the tab order. */
.before\:absolute::before { position: absolute; }
.before\:inset-0::before { inset: 0; }
.before\:content-\[\'\'\]::before { content: ''; }

/* state variants */
.hover\:bg-canvas:hover { background-color: var(--canvas); }
.hover\:bg-brand-wash:hover { background-color: var(--brand-wash); }
.hover\:bg-white\/5:hover { background-color: rgba(255, 255, 255, .05); }
.hover\:border-brand:hover { border-color: var(--brand); }
.hover\:text-white:hover { color: #fff; }
.hover\:text-brand-dark:hover { color: var(--brand-dark); }
.hover\:underline:hover { text-decoration-line: underline; }
.focus-visible\:outline:focus-visible { outline-style: solid; }
.focus-visible\:outline-2:focus-visible { outline-width: 2px; }
.focus-visible\:outline-brand:focus-visible { outline-color: var(--brand); }
.focus-visible\:outline-offset-2:focus-visible { outline-offset: 2px; }

/* responsive — sm (640px) */
@media (min-width: 640px) {
  .sm\:flex-row { flex-direction: row; }
  .sm\:justify-end { justify-content: flex-end; }
  .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .sm\:p-8 { padding: 2rem; }
  .sm\:px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
  .sm\:py-12 { padding-top: 3rem; padding-bottom: 3rem; }
}

/* responsive — lg (1024px). The sidebar's translate lives here and must come
   after `.-translate-x-full`, so the drawer is open at desktop width whatever
   the mobile toggle last left on the element. */
@media (min-width: 1024px) {
  .lg\:hidden { display: none; }
  .lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .lg\:pl-\[240px\] { padding-left: 240px; }
  .lg\:px-8 { padding-left: 2rem; padding-right: 2rem; }
  .lg\:py-9 { padding-top: 2.25rem; padding-bottom: 2.25rem; }
  .lg\:translate-x-0 { transform: translateX(0); }
}

/* --- motion preferences ----------------------------------------------- */
/* Entrance and hover movement go away entirely. The progress bar keeps its
   opacity change, because it is the only signal that a slow upload is still
   running — replacing motion with nothing would remove information. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .rv-enter { animation: none; opacity: 1; transform: none; }
  .rv-page  { animation: none; opacity: 1; transform: none; }
  .btn:hover:not([disabled]), .btn:active:not([disabled]),
  .card--interactive:hover { transform: none; }
  /* The active rail is state, not decoration, so it stays — it just stops
     growing into place and is simply there. */
  .navlink::before { transition: none; }
  body.is-busy .rv-progress::before {
    animation: none;
    transform: none;
    width: 100%;
    opacity: .55;
  }
}
