/*
 * The application's stylesheet — everything outside the admin.
 *
 * One file, because a rule repeated on three pages can only be recognised as
 * shared when the three sit together. It lived in five `{% block style %}`
 * blocks until v68, and the message boxes had been three copies of one box
 * that whole time.
 *
 * **The admin is not styled from here.** It extends Django's own shell, not
 * `base.html`, and `templates/admin/` holds overrides of *its* templates. If
 * it ever needs a stylesheet it gets its own file; nothing in here may assume
 * either page is the other.
 *
 * **The tokens and components below are the Industry design system**, retuned
 * to Atlas red and PT Sans, as published by the Claude Design project this
 * interface was drawn in. Its own rule is that every colour, font, space and
 * shadow comes from a `var(--…)` and never from a literal — so a hex or a px
 * appearing in a component rule below is a bug unless its comment says why.
 * The system layers a "blueprint frame" pass over its own components at the
 * end of the file (square corners, transparent fills, hairline borders); that
 * pass is folded into the declarations here instead, because a second rule
 * undoing the first one four hundred lines later is exactly what this file
 * exists to stop.
 *
 * Page-specific rules live at the bottom under a `.p-…` class that
 * `base.html` puts on `<body>`, so they cannot reach a page that did not ask
 * for them.
 */

/* --- the typeface -------------------------------------------------------- */

/*
 * PT Sans and PT Sans Narrow, vendored rather than fetched from Google.
 *
 * Vendored for the reason `routemap` vendors Leaflet: a third party on a
 * critical path fails silently and at someone else's convenience, and this
 * service is hosted in Russia (§11.1), where fonts.gstatic.com is exactly the
 * kind of dependency that stops answering one morning. `collectstatic`'s
 * `ManifestStaticFilesStorage` rewrites the relative `url()`s below, which is
 * why they are relative.
 *
 * ParaType, SIL OFL 1.1 — the licence ships beside the files in `OFL.txt`,
 * the way Leaflet's BSD-2 does. PT Sans was commissioned for the languages of
 * the Russian Federation, which is the whole reason it is here: this
 * interface is entirely Russian, and the condensed grotesques the design
 * system was originally drawn with carry no Cyrillic at all.
 *
 * **Three subsets, and `latin-ext` is not optional.** Cyrillic is the
 * interface; `latin` carries digits and email addresses. The ruble sign is
 * neither: U+20BD sits in `latin-ext`'s U+20AD-20C0, and every price on every
 * page ends in one. Ship only the first two and «₽» silently falls back to
 * the system font, half a line away from PT Sans digits. `unicode-range`
 * means a page still fetches only the subsets it actually renders.
 */

@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-400-cyrillic.f2664d403038.woff2") format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-400-latin.3f66ddcf744f.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-400-latin-ext.c7e801bf7cc4.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-700-cyrillic.5e1ae1c98141.woff2") format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-700-latin.35e5705c45cf.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'PT Sans';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-700-latin-ext.473ddf2ce4da.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-400-cyrillic.92a60f829c46.woff2") format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-400-latin.29b661fc1e9c.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 400;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-400-latin-ext.a7e819dfa0af.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-700-cyrillic.44a1f884316e.woff2") format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-700-latin.a2c7aa6e8718.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'PT Sans Narrow';
  font-style: normal;
  font-weight: 700;
  font-display: block;
  src: url("../fonts/pt-sans-narrow-700-latin-ext.4add79f9b245.woff2") format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --- tokens -------------------------------------------------------------- */

/*
 * The Industry system's `:root`, copied from the design project's `styles.css`
 * rather than re-derived. The brand retune lives upstream now — accent red and
 * the PT Sans pair are in the system itself, not in a per-document override —
 * so this block should be replaceable wholesale when the design is re-pulled.
 *
 * Two values here look like mistakes and are not:
 *
 * `--font-heading-weight` is 700 because PT Sans Narrow ships no 600 — ask
 * Google Fonts for `wght@600` and it answers 400 Bad Request. Setting 600
 * would leave the browser to synthesise a weight that does not exist.
 *
 * `--color-accent-2-*` is byte-identical to `--color-accent-*`. This is a mono
 * scheme: the system carries a second accent role so that both sets resolve,
 * and there is no second colour. Use `.tag-accent`; `.tag-accent-2` is kept
 * only so the component layer stays complete.
 */

:root {
  --color-bg: #f2f2f3;
  --color-surface: #e9e9ea;
  --color-text: #1d1f20;
  --color-accent: #e8392a;
  --color-accent-2: #e8392a;
  --color-divider: color-mix(in srgb, #1d1f20 16%, transparent);

  /* Tonal ramps — generated in OKLCH on one shared lightness scale, so the
     same step of any role matches the others in visual value. */
  --color-neutral-100: #f5f5f8;
  --color-neutral-200: #e7e7ea;
  --color-neutral-300: #d4d4d7;
  --color-neutral-400: #b7b7ba;
  --color-neutral-500: #98989b;
  --color-neutral-600: #7a7a7d;
  --color-neutral-700: #5d5d60;
  --color-neutral-800: #424244;
  --color-neutral-900: #2b2b2d;

  --color-accent-100: #ececed;
  --color-accent-200: #dddedf;
  --color-accent-300: #c6c8ca;
  --color-accent-400: #ee6d61;
  --color-accent-500: #e8392a;
  --color-accent-600: #cf2e21;
  --color-accent-700: #ab2418;
  --color-accent-800: #861b12;
  --color-accent-900: #5e120c;

  --color-accent-2-100: #ececed;
  --color-accent-2-200: #dddedf;
  --color-accent-2-300: #c6c8ca;
  --color-accent-2-400: #ee6d61;
  --color-accent-2-500: #e8392a;
  --color-accent-2-600: #cf2e21;
  --color-accent-2-700: #ab2418;
  --color-accent-2-800: #861b12;
  --color-accent-2-900: #5e120c;

  --font-heading: "PT Sans Narrow", system-ui, sans-serif;
  --font-heading-weight: 700;
  --font-body: "PT Sans", system-ui, sans-serif;

  --space-1: 3.4px;
  --space-2: 6.8px;
  --space-3: 10.2px;
  --space-4: 13.6px;
  --space-6: 20.4px;
  --space-8: 27.2px;

  --radius-sm: 2px;
  --radius-md: 4px;
  --radius-lg: 7px;

  --shadow-sm: 0 1px 2px color-mix(in srgb, #2b2b2d 14%, transparent);
  --shadow-md: 0 3px 10px color-mix(in srgb, #2b2b2d 16%, transparent);
  --shadow-lg: 0 12px 32px color-mix(in srgb, #2b2b2d 22%, transparent);

  /* **The reading column.** A page that needs a wider one sets this on its own
     `<body>` class; `.p-history` declines it altogether, because six columns
     with two of them full of Russian addresses want the window. */
  --page-width: 780px;
  /* **The gutter, and it is one number for the whole application.** The header
     row stands at it, the content stands at it, and the map screen's furniture
     stands at it — so a logo, a table's first column and the credit in the
     corner of a map are all the same distance from the glass. It was 20px for
     the row and 13.6 for the content, which is exactly the kind of difference
     nobody can name and everybody can see: on the history page the mark sat
     seven pixels right of the column it was heading. */
  --page-gutter: 20px;
}

/* --- shell --------------------------------------------------------------- */

/* The fix for «Откуда is wider than the banner», which was never a width.
   `width: 100%` sizes the *content* box by default, so an input at 100%
   rendered 100% **plus** its padding and border — about 20px past every block
   element beside it, and past the container. A plain <p> has no width of its
   own and ends where the column ends. With border-box the two agree. */
*, *::before, *::after { box-sizing: border-box; }

/*
 * **The shell is a frame the size of the window, and the window never scrolls.**
 *
 * `html` reserved a scrollbar's width unconditionally for a while, because the
 * page column is centred and a page long enough to scroll put it in a
 * different place from one that was not. The quote screen had to be excepted:
 * a reserved gutter *clips* — nothing can paint into it — and the map has to
 * reach the glass. So the two halves of the application disagreed by exactly a
 * scrollbar, and the logo and the account menu jogged 15px on every navigation
 * between them.
 *
 * Pinning the row does not answer that. Measured on a 1240px window: an
 * in-flow block 20px from the edge lands at 1205, and `position: fixed;
 * right: 20px` lands at 1205 too — a fixed element's containing block is the
 * scrollport, not the glass. Only `100vw` escapes, and an element that escapes
 * the scrollport no longer lines up with the content under it.
 *
 * So the window stops scrolling instead. The header row sits outside the one
 * region that does, which is what makes its position independent of whether
 * anything is long; there is no gutter to reserve anywhere; and a bar, when
 * one is needed, appears beside the thing it scrolls — the rule the quote
 * panel already follows for its own middle, applied to the application.
 *
 * At 900px and below the document scrolls again: an inner scroll container on
 * a phone takes away the address bar collapsing and pull-to-refresh, which is
 * a worse trade than a row that moves — and phones draw overlay bars that
 * reserve nothing, so there is nothing there to move. See the phone block
 * below, which is where `scrollbar-gutter` now lives.
 *
 * **900, 680 and 440 are the design's own numbers**, read off the container
 * queries in `Котировки грузоперевозок.dc.html` rather than chosen here: 900
 * narrows the panel and thins the history table, 680 turns the panel into the
 * sheet, 440 spans the navigation and stacks the history. The application used
 * to break at 861 and 720, which were two numbers that agreed with nothing.
 *
 * The quote screen is the one exception and keeps its frame at every width —
 * see the sheet block. A map is a fixed frame on a movable world, and it has no
 * «further down» to scroll to at any size.
 */
@media (min-width: 901px) {
  body { display: flex; flex-direction: column;
    height: 100vh;
    /* `dvh` second, so a browser that knows it overrides the line above. */
    height: 100dvh;
    overflow: hidden; }
  /* The region takes the slack and is the only thing allowed to shrink;
     `min-height: 0` is what lets it be shorter than its contents, and without
     it the region grows to fit and the window scrolls again. The two bands
     above it keep their own height at any window size — a chrome that shrinks
     to nothing on a short screen is worse than one that leaves no room. */
  .chrome, .service-notice, .flash { flex: none; }
  .scroll { flex: 1; min-height: 0; overflow-y: auto; }
}

/*
 * **At 900 and below the window scrolls, and the quote screen still does not.**
 *
 * Everything on this side of the breakpoint goes back to being an ordinary
 * document: the shell stops being a frame, the region stops being the thing
 * that scrolls, and the gutter is reserved again — this is the one place the
 * old jog between a long page and a short one can still happen, and a narrow
 * *desktop* window with classic bars is where it would. A phone draws overlay
 * bars and reserves nothing either way.
 *
 * The quote screen declines all of it. Below 680 it is a map with a sheet over
 * it (see the sheet block), and between 681 and 900 it is still a panel beside
 * a map — neither of which is a document with a bottom. So it keeps the frame
 * at every width, and the gutter that would otherwise be reserved for a
 * scrollbar it never has is taken back off the one page whose map reaches the
 * glass: a reserved gutter *clips*, and a map that stops 15px short of the edge
 * is the bug this file spent §4.1 explaining.
 */
@media (max-width: 900px) {
  /*
   * **Every page reserves, including the one that never scrolls.**
   *
   * The quote screen is a frame here and has no bar to reserve for, so the
   * obvious rule is to give it the space back — and that is the rule that was
   * written first. It costs exactly what §4.1 spends a section on: the two
   * halves of the application then disagree by a scrollbar's width, and the
   * mark and the account menu jog 15px on every navigation between «Расчёт»
   * and «История».
   *
   * The other way round costs the map 15px of its right edge, and this is the
   * one place that is the cheaper of the two. Both costs land only where the
   * scrollbars are *classic* — a desktop browser at a narrow window. A phone
   * draws overlay bars, which reserve nothing, so there the gutter is a no-op,
   * the map reaches the glass and nothing moves either way. So the trade is
   * really «a jogging row on a narrow desktop window» against «a 15px strip on
   * a narrow desktop window», and a row that moves on every navigation is the
   * one somebody notices.
   *
   * Above 900 neither cost exists, because nothing scrolls at the window at
   * all and there is no gutter to reserve anywhere.
   */
  html { scrollbar-gutter: stable; }
  .scroll { overflow: visible; }
  body.p-quote { display: flex; flex-direction: column;
    height: 100vh;
    height: 100dvh;
    overflow: hidden; }
  .p-quote .chrome, .p-quote .service-notice, .p-quote .flash { flex: none; }
  /* The region takes the slack here exactly as it does above the breakpoint.
     Without it the frame has bands and no middle: `body` is a flex column of
     fixed-height items, so `.scroll` is sized by its contents, and a `.scroll`
     that is `overflow: hidden` with nothing telling it to grow is a box of
     height zero — the bar, and then nothing. */
  .p-quote .scroll { flex: 1; min-height: 0; }
}
/*
 * **One scrollbar for the application, wherever the scrolling happens.**
 *
 * Two elements scroll: the shell's region and the quote panel's middle. They
 * are drawn the same, because a browser's default bar beside one of them and a
 * 4px rule beside the other is two designs. The thumb is the neutral ramp with
 * a 3px hole punched through it in the page's own ground, which is how the
 * design draws a 10px track around a 4px bar; `scrollbar-width` and
 * `scrollbar-color` say the same to Firefox, which has no `::-webkit-`
 * pseudo-elements.
 *
 * Neither reserves the space, for the same reason: a reserved gutter comes out
 * of the scroll container's content box, so everything inside stops short of
 * the edge it is drawn to — the table's last column on a history page, and
 * every rule across the quote panel. A page or a panel that fits runs to its
 * own edge, and one that does not gets the bar in the strip at the side, where
 * it reads as belonging to what it scrolls.
 */
.scroll, .p-quote .panel-body {
  scrollbar-width: thin;
  scrollbar-color: var(--color-neutral-400) transparent;
}
.scroll::-webkit-scrollbar, .p-quote .panel-body::-webkit-scrollbar { width: 10px; }
.scroll::-webkit-scrollbar-track,
.p-quote .panel-body::-webkit-scrollbar-track { background: transparent; }
.scroll::-webkit-scrollbar-thumb,
.p-quote .panel-body::-webkit-scrollbar-thumb {
  background: var(--color-neutral-400);
  border: 3px solid var(--color-bg); }
.scroll::-webkit-scrollbar-thumb:hover,
.p-quote .panel-body::-webkit-scrollbar-thumb:hover {
  background: var(--color-neutral-600); }

/* A frame prints one screen. These are documents somebody may print — the
   agreement and the privacy policy most of all — so on paper the shell goes
   back to being as tall as what is in it. */
@media print {
  body { display: block; height: auto; overflow: visible; }
  .scroll { overflow: visible; }
}

/*
 * One ground, one ink. The system is a light design and is not themed: there
 * is no dark half here any more.
 *
 * `color-scheme: light dark` stood at the top of this file until the redesign,
 * and with it every colour had to be chosen twice or left to a system keyword.
 * Industry defines a single light palette and derives its dividers, shadows
 * and tints from that one ground — `--shadow-*` is tuned to #f2f2f3 by
 * construction — so following the reader's system setting would mean deriving
 * a second set of tokens the design does not have and cannot check. Declaring
 * `light` is the honest version of what the stylesheet actually supports.
 */
:root { color-scheme: light; }

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  font-weight: 400;
}

/*
 * Industry's type scale, with one deliberate exception.
 *
 * `h1` is 25px here and 42px in the system. 42 is a landing-page size, and
 * this application has no landing page: every `<h1>` is the title of a working
 * screen sitting directly under the navigation bar, which is what the design
 * draws at 25. The rest of the ramp is the system's, untouched, so `h2`
 * through `h6` still step the way the design's own pages do — `h6` is the
 * small uppercase label the screens use for panel headings.
 */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--font-heading-weight);
  line-height: 1.12;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-2);
}
h1 { font-size: 25px; }
h2 { font-size: 32px; }
h3 { font-size: 25px; }
h4 { font-size: 20px; }
h5 { font-size: 16px; }
h6 { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; }

p { margin: 0 0 var(--space-3); }
img { display: block; max-width: 100%; }

/* A visited link is not a different kind of link. Nothing set a link colour
   before v68, so the browser's default applied and every link the dispatcher
   had followed turned purple and stayed that way — loudest on the history
   list, where it marked out which quotes had been opened. That is a reading
   trail, not information about the quote. Both states get the one colour, and
   they must keep sharing a declaration: splitting them is how the purple
   comes back. */
a:link, a:visited { color: var(--color-accent); text-underline-offset: 3px; }

.text-muted { color: color-mix(in srgb, var(--color-text) 55%, transparent); }

/*
 * Present for a screen reader, absent on the screen.
 *
 * Not `display: none` and not `visibility: hidden` — both take the element out
 * of the accessibility tree, which is the one thing this must not do. The
 * clip-rect dance is the version that survives every engine.
 */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/*
 * Themed rather than the browser's. The system's rule: no element is left with
 * the default blue ring on a page whose accent is red.
 *
 * **Ink, not the accent.** It was accent, and on this interface the accent
 * carries two meanings already — «you can act on this» on a button and «this
 * is wrong» under a field. A third, «you are here», is one more than a single
 * colour can hold, and it landed worst where it mattered most: a focused
 * address box was a 2px red ring pulled tight against a red border, which
 * reads as an objection to something nobody has typed yet. `neutral-800` is
 * the system's own answer and says only where you are.
 */
:focus { outline: none; }
:focus-visible { outline: 2px solid var(--color-neutral-800); outline-offset: 2px; }
/* Selected text keeps the browser's own highlight. The system tinted it
   accent at 30%, which the design has since dropped: a selection is the
   reader's mark on the page rather than ours, every other application they
   have open draws it the same way, and the tint landed on a red that also
   means «error» here. */

/* The centred column every page except the quote screen is. It was `body`
   itself until the redesign, which worked while every page was a column; the
   navigation bar and the service notice are full-bleed and the quote screen
   will be, so the width belongs to the content rather than to the document. */
/*
 * **The column starts at the gutter, it is not centred in the window.**
 *
 * It was centred, which put «Смена пароля» 243px in on a 1240 screen while the
 * mark above it stood at 20 — two left edges on a page with one column of
 * content, and the reader's eye has to find the second one. The design draws
 * every page the same way: the content begins where the header row begins and
 * stops where its own width runs out. What is to the right of it is margin,
 * not a frame.
 */
.page { max-width: var(--page-width); margin: var(--space-8) 0;
  padding: 0 var(--page-gutter); }

/* --- navigation ---------------------------------------------------------- */

/*
 * The only navigation this application has. A history page nothing links to
 * is a page nobody finds, and §10.6 promises the user sees their own.
 *
 * Where you can go on the left, who you are on the right — the brand takes the
 * slack with `margin-right: auto`, which is what parts the two groups without
 * drawing anything between them.
 */
/*
 * **The chrome: where you are, where you can go, and who you are.**
 *
 * One group, two placements. On every ordinary page it is a header row with
 * the mark on the left and the rest pushed right. On the map screen there is
 * no row — the panel is flush to the top edge — so the same group floats over
 * the tiles at the top-right corner and the mark goes into the panel instead.
 * The difference is entirely in `.p-quote`'s own rules below; nothing here
 * knows which page it is on.
 */
/*
 * **The header row spans the window, at the gutter — not the reading column.**
 *
 * It was tied to `--page-width`, which is right on a page whose content is as
 * wide as the window and wrong on every other: on «Соглашение» the mark landed
 * 243px in and the account menu stopped 257px short of the right edge, because
 * both were being centred inside a 780px column of prose. The map screen has
 * always put its navigation at the window's corner; this is every other page
 * agreeing with it.
 *
 * So the row is the window minus the gutter, and what varies per page is the
 * *content* under it. A page that wants its content to line up with the mark —
 * history, whose table is the widest thing here — declines `--page-width`
 * rather than guessing at a number.
 *
 * The bottom gap belongs to `.page`'s own top margin; a padding here would add
 * to it.
 */
/*
 * **`--chrome-top` is the one number that keeps the two placements level.**
 *
 * The row and the floating group are the same object in two positions, so the
 * distance from the top edge has to be stated once. It was written twice — as
 * this padding and as the floating group's own offset — and the two drifted the
 * moment one of them moved, which is a bar that jumps on every navigation
 * between a page with a map and a page without one.
 */
:root { --chrome-top: 20px; }
.chrome { display: flex; align-items: center; gap: var(--space-2);
  padding: var(--chrome-top) var(--page-gutter) 0; }
/* The mark takes the slack, which is what parts «where you can go» from «who
   you are» without drawing anything between them. `height` with `width: auto`
   rather than the attribute pair alone: the file is 187×49 and the row wants
   22px, and the intrinsic attributes stay on the element so the box is
   reserved before the image loads. */
/* `align-self: flex-start` with the optical `-2px`: the mark's own artwork
   carries a rule under the word, so centring the whole image against a 40px
   control leaves the letters sitting low in the row. */
.brand { display: inline-flex; align-self: flex-start; margin-right: auto;
  margin-top: -2px; }
.brand img { height: 20px; width: auto; }

/*
 * **The two destinations as one object.**
 *
 * The design draws them as halves of a segmented control with the current one
 * filled, rather than as two words with the current one underlined — a
 * stronger statement, and the one that survives being put on a map: a bare
 * link floating over tiles has nothing to sit on, and a rule under it competes
 * with the roads beneath.
 *
 * The fill is `neutral-900`, not the accent. The accent says «you can act on
 * this», and the segment you are already on is the one thing here you cannot.
 */
.navgrp { display: flex; background: var(--color-bg);
  border: 1px solid var(--color-divider); }
.navgrp a { display: flex; align-items: center; height: 40px; padding: 0 var(--space-4);
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 15px; letter-spacing: 0.04em; text-transform: uppercase;
  text-decoration: none; }
.navgrp a + a { border-left: 1px solid var(--color-divider); }
/* The colour has to carry the pseudo-classes. A bare `.navgrp a` is (0,1,1)
   and `a:link, a:visited` up in the element layer is also (0,1,1), so source
   order alone would decide it and the group would come out accent red. */
.navgrp a:link, .navgrp a:visited { color: var(--color-text); }
.navgrp a[aria-current="page"]:link,
.navgrp a[aria-current="page"]:visited {
  color: var(--color-bg); background: var(--color-neutral-900); }
/* Hover pays the same tax and is last for the same reason: `:link:hover` is
   (0,3,1), which beats the reset above, ties with the `aria-current` pair and
   wins that tie by sitting below them. The current segment darkens rather than
   reddening — it is already filled, and accent on a fill is a third colour for
   a state that has not changed. */
.navgrp a:link:hover, .navgrp a:visited:hover { color: var(--color-accent); }
.navgrp a[aria-current="page"]:link:hover,
.navgrp a[aria-current="page"]:visited:hover {
  color: var(--color-bg); background: var(--color-neutral-700); }

/*
 * The account menu, on `<details>` — no script.
 *
 * **Its links are in the page whether or not it is open.** That is a property
 * of the element, not a trick: `open` toggles rendering and nothing else, so
 * every `{% url %}` in the panel below is reversed on every authenticated page
 * render. A route that does not exist is a 500 on every page, not a dead link
 * in a menu nobody opened.
 */
.account { position: relative; display: inline-flex; }
/*
 * The hairline is what makes it a control. Without it the person icon was a
 * glyph floating beside two words, and nothing said it could be pressed — the
 * one element up there that opens something looked the least like it did.
 * `border-box` is set globally above, so the border costs no width.
 *
 * 40 square, to match the height of the segments beside it: the two are one
 * row, and a control a few pixels shorter than its neighbour reads as an
 * afterthought rather than as the third part of the same object.
 */
/*
 * **Its own ground, on every page.** It had one only on the map screen, where
 * it floats over tiles — and that rule, `.p-quote .account > summary`, ties
 * with `.account > summary:hover` on specificity and comes later in the file,
 * so it won. The button's border answered the pointer there and its fill did
 * not: half a hover, on the one control you have to find before you can do
 * anything with the account.
 *
 * Elsewhere this paints `--color-bg` over `--color-bg` and changes nothing
 * visible, which is the point — the ground is a fact about the control, and a
 * rule that states it per screen is a rule that can lose an argument with a
 * state. What is left to the map screen is the shadow, which is the only part
 * that really is about floating over something.
 */
.account > summary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; cursor: pointer;
  background: var(--color-bg);
  border: 1px solid var(--color-divider);
  list-style: none;            /* Firefox */
}
.account > summary::-webkit-details-marker { display: none; }
/* Both the fill and the edge answer the pointer. The rows inside the panel
   already lit on hover; until the design said so the control that opens it
   did not, which made the one thing you have to find first the only unlit
   thing in the bar. */
.account > summary:hover {
  background: var(--color-neutral-200);
  border-color: var(--color-neutral-600);
}
.account-menu {
  position: absolute; right: 0; top: calc(100% + var(--space-2)); z-index: 30;
  min-width: 240px; padding: var(--space-3) 0; font-size: 13px;
  background: var(--color-bg); border: 1px solid var(--color-divider);
  box-shadow: var(--shadow-lg);
}
/* **No underlines.** A menu row is a row: it is already a target the width of
   the panel, it lights under the pointer, and four underlined phrases stacked
   in a 240px box read as a list of references rather than as somewhere to go.
   The design draws them plain, and the one rule the underline was carrying —
   «this is a link» — is carried here by the shape. */
.account-menu a { display: block; padding: var(--space-2) var(--space-4);
  text-decoration: none; }
.account-menu a:link, .account-menu a:visited { color: var(--color-text); }
.account-menu a:hover { background: var(--color-neutral-200); }
.account-menu .who { padding: 0 var(--space-4) var(--space-3); }
/* The caption over the address. Scoped to the menu so it cannot collide with
   the service notice's own label, and here rather than on the element because
   a size and a letter-spacing written into the markup are a design decision
   that no stylesheet can be asked about. */
.account-menu .who .kicker { font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; }
/* The address wraps rather than widening the panel: an email long enough to
   matter is longer than any sensible menu. */
.account-menu .who span { display: block; overflow-wrap: anywhere; }
.account-menu .out { margin-top: var(--space-2); padding-top: var(--space-2);
  border-top: 1px solid var(--color-divider); }
/*
 * A form, not a link — logout is POST-only since Django 5, which also stops a
 * stray prefetch from signing people out. So it is a `<button>` whatever it is
 * dressed as, and here it is dressed as the menu row above it.
 *
 * `.menu-item` is the third thing a button may be, beside `.btn-primary` and
 * `.btn-secondary`: not a standalone action with a fill of its own, but a row
 * whose shape comes from the thing containing it. It is a named class rather
 * than a bare `.account-menu button` selector so that `tests/test_web.py` can
 * still insist every button in this application declares which of the three it
 * is — a button that declares nothing has no fill at all, which is the point.
 */
.account-menu form { margin: 0; }
.menu-item {
  display: block; width: 100%; text-align: left;
  padding: var(--space-2) var(--space-4);
  font: inherit; color: var(--color-text);
  background: none; border: 0; cursor: pointer;
}
.menu-item:hover { background: var(--color-neutral-200); }

/*
 * The bar on a phone.
 *
 * **It becomes a real bar.** On a desktop page the row floats on the page's own
 * ground with the content lined up under it and needs no edge; at 390px the
 * navigation and the first line of the page are close enough together that
 * without a rule they read as one block. The design draws the edge, tightens
 * the gutter to 14px and drops the mark to 16px — the mark is a picture of
 * where you already are, and it is the one thing here that is not a control.
 *
 * Everything else grows. A 36px target is a comfortable click and a missed tap;
 * the segments, the account button, the menu rows and every button in the
 * application go to 44, which is the number both platforms' guidance settled
 * on. The `.p-quote` overrides that used to carry this for the calculator's own
 * fields are still below — what changed is that they are no longer the only
 * place in the application where a finger was considered.
 */
@media (max-width: 900px) {
  /*
   * **The mark centres here, and keeps its size.**
   *
   * Two things were wrong with it. The desktop row is padded at the top and
   * not the bottom, so the mark hangs from the top of it with the optical
   * nudge its artwork needs; this bar is padded evenly, so hanging left the
   * logo two pixels above a 44px control and reading as though it had slipped.
   * `align-self: center` is the whole of the fix, and the nudge goes with the
   * hanging it was correcting.
   *
   * And it stays 20px. The artboard draws 16 at 390, which was solving a width
   * problem this bar does not have at a 14px gutter — measured, the mark, both
   * segments and the account button leave room to spare. A mark that changes
   * size partway down the widths is a mark that is a different object on a
   * tablet than on a phone, and the size it changed at was not even one of the
   * design's own.
   */
  .brand { align-self: center; margin-top: 0; }
  .navgrp a { height: auto; min-height: 42px; padding: 0 var(--space-4); }
  /* Nothing about the bar's *dress* belongs in this tier. A tablet is a touch
     device, so the targets grow here; the rule under the bar and the tighter
     top inset are the phone's, and the artboards say so — `1g` draws the border
     at 390 and `8b` and `9a` draw no rule at all at 1240. Putting them here
     gave a tablet a line the design never drew for it. */
  .account > summary { width: 44px; height: 44px; }
  /* The menu is narrower than the desktop's 240 and refuses to be wider than
     the screen it opens on — `right: 0` keeps it on the frame, but a 240px box
     at 320px wide with a gutter either side is already touching both edges. */
  .account-menu { min-width: 220px;
    max-width: calc(100vw - 2 * var(--page-gutter)); }
  .account-menu a, .menu-item { padding-block: 11px; }
  .btn { min-height: 44px; }
  .btn-icon { width: 44px; height: 44px; }
}

/* --- buttons ------------------------------------------------------------- */

/*
 * Geometry on `.btn`, colour on the role class. A button with a role but no
 * `.btn` has no shape and one with neither has no fill at all, which is
 * deliberate: it fails visibly when it is added rather than silently claiming
 * to be the page's primary action. `tests/test_web.py` catches it first.
 */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; text-decoration: none;
  font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 14px; line-height: 1.2; color: var(--color-text);
  background: transparent;
  border: 1px solid var(--color-divider); border-radius: 0;
  padding: var(--space-2) calc(var(--space-3) * 1.2);
}
.btn svg { display: block; }
/* `:disabled`, and the cursor says which kind of waiting it is: the quote
   form's submit is disabled for the length of the request (`hx-disabled-elt`),
   which is progress, not refusal. */
.btn:disabled { opacity: 0.45; cursor: progress; }
.btn-primary { background: var(--color-accent); color: var(--color-bg);
  border-color: var(--color-accent); }
.btn-primary:hover { background: var(--color-accent-600); border-color: var(--color-accent-600); }
.btn-primary:active { background: var(--color-accent-700); border-color: var(--color-accent-700); }
.btn-secondary { border-color: var(--color-divider); }
.btn-secondary:hover { background: color-mix(in srgb, var(--color-text) 7%, transparent); }
.btn-secondary:active { background: color-mix(in srgb, var(--color-text) 14%, transparent); }
.btn-ghost { color: var(--color-accent); border-color: transparent;
  padding-inline: var(--space-1); }
.btn-ghost:hover { background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
.btn-ghost:active { background: color-mix(in srgb, var(--color-accent) 18%, transparent); }
/* An icon button is mostly border, so hover has to move the border too — 7%
   of ink laid over a 36px square reads as a smudge where the same wash across
   a labelled button reads as a press. */
.btn-icon { width: 36px; height: 36px; padding: 0; }
.btn-icon:hover { background: var(--color-neutral-200);
  border-color: var(--color-neutral-600); }
.btn-block { width: 100%; margin-top: var(--space-2); }
/* Anchors dressed as buttons inherit the link colour otherwise, which is the
   one thing the role classes are supposed to decide. */
a.btn:link, a.btn:visited { color: var(--color-text); }
a.btn-primary:link, a.btn-primary:visited { color: var(--color-bg); }
a.btn-ghost:link, a.btn-ghost:visited { color: var(--color-accent); }

/* Hover only while the button is live. A button that keeps lighting up under
   the cursor while it is refusing clicks says the opposite of what the dimming
   above is saying. */
.btn:disabled:hover { background: var(--color-accent); }
/*
 * A disabled *ghost* button is the one case where dimming is not enough: it
 * has no fill to fade, so all that dims is the accent word — and a pale red
 * phrase still reads as something to press. «Отправить письмо ещё раз» spends
 * most of its life like this, waiting out the cooldown. Neutral says
 * unavailable; the cursor says so a second time for anyone who tries.
 */
.btn-ghost:disabled { color: var(--color-neutral-600); cursor: not-allowed;
  white-space: nowrap; }
.btn-ghost:disabled:hover { background: transparent; }
.btn-secondary:disabled:hover, .btn-ghost:disabled:hover { background: transparent; }

/* --- forms --------------------------------------------------------------- */

/*
 * **`label` bare as well as `.field > label`, and that breadth has a cost.**
 *
 * The bare selector has to be here: Django renders the password forms and the
 * reset form itself, and those labels sit in no `.field` of ours. What it also
 * catches is every `<label>` used as a *container* rather than as a caption —
 * a whole clickable row — and it gives each one a 5px bottom margin, a 12px
 * base size and 70% ink that the row's contents inherit.
 *
 * Two rows are built that way and both have to say so: `.p-auth .consent`
 * below, and `.p-quote .variant`, where this went unnoticed for a while —
 * the margin parted rows that should touch, so the selected row's fill had a
 * gap under it, and every price in the list rendered muted. A third one will
 * need the same three declarations; `test_a_label_used_as_a_row_states_its_own_box`
 * is what asks for them.
 */
.field > label, label {
  display: block; font-size: 12px; margin-bottom: 5px;
  color: color-mix(in srgb, var(--color-text) 70%, transparent);
}
/*
 * `type=email` and `type=password` belong in the selector beside `.input`:
 * Django renders the password-reset address field and the whole password
 * change form itself, and those inputs carry no class of ours.
 */
.input,
input[type=text], input[type=email], input[type=password] {
  width: 100%; min-height: 36px; padding: 6px 10px; font: inherit;
  font-size: 14px; color: var(--color-text);
  /* The caret is ink, not accent. Industry paints it accent and it is the one
     token this file declines: the accent means «you can act on this» for a
     button and «this is wrong» for a field error, and a blinking red bar in a
     box somebody is typing an address into reads as the second. It also moves
     — a still red mark is a state, a flashing one is an alarm. */
  caret-color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-divider); border-radius: 0;
}
.input:hover,
input[type=text]:hover, input[type=email]:hover, input[type=password]:hover {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
}
/*
 * **A field takes a halo, not a ring.** Everything else on the page takes the
 * 2px ink outline above; a box is a box already, and an outline drawn round
 * one is a second rectangle a hair outside the first. The border goes to ink —
 * which is the change that carries it, from a 16% hairline to solid — and a
 * 3px wash of 12% ink sits outside that, so the whole box lifts rather than
 * gaining an edge.
 *
 * **Forced colours keep the ring**, because they throw the halo away: a
 * system palette repaints borders and shadows to its own and a `box-shadow`
 * is not guaranteed to survive at all, so the one indicator that does is put
 * back for the readers who need it most.
 */
.input:focus-visible,
input[type=text]:focus-visible, input[type=email]:focus-visible,
input[type=password]:focus-visible {
  border-color: var(--color-neutral-800);
  outline: none;
  box-shadow: var(--focus-halo);
}
@media (forced-colors: active) {
  .input:focus-visible,
  input[type=text]:focus-visible, input[type=email]:focus-visible,
  input[type=password]:focus-visible { outline: 2px solid; outline-offset: 0; }
}
/* Checkboxes keep the browser's box and take the accent, which is all the
   design asks of them. */
input[type=checkbox] { accent-color: var(--color-accent); }

/*
 * **The eye on a password box.**
 *
 * The design puts one on every password field it draws (`1e`, `3a`) and draws
 * it open, because nothing in an artboard is ever revealed. It is the same
 * object as the quote form's ✕: a ghost button inset into the right of the
 * box, lighting on hover and darkening the edge of the box it belongs to — the
 * pair is what says the control acts on *this* field rather than on the row it
 * happens to sit in.
 *
 * `min-height: 0` is not tidying. Below 900 every `.btn` grows to a 44px
 * target, and this one is inside a 36px box: without it the eye is taller than
 * the field it sits in, and a control that overflows the thing it belongs to
 * reads as a mistake. It gets its own touch size instead, with the box growing
 * to match — the same trade `.p-quote .clear` makes.
 */
.pw { position: relative; }
.pw .input,
.pw input[type=password], .pw input[type=text] { padding-right: 34px; }
/* **Centred by `inset` and `margin: auto`, not by `top: 50%` and a transform.**
   The pair works and is what `.p-quote .clear` uses, but it costs a transform
   on an element whose whole job is to sit still — and it puts the control's
   position at the mercy of anything that later has an opinion about
   `transform`. Two zero insets and an auto margin say «centre in the box» to
   the layout itself, which is one fact rather than two that have to agree. */
.eyebtn { position: absolute; right: 2px; top: 0; bottom: 0;
  margin-block: auto;
  width: 28px; height: 28px; min-height: 0; padding: 0;
  color: var(--color-neutral-600); }
.eyebtn:hover { background: var(--color-neutral-200); color: var(--color-text); }
.field:has(.eyebtn:hover) .input,
.field:has(.eyebtn:hover) input[type=password],
.field:has(.eyebtn:hover) input[type=text] {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }
/* One of the two is drawn, and `aria-pressed` is which — the state a screen
   reader is told and the state the eye is shown are one attribute, so they
   cannot drift. */
.eyebtn svg { display: block; }
.eyebtn .eye-off { display: none; }
.eyebtn[aria-pressed="true"] .eye-open { display: none; }
.eyebtn[aria-pressed="true"] .eye-off { display: block; }

/*
 * The touch size, and it has to be stated *here* rather than with the other
 * 44px targets above. Those are in a block that comes earlier in the file, and
 * `.eyebtn` is one class either way — so the base 28px below it simply won on
 * source order and the eye stayed thumb-sized for nobody. A media query that
 * overrides a rule has to come after it, which is the same trap the phone's
 * spinner fell into.
 *
 * The box grows with it: the eye lives *inside* the field, so it cannot take
 * the 44px the free-standing buttons take without being taller than what it
 * sits in.
 */
@media (max-width: 900px) {
  .pw .input,
  .pw input[type=password], .pw input[type=text] {
    min-height: 44px; padding-right: 42px; }
  .eyebtn { width: 38px; height: 38px; }
}

.hint { font-size: 12px; margin-top: var(--space-1);
  color: color-mix(in srgb, var(--color-text) 55%, transparent); }
/* The password validators' complaints. Red from the ramp's deep step rather
   than the accent itself: the accent-to-ground pair is tuned to 3:1, which is
   enough for chrome and large text and not for a line of body copy. */
.errorlist { margin: var(--space-2) 0 0; padding-left: 1.1rem; font-size: 12px;
  color: var(--color-accent-800); }

/* --- cards, tags, tables, dialogs ---------------------------------------- */

/*
 * A card is a line drawing, not a filled block — transparent, square, hairline
 * bordered. `.blueprint` adds the four registration marks the system frames
 * its objects with; they are drawn outside the box, which is why the element
 * needs `position: relative` and must not clip.
 */
.card {
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-3); border-radius: 0;
  background: transparent; border: 1px solid var(--color-divider);
}
.card-kicker { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--color-accent); }
.card-title { font-family: var(--font-heading); font-weight: var(--font-heading-weight);
  font-size: 17px; line-height: 1.2; }
.card-body { margin: 0; font-size: 13px; opacity: 0.8; flex: 1; }
.card-meta { display: flex; align-items: center; gap: 6px; font-size: 11px;
  color: color-mix(in srgb, var(--color-text) 50%, transparent); }

.blueprint { position: relative; border: 1px solid var(--color-divider);
  border-radius: 0; }
.blueprint > .corner {
  position: absolute; width: 11px; height: 11px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.blueprint > .corner::before, .blueprint > .corner::after {
  content: ""; position: absolute; background: currentColor;
}
.blueprint > .corner::before { left: 5px; top: 0; width: 1px; height: 100%; }
.blueprint > .corner::after  { top: 5px; left: 0; width: 100%; height: 1px; }
.blueprint > .corner.tl { top: -6px; left: -6px; }
.blueprint > .corner.tr { top: -6px; right: -6px; }
.blueprint > .corner.bl { bottom: -6px; left: -6px; }
.blueprint > .corner.br { bottom: -6px; right: -6px; }

.elev-sm { box-shadow: var(--shadow-sm); }
.elev-md { box-shadow: var(--shadow-md); }
.elev-lg { box-shadow: var(--shadow-lg); }

.tag {
  display: inline-flex; align-items: center; font-size: 11px;
  letter-spacing: 0.02em; padding: 3px 10px; border-radius: 0;
  white-space: nowrap;
}
.tag-accent { background: var(--color-accent-100); color: var(--color-accent-800); }
.tag-accent-2 { background: var(--color-accent-2-100); color: var(--color-accent-2-800); }
.tag-neutral { background: var(--color-neutral-100); color: var(--color-neutral-800); }
.tag-outline { border: 1px solid var(--color-accent); color: var(--color-accent); }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  text-align: left; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in srgb, var(--color-text) 60%, transparent);
  padding: var(--space-2); border-bottom: 1px solid var(--color-divider);
  white-space: nowrap;
}
.table td { padding: var(--space-2);
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); }
.table tbody tr:hover { background: var(--color-neutral-200); }

/*
 * **No dialog.** Industry ships one and this application rendered two of them,
 * for the §6 confirmation and for a refusal. The design replaced both with
 * messages that sit in the panel — `5b` and `5c` — so the component is gone
 * rather than kept against a use that no longer exists. A card and a message
 * box are what this interface has; adding a third object back is a decision to
 * take when something needs it.
 */

/* --- messages ------------------------------------------------------------ */

/*
 * One box in three colours. They were three copies of the same six
 * declarations across ten templates; only the border and the fill differ, and
 * each keeps its own name so no markup had to change to say so.
 *
 * The tints come from the ramps rather than from `color-mix` on an arbitrary
 * hex, which is the system's rule and the reason these three now sit at the
 * same visual weight as each other.
 */
.banner, .error, .notice {
  border: 1px solid; padding: var(--space-3) var(--space-4); border-radius: 0;
  margin-bottom: var(--space-6); font-size: 14px;
}
.banner { border-color: var(--color-accent-400); background: var(--color-accent-100);
  color: var(--color-accent-800); }
.error  { border-color: var(--color-accent-500); background: var(--color-accent-100);
  color: var(--color-accent-800); }
.notice { border-color: var(--color-neutral-400); background: var(--color-neutral-100);
  color: var(--color-neutral-800); }

/*
 * The service notice — §9.7's "channel of its own".
 *
 * `Quote.warnings` is operator information and never reaches a page; a planned
 * outage or a seasonal restriction is the opposite, and had nowhere to go. It
 * is full-bleed and above the page column because it is about the service
 * rather than about the screen you happen to be on.
 */
.service-notice {
  display: flex; align-items: center; gap: var(--space-4);
  padding: 9px var(--space-4); font-size: 13px;
  background: var(--color-accent-100); color: var(--color-accent-800);
  border-bottom: 1px solid var(--color-divider);
}
.service-notice .label { font-family: var(--font-heading); font-size: 11px;
  letter-spacing: 0.06em; text-transform: uppercase; }
.service-notice .body { flex: 1; }
.service-notice button { border: 0; color: var(--color-accent-800); }

/*
 * **A flash is the same band, saying something about what just happened.**
 *
 * The service notice is about the service and stays until dismissed; this is
 * about the navigation that just finished and is gone on the next one. Same
 * shape, because they occupy the same strip and two shapes there would read as
 * two systems — and the same ✕, because a band across the top of a screen
 * somebody is working in should be closeable whatever put it there.
 *
 * Neutral rather than accent for the ordinary case: the accent means «you can
 * act on this» and an outcome is the opposite, a thing that has already
 * happened. An error takes the accent because that is the one flash worth
 * interrupting for.
 */
.flash {
  display: flex; align-items: center; gap: var(--space-4);
  padding: 9px var(--space-4); font-size: 13px;
  background: var(--color-neutral-100); color: var(--color-neutral-800);
  border-bottom: 1px solid var(--color-divider);
}
.flash .body { flex: 1; }
.flash button { border: 0; color: inherit; }
.flash-error { background: var(--color-accent-100); color: var(--color-accent-800); }

/* --- the map's palette --------------------------------------------------- */

/*
 * **The map paints itself from these five properties.**
 *
 * `routemap` ships its own defaults and reads each role as
 * `var(--role, fallback)`, so declaring them here is the whole of retuning it.
 * Nothing passes the component a colour or a width and no script reads one
 * back: its paths and pins carry classes saying what they *are* and which are
 * selected or hovered, and `routemap.css` paints them.
 *
 * On `:root` rather than on the container, so the address dots below can use
 * the same two properties. A dot beside «Откуда» and the pin at the start of
 * the route are one mark shown twice, and they cannot be allowed to drift.
 *
 * The routes are deliberately not the accent. The design keeps red for what
 * you can act on; the unselected ones are pale enough to read as alternatives
 * and dark enough not to read as roads.
 *
 * The chosen one was `neutral-900`, the page's own ink, drawn at 6px over
 * tiles — the darkest thing on the screen by a distance, including the pins
 * marking where it starts and ends. `neutral-700` is what the design moved it
 * to: still unmistakably the selected line against `neutral-400`, and no
 * longer a bar laid across the map.
 */
:root {
  --route-selected: var(--color-neutral-700);
  --route-unselected: var(--color-neutral-400);
  --route-casing: var(--color-bg);
  --pin-origin: var(--color-accent);
  --pin-destination: var(--color-neutral-900);
  /* The casings and the hover derive from these two, so heavier lines stay in
     proportion rather than being six numbers to keep in step by hand. */
  --route-width: 5px;
  --route-width-selected: 6px;
  /* The zoom buttons, so they read as the same kind of object as every other
     control on the page rather than as Leaflet's. */
  /* 20px from the edges, which is what everything else standing on the map
     is inset by — the chrome at the top right, the credit at the bottom. */
  --control-inset: 20px;
  --control-bg: var(--color-bg);
  --control-bg-hover: var(--color-neutral-200);
  --control-border: var(--color-divider);
  /* The edge moves with the fill, as it does on every other icon button here
     — see `.btn-icon:hover`. A 36px square is mostly border, so a wash inside
     one without the edge is a smudge rather than a press. */
  --control-border-hover: var(--color-neutral-600);
  --control-text: var(--color-text);
}

/*
 * **A caution is amber, and amber is not in Industry.**
 *
 * The system has one accent and a neutral ramp, which is enough for «act on
 * this» and «this is wrong» but leaves «this will work, and here is what it
 * will do» with nothing to say. It was drawn in neutrals and read as disabled
 * rather than as a warning — a grey box beside a red one looks like the box
 * that does not matter. Three values, from the design, and no more: a ground,
 * a rule, and a mark solid enough to be legible at 17px.
 */
:root {
  --color-warning-bg: #fdf6e8;
  --color-warning-border: #d8a33f;
  --color-warning-mark: #c98a1e;
  /* One step deeper, for the pointer. The red ramp has a step to move to and
     this does not, so the design names a fourth value rather than tinting one
     of the three by eye. */
  --color-warning-deep: #a56d12;

  /* **A focused field's halo**, which is the system's answer for a box rather
     than a ring: the border goes to ink and this sits outside it. Named here
     because a box in trouble has to compose the two — `box-shadow` is one
     list, so its own edge and this cannot be layered by the cascade. */
  --focus-halo: 0 0 0 3px color-mix(in srgb, var(--color-text) 12%, transparent);
}

/* --- the quote form ------------------------------------------------------ */

/*
 * The one screen that is not a column.
 *
 * `.page` centres everything else at 780px; here the panel and the map divide
 * the whole width, so the quote page opts out. The nav and the service notice
 * stay full-bleed above it either way.
 *
 * The chain of heights is the fiddly part and all three links are needed: the
 * map is told `height: 100%` by the component, which resolves only if its pane
 * has a definite height, which it gets from the grid row, which stretches only
 * because the page fills the viewport. Break any one and the map collapses to
 * nothing — the single most common way a Leaflet map appears broken.
 */
/*
 * **Nothing here scrolls at all — not even the region that does elsewhere.**
 *
 * The frame is the shell's now (see `@media (min-width: 901px)` above); what
 * this page adds is that its scroll region does not scroll either. The map is
 * a fixed frame on a movable world: there is no "further down" in it, and a
 * bar offering one is an offer it cannot keep. The panel scrolls in its own
 * middle instead.
 *
 * `min-height: 0` on both is what lets them be shorter than their contents;
 * without it a flex item refuses to shrink below them and the clipping lands
 * on the map instead.
 */
.p-quote .scroll { display: flex; flex-direction: column; overflow: hidden; }
.p-quote .page { max-width: none; margin: 0; padding: 0; flex: 1; min-height: 0;
  display: flex; flex-direction: column; }
/*
 * **The map is the page; everything else floats on it.**
 *
 * It was two columns of a grid, panel beside map. The design turned that
 * around: the map fills the frame and the panel, the navigation, the zoom
 * buttons and the credit are objects standing on top of it. What that buys is
 * the map — at 1240 the pane went from 780px wide to the whole 1240, and a
 * route drawn across a region is mostly a picture of distance.
 *
 * `min-height: 0` is still the link that lets this be shorter than its
 * contents want to be; without it a flex item refuses to shrink below them and
 * the overflow reappears on the document, where there is nothing to scroll.
 */
.p-quote .quote { position: relative; flex: 1; min-height: 0; }

/*
 * The panel: flush to the left, top and bottom edges, over the map.
 *
 * **Two bands, the same two every other page has.** A fixed strip with the
 * mark in it, and one scrolling region under it holding everything else — on
 * this screen that region is the panel rather than the window, but the shape
 * is the shell's. What it buys is one bar down the side of the pane: the head
 * held the addresses for a while, which put a short bar starting below them
 * and ending above «Сохранить», floating in the middle of a pane rather than
 * running down the side of one.
 *
 * The addresses scroll away with everything else, which is what one bar costs
 * and the reason «Сохранить» is sticky instead: a control you have to hunt for
 * is worse than a question you have to scroll back to.
 *
 * `overflow: hidden` on the panel is what stops the suggestion list escaping
 * it — the list is positioned against a box inside the scroll region, and
 * nothing else would hold it inside the panel.
 */
.p-quote .panel { position: absolute; left: 0; top: 0; bottom: 0; z-index: 10;
  width: var(--panel-width); display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--color-bg);
  border-right: 1px solid var(--color-divider);
  box-shadow: var(--shadow-lg); }
/* The mark, and nothing else. No rule under it: the shell's header row has
   none either, and the one that used to be here belonged to the addresses —
   it went into the scroll region with them. */
.p-quote .panel-head { flex: none; padding: 18px var(--space-6) 14px; }
/* `margin-top: 0` undoes the header row's optical nudge: that one is about
   sitting a mark beside a 40px control, and here the logo is a block at the
   top of a padded box with nothing to line up with. */
/* `fit-content`, or the link is the whole strip. A block-level anchor fills
   its container, so the 77px logo came with 320px of panel beside it that
   navigated when clicked — an invisible target, which is the worst kind. The
   box stays block-level rather than going inline: an inline one sits on a
   text baseline and the line box under it would push the addresses down by a
   descender. */
/* The shell renders a mark too, and everywhere but one state it is this one
   that is shown — the bar's copy is drawn only on a phone before there is a
   map, where there is no panel head to hold it. */
.p-quote .chrome .brand { display: none; }
.p-quote .panel-head .brand { display: block; width: fit-content; margin: 0; }
.p-quote .panel-head .brand img { height: 20px; width: auto; }
/*
 * The question, at the top of the region that scrolls, under the rule that
 * used to be the head's.
 *
 * The bleed is what lets a block inside a padded box draw a rule across the
 * whole panel; the padding puts its contents back where they were. The
 * negative top margin lifts it past the region's own padding so it sits flush
 * under the mark, and the bottom margin gives back the gap that padding was
 * providing to whatever comes next.
 */
.p-quote .panel-form { flex: none;
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-6);
  padding: 0 var(--space-6) 16px;
  border-bottom: 1px solid var(--color-divider); }
/*
 * The middle. `min-height: 0` again, for the same reason as everywhere else in
 * this file: without it the flex item grows to its contents and the scrollbar
 * lands on the wrong element.
 *
 * **No reserved gutter here, and that is a trade rather than an oversight.**
 * A reserved gutter comes out of the scroll container's content box, and
 * nothing inside a scroll container can be drawn wider than that — so with one
 * reserved, every rule across this panel stopped 10px short of its right edge:
 * measured, 0–429 inside a 440px pane. The rules under the addresses and above
 * «Сохранить» are the panel's own edges drawn across it, and a rule that stops
 * short of the edge it is drawn to reads as a mistake, because it is one.
 *
 * What it costs is the shift the reservation was there to prevent: an answer
 * long enough to need a bar narrows the column by 10px as it arrives. That
 * happens when a new answer replaces the old one, which is a moment the whole
 * panel is changing anyway — where the rules are wrong on every screen, all
 * the time. The shell's region does the same for the same reason.
 *
 * How the bar is *drawn* is above, with the shell's, since both scrollers draw
 * it the same way.
 */
/*
 * A flex column, and not only for the gaps: it is what gives the answer inside
 * it a height to fill. The design draws «Сохранить» at the foot of the panel
 * whether there are six route variants or one, and the rule that puts it there
 * is `margin-top: auto` on the foot — which measures nothing unless every box
 * between it and the panel is stretching. Take the flex chain apart anywhere
 * and the button rides up under the last route.
 */
.p-quote .panel-body { flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column;
  padding: var(--space-6) var(--space-6) 0; }
/*
 * The foot: the last thing in the region, and nothing more than that.
 *
 * **It was sticky, and it should not have been.** «Сохранить» on the calculator
 * earns it — it is the one control the screen exists to reach, and hunting for
 * it behind a scroll is the difference between selling a price and losing it.
 * «Открыть в панели администратора» is the opposite kind of control: staff
 * only, opened rarely, and a way *out* of the page rather than the thing the
 * page is for. Pinning it spent a band of every quote screen, at every width,
 * on a link most readers never see and no reader needs in a hurry — and drew a
 * rule across the panel to do it.
 *
 * So the construction goes with the stickiness: no bleed, because it draws no
 * rule to reach the edges with; no ground, because nothing passes behind it;
 * and no `margin-top: auto`, because it is no longer looking for the bottom.
 * It sits under the price card, which is where the design draws it.
 */
.p-quote .panel-foot { flex: none;
  margin-top: var(--space-6); padding-bottom: var(--space-6); }
.p-quote .panel-foot .btn-block { margin-top: 0; }

/*
 * The chrome floats at the top-right, clear of the panel.
 *
 * It carries a shadow here and nowhere else: on an ordinary page it sits on
 * the page's own ground and needs nothing, and over tiles it needs to be
 * legible as a thing in front rather than a thing printed on the map. The
 * brand is not in it — see `{% block brand %}`; on this screen the mark is in
 * the panel head, above the addresses.
 */
/*
 * **In the flow, with no height of its own.** `position: absolute` was measured
 * against the initial containing block rather than against what precedes it, so
 * a live `SERVICE_NOTICE` rendered its band at the top of the page and the
 * navigation floated on top of it — rare, silent, and shipped. A zero-height
 * flex item takes no room, so the map still reaches the top edge, but the
 * notice is a flex item *before* it and now pushes it down.
 */
/*
 * **And it floats over the frame, not over the window.** `order` is what says
 * so. The bar is the first child of the body's column, so a zero-height box at
 * the top of it puts its controls at the *window's* top-right — which is where
 * a service notice's own band is. v134 claimed to have fixed that by bringing
 * the bar into the flow; measured, it fixed nothing, because a zero-height
 * first item and an absolutely positioned one land in the same place. The
 * notice's ✕ sat under the account button at 1190..1226 against 1180..1220,
 * and `elementFromPoint` at its centre returned the account's own summary: the
 * one control on that band could not be pressed at all.
 *
 * Ordering the bar after the notices puts its zero-height box at the top of the
 * region instead, so the controls float over the map and the band above them is
 * whole. `order` moves the box and not the markup, so the tab order is
 * untouched — the navigation is still the first thing reached, as on every
 * other page.
 */
.p-quote .chrome { position: relative; z-index: 20; order: 2;
  height: 0; overflow: visible;
  max-width: none; margin: 0; padding: 0 var(--page-gutter);
  align-items: flex-start; justify-content: flex-end;
  border-bottom: 0; }
.p-quote .service-notice, .p-quote .flash { order: 1; }
.p-quote .scroll { order: 3; }
/* **The inset is the children's, not the box's.** A margin or a vertical
   padding on the bar would be laid out even though its height is zero, and the
   map would start that far down the frame behind a strip of page ground — which
   is a white line across the top of the screen, and was exactly the bug. The
   horizontal padding is free: it adds no height. */
/* **Not `--control-inset`.** The zoom buttons and the credit take that one,
   and the bar is not one of them: it is the same group that stands in the
   header row on every other page, and it has to line up with where that row
   puts it. Reading the furniture's inset instead put it 8px in from the right
   and 2px up from the top of where «История» draws it, at tablet widths where
   the two numbers part company. */
.p-quote .chrome > * { margin-top: var(--chrome-top); }
/* Only the shadow: the ground is theirs everywhere (see `.account > summary`),
   and stating it again here is what made this rule outrank their hover. */
.p-quote .navgrp, .p-quote .account > summary { box-shadow: var(--shadow-md); }
/*
 * **`z-index: 0` here is load-bearing: it is what keeps the map underneath the
 * rest of the page.**
 *
 * Leaflet stacks its own furniture from 200 to 1000 — panes at 400, markers at
 * 600, controls at 1000. None of that is contained by default: neither this
 * pane nor the panel beside it creates a stacking context, so those numbers
 * compete directly with ours in the root context and win every time. The
 * account menu opened at 30 and the suggestion list at 20, both of them behind
 * the map.
 *
 * A stacking context here confines all of it. The alternative — raising every
 * overlay in the application past 1000 — is the same bug deferred: the next
 * thing that opens over the map starts the auction again.
 */
/*
 * It is also what fills the frame: `inset: 0` against `.quote`, so the map is
 * the whole screen and the panel stands on it. `absolute` with a `z-index`
 * makes the stacking context just as `relative` did — and `height: 100%` on
 * the container inside now resolves, which it cannot against a parent whose
 * own height is its content's.
 */
.p-quote .map-pane { position: absolute; inset: 0; z-index: 0; min-width: 0; }
/* Square, because it is a pane of the screen rather than a card on it. */
.p-quote .routemap { border-radius: 0; }

/* The two addresses as one object: a dot each, and the swap between them. */
/*
 * A grid rather than a row of boxes, because the swap is drawn between the two
 * addresses and written after «Рассчитать».
 *
 * Tab follows the source, so the source is the order a quote is entered in —
 * «Откуда», «Куда», «Рассчитать», then the swap. Grid placement is what lets
 * the picture disagree with that: `order` would have moved the button and left
 * the sequence exactly where it was, which is the opposite of what is wanted.
 *
 * The form's other children are the CSRF and idempotency inputs, and a hidden
 * input is `display: none`, so neither becomes a grid item and neither needs a
 * placement.
 */
/*
 * **The two gutters that separate a box from the panel's edges, named.**
 *
 * On the left the coloured dot and its gap; on the right the swap button and
 * the grid's column gap. Three rules spend these numbers — the row, the swap
 * column and the suggestion list, which has to reach back across both to run
 * the panel's full width — and a list that did not follow the boxes when one
 * of them changed would be the bug this prevents.
 */
/* The panel's width, named because three rules need it and the design has
   already moved it twice — 372 to 460 when the addresses turned out to be
   longer than the column they were read in, and back to 440 when the map went
   full-bleed underneath and the panel became something standing on it. */
/*
 * **The bleed is arithmetic, so everything it counts has to be a token.**
 *
 * The suggestion list reaches the panel's edges from a box that is inset from
 * them twice over — by the panel's own padding, and by the dot on one side and
 * the swap on the other. It was written as `--lead-gutter + --space-6` and
 * `36px + --space-3 + --space-6`, which is three constants, two of which stop
 * being true on a phone: the padding there is the page gutter, and the swap
 * grows to 44px for a thumb. The list then hung six pixels off the left edge of
 * the screen and stopped seventeen short of the right.
 */
.p-quote { --panel-width: 440px;
  --panel-pad: var(--space-6);
  --swap-size: 36px;
  --lead-gutter: 19px;
  --trail-gutter: calc(var(--swap-size) + var(--space-3)); }
/* The gap under the mark is the *form's*, not the addresses'. It was a
   `margin-top` on `.endpoints`, which is one of two items in the grid's first
   row — so the row was taller than the boxes by that margin, and the swap,
   centred in the row, sat half of it above the middle of the pair it swaps.
   Nothing looked wrong; it was seven pixels high. */
.p-quote .quote-form { display: grid; grid-template-columns: 1fr auto;
  column-gap: var(--space-3); padding-top: var(--space-4); }
.p-quote .endpoints { grid-area: 1 / 1; display: flex; flex-direction: column;
  gap: var(--space-3); min-width: 0; }
/* Centred against the pair of boxes, which is where it was when it was their
   sibling — and now that the row is exactly the pair, that is what it means. */
.p-quote #swap { grid-area: 1 / 2; align-self: center; }
/* Row 2, spanning both columns, and placed rather than left to flow: the row
   above is positioned explicitly, so an unplaced item would be auto-placed
   into the first free cell. It was row 3 while «Рассчитать» held row 2. */
.p-quote #form-message { grid-area: 2 / 1 / 3 / -1; }
.p-quote #form-message:empty { display: none; }
/* Centred, because the box is the whole row now. It was bottom-aligned while a
   label sat above the input, which is why the dot carried a margin to push it
   down to the box; both go together. */
.p-quote .row { display: flex; align-items: center; gap: 9px; }
/* The very same properties the pins are drawn from, declared once above. A dot
   and the pin it stands for are one mark shown twice. */
.p-quote .dot { width: 10px; height: 10px; flex: none; border-radius: 50%; }
.p-quote .dot.from { background: var(--pin-origin); }
.p-quote .dot.to { background: var(--pin-destination); }

.p-quote .field { position: relative; flex: 1; min-width: 0; }
/* Room for the clear button, which sits inside the box. */
.p-quote .field .input { padding-right: 34px; }
/*
 * **There is nothing to clear in an empty box, so there is no ✕ in one.**
 *
 * `:placeholder-shown` is the test, and it is the right one rather than a
 * convenient one: it matches exactly when the box is empty, it updates when
 * the script writes a value as well as when somebody types, and it needs no
 * class for a script to forget to take off. Both boxes carry a placeholder —
 * «Откуда» and «Куда» — which is what makes it work here.
 *
 * The room the ✕ occupies is held whether or not it is drawn. It only matters
 * to text long enough to reach it, and text long enough to reach it is text
 * that would reflow the moment the first character arrived.
 */
.p-quote .clear { position: absolute; right: 2px; top: 50%;
  transform: translateY(-50%);
  width: 28px; height: 28px; padding: 0; font-size: 12px;
  color: var(--color-neutral-600); }
.p-quote .input:placeholder-shown + .clear { display: none; }
/* It lights like every other control, and the box it belongs to darkens its
   edge with it — the pair is what says the ✕ acts on *this* box rather than on
   the row it is sitting in. */
.p-quote .clear:hover { background: var(--color-neutral-200);
  color: var(--color-text); }
.p-quote .field:has(> .clear:hover) .input {
  border-color: color-mix(in srgb, var(--color-text) 45%, transparent); }

/*
 * **The list runs the panel's width, not the field's.**
 *
 * It opens under its own box, so `.field` still places it vertically — but
 * horizontally it reaches back over the dot and out under the swap, because
 * the narrowest box on the screen was the one holding the longest strings.
 * The two insets are the gutters named above, so widening a box cannot leave
 * the list behind.
 *
 * **Capped and scrollable, and since v91 the cap matters.** The panel is a
 * scroll container now, so it clips this — the list can no longer spill over
 * the map the way it could when the panel had no overflow of its own. What
 * saves a long list on a short screen is that it scrolls *with* the panel:
 * it is positioned against a box inside the scrolled content, so the panel's
 * own bar reaches the rows the clip hides. Two nested scrollers is the cost of
 * not measuring the panel from script.
 */
.p-quote .suggestions {
  position: absolute; z-index: 20; top: calc(100% + var(--space-3));
  /* Out to the panel's edges, past its padding on both sides — the design
     draws the list as a region of the panel rather than as a box under a
     field. The two gutters put it level with the field; the panel's own
     padding takes it the rest of the way. */
  left: calc(-1 * (var(--lead-gutter) + var(--panel-pad)));
  right: calc(-1 * (var(--trail-gutter) + var(--panel-pad)));
  margin: 0; padding: 0; list-style: none;
  background: var(--color-bg);
  /* A rule where it meets the fields and a shadow below it, rather than a box
     around it. It is not a card sitting on the panel; it is the panel, from
     here down, given over to the list. */
  border: 0; border-top: 1px solid var(--color-divider);
  box-shadow: var(--shadow-lg);
  /* As tall as its contents, and no taller than the panel has room for.
     `--suggestions-room` is measured when the list opens — see
     `quote_form.html` — because only the browser knows how much of the panel
     is left below whichever of the two boxes is being typed in. One
     suggestion is one row high; ten fill what is left and scroll. */
  max-height: var(--suggestions-room, 15rem);
  overflow-y: auto;
}
.p-quote .suggestions:empty { display: none; }
/*
 * Two lines per row: the street, then where it is.
 *
 * The streets line up and read down the list; the towns underneath are what
 * tell «Северная ул, д 12» in one town from the same street in another. A city
 * or a district has no street to lead with and carries `.addr` alone.
 *
 * The side padding is the panel's, so the street starts on the same vertical
 * as the address boxes above it although the list itself runs wider.
 */
.p-quote .suggestions li { display: flex; flex-direction: column; gap: 2px;
  padding: 11px var(--space-6); cursor: pointer;
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent); }
.p-quote .suggestions .addr { font-size: 14px; }
.p-quote .suggestions .area { font-size: 12px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.p-quote .suggestions li:hover,
.p-quote .suggestions li[aria-selected="true"] { background: var(--color-neutral-200); }
/* Locked once a quote exists: the key was minted at render time, so a second
   submit would reuse it (§9.2). Starting over is a page load. */
.p-quote input[readonly] { opacity: .6; cursor: default; }

/*
 * The results area grows into whatever is left — and no further.
 *
 * `flex: 1` is what puts «Сохранить» at the bottom of the panel rather than
 * under the last route when the list is short. What went with the scrollpane
 * is `min-height: 0`, which let these three shrink below their contents: the
 * list then scrolled inside itself and the footer stayed pinned, so a panel
 * with six variants had a scrolling strip in the middle of it and «Рассчитать»
 * fixed above. Left at the default `auto` they refuse to shrink, the panel is
 * the thing that overflows, and the footer follows the last route down.
 */
/*
 * The answer fills the middle when it is short, so the sticky foot below sits
 * at the bottom of the panel rather than under the last route. `min-height` is
 * deliberately left at `auto`: these must be free to grow past the middle,
 * because the middle is the thing that scrolls.
 */
.p-quote #result { display: flex; flex-direction: column; flex: 1; }
.p-quote .routes { display: flex; flex-direction: column; flex: 1; }
.p-quote .routes > form { display: flex; flex-direction: column; flex: 1; }
.p-quote .routes-head { display: flex; justify-content: space-between;
  align-items: baseline; }
.p-quote .routes-head h6 { margin: 0; }
.p-quote .routes-head span { font-size: 11px; }

/*
 * The variants.
 *
 * A radio group, styled as rows. The native input stays in the accessibility
 * tree and keeps arrow-key navigation between the three; it is moved off-screen
 * rather than `display: none`, which would take it out of the tree and with it
 * the only thing announcing that these are three choices and one is chosen.
 */
.p-quote .variants { list-style: none; margin: var(--space-3) 0 0; padding: 0;
  border-top: 1px solid var(--color-divider); }
/*
 * **It states its own box, because it is a `<label>` that is not a field
 * label.** The rule up in the forms section styles `label` bare as well as
 * `.field > label`, which it has to: Django renders the password forms itself
 * and those labels carry no class of ours. It reached this row too, and left
 * three marks — a 5px bottom margin, so the rows did not touch and the
 * selected row's fill had a gap under it; a 12px base size; and 70% ink, which
 * the price inherited, so every figure in the list was rendered muted against
 * a design that draws it at full strength. Setting all three here is what makes
 * the row immune to it.
 */
.p-quote .variant { display: block; cursor: pointer;
  margin: 0; font-size: 14px; color: var(--color-text);
  padding: 13px var(--space-1) 13px var(--space-3);
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
  border-left: 3px solid transparent; }
.p-quote .variant input { position: absolute; opacity: 0; width: 0; height: 0; }
/* The tint alone. The design previews the selection with a grey copy of the
   left bar as well, and it reads as a second selected row rather than as a
   pointer resting somewhere — the bar is what says «this is the one», so a bar
   that appears under the cursor says it twice. */
.p-quote .variant:hover { background: var(--color-neutral-100); }
.p-quote .variant:has(input:checked) { border-left-color: var(--color-accent);
  background: var(--color-neutral-200); }
/* The focus ring goes on the row, since the input it belongs to is off-screen
   and a ring nobody can see is a keyboard user with no cursor. Ink, like every
   other ring — and here the reason is plain, because the row's own selected
   mark is the accent left bar and the two would otherwise be one colour
   saying two things. */
.p-quote .variant:has(input:focus-visible) {
  outline: 2px solid var(--color-neutral-800); outline-offset: -2px; }
/*
 * **Except for the one focus nobody asked for.**
 *
 * When the prices arrive the script moves focus to the chosen variant, so the
 * caret leaves the address box it is finished with and the arrows work from
 * where the work continues. That is script focus — and a browser counts script
 * focus as keyboard focus when the element it came from had a visible ring,
 * which a text input always does. So every calculation drew an accent ring
 * round the chosen row, on top of the accent left bar and the grey fill that
 * are what «this is the one» actually looks like: three marks where the design
 * has one, reported as a red outline where a red bar belongs.
 *
 * The same shape of bug as the map's red frame, and the same answer: a ring is
 * for somebody who navigated here. The script says it did the moving, and the
 * first key the dispatcher presses takes the class off again — so a keyboard
 * user arrowing between routes has the ring back before they need it.
 */
.p-quote .variants.focus-unasked .variant:has(input:focus-visible) { outline: none; }
.p-quote .variant .price { display: block; font-family: var(--font-heading);
  font-weight: var(--font-heading-weight); font-size: 21px; line-height: 1.1; }
.p-quote .variant .meta { display: flex; align-items: center; gap: var(--space-2);
  margin-top: 5px; font-size: 13px; }

/*
 * **The same three routes, drawn small.** The chips are the lowered sheet's
 * rendering of the list above — artboard `8g` — and they exist because a sheet
 * 30dvh tall has room for the prices and nothing else: no heading, no distance
 * tag, no addresses.
 *
 * **They are a second control over the same radio group, not a second radio
 * group.** The radios in `.variants` remain the only thing that holds which
 * route is chosen — a chip writes to them and reads back from them, and says so
 * with `aria-pressed`. Two radio groups with one name would have been two
 * answers to one question, and since the renderings swap mid-drag, the moment
 * they disagreed *dragging the sheet* would change the price.
 *
 * Hidden with `display: none` rather than moved off-screen, which is not a
 * detail: the inactive rendering must leave the tab order, or every route is
 * two tab stops and the keyboard walks through a copy nobody can see. A
 * `<button>` rather than a label for the same reason from the other side — when
 * the chips are what is showing, the rows are display-none and their radios
 * cannot be focused, so the chips have to be reachable in their own right.
 */
.route-chips { display: none; gap: var(--space-2);
  margin: 0; padding: 4px var(--space-6) 0; list-style: none; }
.route-chips li { flex: 1; min-width: 0; display: flex; }
/* Restating the three things a bare `label` inherits from the caption rule
   above — the 5px margin, the 12px size and 70% ink — for the same reason
   `.variant` restates them: a row built as a label is a row, not a caption, and
   a price drawn at 70% is a slightly different design rather than a broken
   one. */
.route-chip { display: flex; flex-direction: column; align-items: flex-start;
  gap: 2px; flex: 1; min-width: 0; min-height: 64px;
  margin-bottom: 0; padding: 9px 11px; font-size: inherit;
  color: var(--color-text); text-align: left; cursor: pointer;
  background: none; border: 1px solid var(--color-divider); }
.route-chip .price { font-family: var(--font-heading);
  font-weight: var(--font-heading-weight); font-size: 19px; line-height: 1.05; }
.route-chip .km { font-size: 12px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.route-chip:hover { background: var(--color-neutral-200);
  border-color: var(--color-neutral-600); }
/* Chosen: the fill and the edge together, which is how every other control here
   answers. Read from `aria-pressed` rather than from a class, so the state a
   screen reader is told and the state the eye is shown are one attribute and
   cannot drift. `cursor: default` because pressing the one already chosen does
   nothing — the row version says the same with its accent bar. */
.route-chip[aria-pressed="true"] { background: var(--color-neutral-200);
  border-color: var(--color-neutral-900); cursor: default; }

/* The grip belongs to the sheet and the sheet only exists below 680; every
   other width has a panel with edges and nothing to drag. */
.sheetgrip { display: none; }

/*
 * **«Сохранить» stays on screen, whatever the list is doing.**
 *
 * The design draws it as a foot of the panel, below the scrolling middle and
 * outside it. In the markup it cannot be: it is the submit button of the form
 * the variants are radios of, and the fragment htmx swaps in owns both. So it
 * is sticky instead — its own place is after the last route, and it stops at
 * the bottom of the middle rather than travelling past it.
 *
 * The negative margins are what let a foot inside a padded box draw a rule
 * across the whole panel; the padding puts its contents back where they were.
 * Its own background is what the rows pass behind rather than through.
 */
.p-quote .routes-foot { position: sticky; bottom: 0;
  display: flex; justify-content: space-between; align-items: flex-end;
  gap: var(--space-4); margin: auto calc(-1 * var(--space-6)) 0;
  padding: var(--space-4) var(--space-6) 18px;
  background: var(--color-bg);
  border-top: 1px solid var(--color-divider); }
.p-quote .routes-foot p { margin: 0; font-size: 11px; line-height: 1.5; }

/*
 * Tablet — 900 and below, where the design starts giving the map its room back.
 *
 * The panel is still beside the map and still floating; it is only narrower,
 * and the furniture standing on the map pulls in with it. `min(380px, 66%)` is
 * the design's own expression rather than a number picked here: a fixed 380
 * until two thirds of the screen is less than that, after which the map keeps a
 * third of the frame whatever happens.
 */
@media (max-width: 900px) {
  /* One number pulls in the panel's neighbours: the zoom buttons, the credit
     and the navigation are all furniture standing on the map, and they are
     inset by the same `--control-inset` at every width. The bar used to say it
     again as `right`/`top` of its own, which stopped being an inset at all the
     day it came into the flow — it became a 12px *shift*, left and down, off
     the corner it was meant to sit in. */
  .p-quote { --panel-width: min(380px, 66%);
    --control-inset: 12px; }
}

/*
 * Phone — 680 and below. **The routes are on a sheet over the map.**
 *
 * This block replaces a stacked column, and the note it replaces said the sheet
 * "needs drag handling to be worth having and is not built yet". That was true
 * and is now answered rather than argued away: the drag is written, in
 * `app.js`, on Pointer Events. What the column could not do is the reason it
 * had to be — selecting a variant drives the map, and in a column the map was
 * 55vh below the thing selecting it, so the one interaction this screen has was
 * built and unreachable. On a sheet both ends of it are on screen at once.
 *
 * **The quote screen keeps its frame at every width**, which is the one place
 * the application disagrees with itself on purpose. Everywhere else the
 * document scrolls below 900 so a phone keeps its address-bar collapse and
 * pull-to-refresh. Here a pull-to-refresh reloads the page and throws away a
 * priced draft held under an idempotency key, and there is no «further down» on
 * a map to want. The sheet is the thing that moves instead.
 *
 * **Three rests, two of them drawn.** The sheet arrives at `--sheet-rest` and
 * the drag snaps to 96dvh or 30dvh — artboards `8f` and `8g`. Nearest-point
 * snapping, so the arrival height is reachable again rather than being a state
 * you can only leave.
 *
 * **Height and content are two tracks and they do not change together.** The
 * height follows the finger and settles on release; the content swaps live,
 * mid-drag, and is biased hard toward the rows — `.shows-chips` holds only
 * across the bottom quarter of the travel. `app.js` owns both thresholds; this
 * file only says what each class means. Which is why the state classes are on
 * `<body>` rather than on the panel: the map furniture reads them, and so does
 * the navigation, which is outside `.scroll` entirely and is not the panel's
 * sibling or its descendant. One host that every part of the screen can see.
 */
@media (max-width: 680px) {
  /*
   * **The gutter narrows, and it is still one number.** The design sets the
   * phone's margin at 14px; 20 on a 390px screen is a tenth of the width given
   * to each edge. It is redeclared here rather than written into the rules that
   * want it, because the bar and the content under it agreeing is the whole
   * point of there being a token — a `padding: 10px 14px` on the row is how the
   * mark ends up six pixels off the column it heads.
   */
  :root { --page-gutter: 14px; --chrome-top: 10px; }

  /* The bar becomes a real bar: evenly padded, with a rule under it. At 390 the
     navigation and the first line of the page are close enough together that
     without one they read as a single block (`1g`). */
  .chrome { padding: var(--chrome-top) var(--page-gutter);
    border-bottom: 1px solid var(--color-divider); }

  .p-quote { --sheet-rest: 62dvh; --control-inset: 12px; }

  /*
   * **Before there is an answer there is no map** (`1g`). The calculator opens
   * as an ordinary page — the bar, and two boxes — and becomes the map with the
   * sheet over it when the routes arrive. A map with nothing drawn on it is a
   * picture of St Petersburg above a form, taking two thirds of a phone to say
   * nothing, and the addresses are the only thing being asked for.
   *
   * So this state undoes the frame rather than the frame undoing it: the sheet
   * below is the exception, and a page with no answer on it is the ordinary
   * case. `quote_detail` never lands here — it always has a route, so `app.js`
   * has always set the class by the time it matters.
   */
  .p-quote:not(.has-map) { height: auto; min-height: 100dvh; overflow: visible; }
  .p-quote:not(.has-map) .scroll { display: block; overflow: visible; }
  .p-quote:not(.has-map) .map-pane { display: none; }
  .p-quote:not(.has-map) .sheetgrip { display: none; }
  .p-quote:not(.has-map) .panel { position: static; width: auto;
    height: auto; max-height: none; overflow: visible;
    border: 0; box-shadow: none; transition: none; }
  /* The panel is the page here, so its padding is the page's gutter — and the
     bleed is told, rather than left to assume the panel's own. */
  .p-quote:not(.has-map) { --panel-pad: var(--page-gutter); }
  .p-quote:not(.has-map) .panel-body { display: block; overflow: visible;
    padding: var(--space-6) var(--panel-pad) 0; }
  /* The head keeps its box — the page's `<h1>` is in it, visually hidden — and
     gives up only the mark, which the bar is showing instead. */
  .p-quote:not(.has-map) .panel-head { padding: 0; }
  .p-quote:not(.has-map) .panel-head .brand { display: none; }
  .p-quote:not(.has-map) .panel-form { margin: 0; padding: 0 0 16px;
    border-bottom: 0; }
  /*
   * The bar goes back to being a bar: it has a height again, it sits on the
   * page's own ground rather than over tiles, and the mark comes into it. The
   * shadow goes with the map — over a white page it is a group floating above
   * nothing.
   */
  .p-quote:not(.has-map) .chrome { height: auto;
    padding: var(--chrome-top) var(--page-gutter);
    justify-content: flex-start; border-bottom: 1px solid var(--color-divider); }
  .p-quote:not(.has-map) .chrome > * { margin-top: 0; }
  /* …except the mark, which keeps the optical nudge every other bar gives it.
     The rule above is the floating bar's inset and has no business cancelling
     it: the mark's own artwork carries a rule under the word, so centring the
     image against a 44px control leaves the letters sitting two pixels low.
     Without this the logo stands 2px below where «История» draws it, which is
     a jump between two pages that are meant to have the same bar. */
  .p-quote:not(.has-map) .chrome .brand { display: inline-flex; }
  .p-quote:not(.has-map) .navgrp,
  .p-quote:not(.has-map) .account > summary { box-shadow: none; }

  /*
   * The panel stops being a column down the left and becomes a band across the
   * bottom. Every edge it had is restated: `top` released so `bottom` and
   * `height` decide it, `width` given up to `left`/`right`, and the rule that
   * separated it from the map moving from its right edge to its top one.
   */
  .p-quote .panel { top: auto; right: 0; bottom: 0; left: 0;
    width: auto; height: var(--sheet-rest); max-height: 96dvh;
    border-right: 0; border-top: 1px solid var(--color-divider); }
  /* The settle is the only thing animated, and `.sheet-dragging` below turns it
     off — a sheet that eases toward the finger instead of tracking it reads as
     lag rather than as weight. */
  .p-quote .panel { transition: height 180ms ease-out; }
  .p-quote.sheet-tall .panel { height: 96dvh; }
  .p-quote.sheet-short .panel { height: 30dvh; }
  /* Last, so it outranks both states at equal specificity: while the finger is
     down the height is whatever the script measured, and nothing eases. */
  .p-quote.sheet-dragging .panel { height: var(--sheet-drag);
    transition: none; }

  /*
   * The grip. `touch-action: none` is what makes this a drag at all — without
   * it the browser claims the gesture and scrolls the page underneath. It is on
   * the grip alone, so the sheet's own middle still scrolls with a finger: drag
   * at the top, scroll in the body, and the two never contend for one gesture.
   */
  .p-quote .sheetgrip { display: block; width: 100%; padding: 10px 0 6px;
    background: none; border: 0; cursor: grab; touch-action: none; }
  .p-quote .sheetgrip:active { cursor: grabbing; }
  .p-quote .sheetgrip span { display: block; width: 46px; height: 4px;
    margin: 0 auto; background: var(--color-neutral-400); }
  .p-quote .sheetgrip:hover span { background: var(--color-neutral-600); }

  /* The head's own top padding goes: the grip is above it now and is its own
     gap. */
  .p-quote .panel-head { padding-top: 0; }
  /* The bar goes with the sheet — the design draws no scrollbar here, and a
     10px track inside a 390px sheet is a tenth of a price column. */
  .p-quote .panel-body { scrollbar-width: none; }
  .p-quote .panel-body::-webkit-scrollbar { width: 0; height: 0; }

  /*
   * **What the sheet is showing, which is not the same question as how tall it
   * is.** Lowered it is three prices side by side and the one control; raised
   * it is the mark, the addresses and the full rows. Nothing is destroyed
   * either way — both renderings are in the page over one radio group, so the
   * swap is `display` and the chosen route survives it.
   */
  .p-quote.shows-chips .panel-head,
  .p-quote.shows-chips .panel-form,
  .p-quote.shows-chips .routes-head,
  .p-quote.shows-chips .variants,
  .p-quote.shows-chips .issued,
  /* The fixture-mode banner goes with the prose. It only exists with DEBUG on
     and fake providers configured, and the lowered sheet is 30dvh — leaving it
     in makes the collapsed state four lines of warning above one row of prices.
     It is one drag from being read, and the rows it qualifies are one drag from
     being shown. */
  /* `.panel-body > .skeleton` stood here and never matched — the skeleton is a
     child of `#calculating`, not of the region — and it is dropped rather than
     repointed, because below 680 the skeleton is hidden by width already.

     **`#calculating` is deliberately not hidden with the rest.** A calculation
     cannot be *started* from a lowered sheet: the address boxes are among the
     things hidden two rules up. It can be dragged into one, by lowering the
     sheet while a request is in flight — and there `#result` is hidden too, so
     the chips go with it. The sheet would hold a grip and nothing else. The
     spinner is what is left to say, and it is the right thing to be saying. */
  .p-quote.shows-chips .banner { display: none; }
  .p-quote.shows-chips .route-chips { display: flex; }
  /* The middle stops scrolling when there is nothing in it to scroll; without
     this the chips sit against a region still claiming the panel's height. */
  .p-quote.shows-chips .panel-body { flex: none; overflow: visible;
    padding-bottom: 0; }

  /*
   * The map's furniture stands above the sheet, and follows it exactly: `app.js`
   * mirrors the panel's measured height into `--sheet-h` from a ResizeObserver,
   * so this tracks the drag and the settle without either one being described
   * twice. The properties cross into the component; no pixel does (§4.1).
   *
   * Raised, the zoom buttons go. At 96dvh there is no strip of map to put them
   * in, and a control drawn over the sheet it is hidden behind is worse than an
   * absent one. The credit goes with them for the same reason and comes back
   * the moment any map does.
   */
  .p-quote {
    --control-inset-bottom: calc(var(--sheet-h, 62dvh) + 14px);
    --control-zoom-top: calc(100% - var(--sheet-h, 62dvh) - 52px);
    --control-zoom-shift: -100%; }
  .p-quote.sheet-tall { --control-display: none; }
  /*
   * Raised, the navigation goes with them. The sheet is 96dvh and the bar would
   * be a floating group over a four-point strip of map — and the mark it sits
   * beside is in the sheet's own head at that height, so the corner would hold
   * a second, smaller copy of the same object. Artboard `8f` draws no bar at
   * all; `8g` is where it comes back.
   */
  .p-quote.sheet-tall .chrome { display: none; }

  /* Thumbs, not cursors. */
  .p-quote .field .input { min-height: 44px; padding-right: 42px; }
  .p-quote .clear { width: 38px; height: 38px; }
  /* Stated as the token the bleed reads, not as a width of its own — the
     suggestion list has to clear whatever this is. */
  .p-quote { --swap-size: 44px; }
  .p-quote #swap { width: var(--swap-size); height: var(--swap-size); }
}

/*
 * The small phone — 440 and below.
 *
 * The navigation stops being a group in the corner and spans the frame, because
 * two Russian words and an account button no longer leave a corner to sit in.
 * The sheet gives up six points of height to it.
 */
@media (max-width: 440px) {
  .p-quote { --sheet-rest: 56dvh; }
  /* The bar is a zero-height flex item rather than an absolutely positioned
     one, so it spreads with its own padding and `justify-content` — there is no
     `left`/`right` here to set, and the vertical inset stays on the children
     for the reason given where they get it. */
  .p-quote .chrome { justify-content: space-between; }
}

/*
 * **While the routes are being fetched.**
 *
 * Hidden until htmx puts `.htmx-request` on the form, which it does for the
 * life of the request and takes off however the request ends — so this cannot
 * be left showing by a failure, an abort, or an `hx-sync` replacement the way
 * a script-toggled class can.
 *
 * **And `.is-slow`, which is a timer, because a loading state that flashes is
 * worse than none.** Against the fixture providers a calculation answers in
 * about 57ms; the bar travels 5% of one cycle in that time, so it appeared and
 * vanished as a static red stripe — reported, correctly, as a stripe that does
 * not move. Under `QUOTE_PROGRESS_AFTER_MS` nothing is shown at all and the
 * answer simply replaces the one before it, which is right for a wait nobody
 * perceived.
 *
 * **Both classes, and that ordering is the point.** The script only ever adds
 * `.is-slow`; htmx alone decides `.htmx-request`. So a timer that somehow
 * outlived its request cannot leave this showing — the selector needs the
 * class htmx removes, whatever happened.
 *
 * The previous answer goes while it shows. Prices from the last pair sitting
 * under a bar that says a new pair is being priced are two claims about the
 * same panel, and the older one looks current.
 *
 * **Matched through the panel, not as a sibling.** These were `~ #calculating`
 * and `~ #result`, which was true while the form and the answer were children
 * of one box — and stopped being true the day the panel became a fixed head
 * and a scrolling middle, because the form went into the head and the answer
 * into the middle. A sibling combinator that can no longer match fails the way
 * this one did: silently, and with no loading state on any calculation at all.
 * `:has()` on their nearest common ancestor says what is actually meant —
 * «while this panel's form is in flight» — and survives the next time the
 * panel is rearranged.
 */
.p-quote #calculating { display: none; }
.p-quote .panel:has(.quote-form.htmx-request.is-slow) #calculating { display: block; }
.p-quote .panel:has(.quote-form.htmx-request.is-slow) #result { display: none; }

/*
 * **A refusal from the last press goes the moment the next one starts.**
 *
 * «Сервис маршрутов сейчас недоступен» sat above «Рассчитать» until the new
 * answer arrived and swapped it — so a fresh calculation ran under a sentence
 * saying the last one had failed, and the reader had no way to tell whether it
 * was about the press they had just made or the one before it. A message that
 * describes a request which is no longer the current request is worse than no
 * message: it is the same kind of claim as a price left standing over addresses
 * that have since been edited, which is why `_routes.html` clears this slot on
 * the way in.
 *
 * **Without `.is-slow`, unlike the rules above.** Those wait 250ms because a
 * loading state that flashes is worse than none — but this is not a state being
 * *shown*, it is a stale one being taken away, and there is no reason to leave
 * it up for a quarter of a second first. Whatever replaces it, including
 * another refusal, arrives with the response.
 *
 * `.htmx-request` rather than a script, for the reason the rest of this block
 * gives: htmx takes the class off however the request ends, so a failure, an
 * abort or an `hx-sync` replacement cannot leave the slot hidden with something
 * in it.
 */
.p-quote .panel:has(.quote-form.htmx-request) #form-message { display: none; }

/*
 * **One device per width, and each says something the other cannot.**
 *
 * There was a third — a 2px rule under the boxes with a third of it travelling,
 * drawn on the desktop and hidden on the phone. It is gone at both. An
 * indeterminate loop wearing `role="progressbar"` is a claim the shape cannot
 * keep: a stripe that crosses the track and returns to the left reads as a
 * request that restarted rather than one still running, and there was nothing
 * for it to be a percentage of. It also stood beside the skeleton on the one
 * width that had it, which is two marks for one fact.
 *
 * What is left divides on whether there is room to draw the answer:
 *
 *   - the desktop draws the shape of it — three priced rows, pulsing;
 *   - the phone says it in words, because a sheet 30dvh tall has nowhere to put
 *     three rows, and a skeleton drawn under the fold is drawn for nobody.
 */

/* The variants, drawn as the shape they will be. Staggered, because three rows
   pulsing in unison read as one object blinking rather than as a list. */
.p-quote .skeleton { margin-top: var(--space-6); }
.p-quote .skeleton .variants { border-top: 1px solid var(--color-divider); }
.p-quote .skeleton li { padding: 13px var(--space-1) 13px var(--space-3);
  border-bottom: 1px solid color-mix(in srgb, var(--color-text) 8%, transparent);
  animation: pulse 1.4s ease-in-out infinite; }
.p-quote .skeleton li:nth-child(2) { animation-delay: .2s; }
.p-quote .skeleton li:nth-child(3) { animation-delay: .4s; }
.p-quote .skeleton .price { display: block; width: 120px; height: 21px;
  background: var(--color-neutral-400); }
.p-quote .skeleton li:nth-child(2) .price { width: 104px; }
.p-quote .skeleton li:nth-child(3) .price { width: 112px; }
.p-quote .skeleton .meta { display: block; width: 64px; height: 12px;
  margin-top: 7px; background: var(--color-neutral-300); }
.p-quote .skeleton li:nth-child(2) .meta { width: 78px; }
.p-quote .skeleton li:nth-child(3) .meta { width: 56px; }
@keyframes pulse { 0%, 100% { opacity: .35; } 50% { opacity: .7; } }

/* The phone's version, and the only one it gets — see the breakpoint below. */
.p-quote .spinner { display: none; align-items: center; justify-content: center;
  gap: var(--space-3); margin: 0; padding: 34px 0 30px; font-size: 13px;
  color: color-mix(in srgb, var(--color-text) 55%, transparent); }
.p-quote .spinner .ring { width: 22px; height: 22px; flex: none; border-radius: 50%;
  border: 2px solid var(--color-neutral-300); border-top-color: var(--color-accent);
  animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/*
 * The phone shows the spinner and nothing else (`7b`): the routes live behind
 * a sheet there, so a skeleton of them would be drawn where nobody is looking.
 *
 * **Here rather than in the breakpoint above**, which is where it was first
 * written and where it did nothing. These rules are declared after that block,
 * so at equal specificity the base `display: none` on `.spinner` won on source
 * order and the phone got no indicator at all. A media query that overrides a
 * rule has to come after it.
 */
@media (max-width: 680px) {
  .p-quote .skeleton { display: none; }
  .p-quote .spinner { display: flex; }
}

/*
 * **`prefers-reduced-motion` is deliberately not honoured here, and this is the
 * comment that says so** — an absence, in a file that answers `forced-colors`,
 * reads as something nobody thought about.
 *
 * There was a block that switched the animations off and left the elements
 * standing, written on the rule that the states must stay distinguishable so
 * nothing is hidden. Applied to the bar, that argument produced a red stripe
 * frozen a third of the way along its track — which does not read as «motion is
 * off» but as «this request has hung». The fallback was worse, for the reader
 * who asked for it, than the motion it replaced, and it was reported as the
 * loading state being broken.
 *
 * What is left is two short-lived indicators on one screen rather than motion
 * across an application, and the preference arrives set far more often by the
 * environment than by a decision: WSL, remote desktop and Windows' own
 * «Animation effects» switch all report it.
 *
 * The cost is real and belongs here rather than implied: a reader who does want
 * less motion gets a pulsing skeleton or a turning ring anyway. Restoring the
 * behaviour is one block — but the thing to restore is not the old one. It is a
 * state that does not look stuck, which for the skeleton is the flat opacity it
 * already had, and for the ring is the sentence without it.
 */

/*
 * The issued quote, and the two messages that can appear in its place.
 *
 * All three are swapped into `#result`, which is a flex column filling the
 * panel — so each needs to say it does not stretch, or a refusal one line long
 * is drawn the height of the screen.
 */
/*
 * The issued quote as the calculator shows it: one block in the scrolling
 * middle, under the form that produced it. On `quote_detail.html` there is no
 * `.issued` at all — the same three pieces are the panel's three regions — so
 * everything below is written against the pieces rather than against it.
 */
.p-quote .issued { flex: none; margin-top: var(--space-6); }
/* The gaps between the three, wherever they are. On the quote page the head
   and the foot have the panel's own padding and the middle has its own top
   margin, so only the price needs spacing from what is above it. */
.p-quote .price-card { margin-top: var(--space-6); }
.p-quote .issued-head { display: flex; justify-content: space-between;
  align-items: baseline; gap: var(--space-3); }
.p-quote .issued-head h6 { margin: 0; }
.p-quote .issued-head span { font-size: 11px; }
.p-quote .issued-route { margin-top: var(--space-4); }
.p-quote .issued-route p { display: flex; gap: var(--space-3);
  margin: 0 0 var(--space-3); font-size: 14px; }
/* The dot sits on the first line of a wrapped address rather than centred
   against the whole block, which is why it has a top margin instead of
   `align-items: center`. */
.p-quote .issued-route .dot { margin: 6px 0 0; }

/* The one solid object on the panel. `.blueprint`'s registration marks are
   drawn outside the box, so it needs room around it as well as within. */
.p-quote .price-card { padding: var(--space-4); gap: 0; }
.p-quote .price-card .total { font-family: var(--font-heading);
  font-weight: var(--font-heading-weight); font-size: 38px; line-height: 1; }
.p-quote .price-card dl { display: grid; grid-template-columns: 1fr auto;
  gap: 7px var(--space-4); margin: var(--space-4) 0 0; font-size: 13px; }
.p-quote .price-card dt, .p-quote .price-card dd { margin: 0; }
.p-quote .price-card dd { text-align: right; }
.p-quote .price-card .hint { margin-top: var(--space-2); }
.p-quote .issued .btn-block { margin-top: var(--space-6); }
/* Which quote this is, at the top of the scroll region, where the calculator
   has its addresses. Both sit in `.panel-form`, under the same rule. */
.p-quote .panel-form .issued-head { margin-top: 0; padding-top: var(--space-4); }

/*
 * The §6 confirmation, and the refusal that can stand in its place.
 *
 * Both arrive in `#result` by htmx swap, so both need to say they do not
 * stretch — see the rule above — and both need the space that used to belong
 * to the dialog they replaced.
 *
 * What was resolved is a quiet pair of lines rather than a definition list.
 * The list's «Откуда»/«Куда» headings restated the dots on the boxes two
 * inches above, and «вы ввели: …» restated the text still sitting in them;
 * what is new on this screen is what the geocoder made of it, so that is what
 * is left.
 */
.p-quote .form-error, .p-quote .notice-box { flex: none; margin-top: var(--space-6); }
.p-quote .confirm .resolved { display: flex; flex-direction: column;
  margin: var(--space-3) 0 0;
  color: color-mix(in srgb, var(--color-text) 65%, transparent); }
/* A line of text, which is what the design draws, and a button, which is what
   it has to be. `.btn-ghost` brings the accent; the rest is undone here. */
.p-quote .confirm form { margin-top: var(--space-3); }
.p-quote .confirm .btn-ghost { padding: 0; border: 0; font-family: var(--font-body);
  font-weight: 400; font-size: 13px; color: var(--color-accent-700); }
.p-quote .confirm .btn-ghost:hover { background: none; text-decoration: underline;
  text-underline-offset: 3px; }

/* --- the history list ---------------------------------------------------- */

/* Six columns, two of them addresses, and no amount of a 780px column makes
   «Уткина Заводь, Октябрьская набережная, 104» and its destination share a
   row. The design draws this page the full width of the frame, which is also
   what lines the table's first column up with the mark above it. */
.p-history { --page-width: none; }

.p-history .history-head { margin-bottom: var(--space-6); }
.p-history .history-head h1 { margin: 0; }
.p-history .history-head span { font-size: 13px; }

/* The outer cells lose their outer padding, so the table's *content* reaches
   the column edges the way the heading and the hint above it do. The table was
   already full width and the rules under each row already ran edge to edge —
   it was the first column's text sitting a space in, and the price's right
   edge stopping a space short, that made the list read as narrower than its
   page. */
.p-history .table th:first-child, .p-history .table td:first-child { padding-left: 0; }
.p-history .table th:last-child, .p-history .table td:last-child { padding-right: 0; }
/* The heading too, not only the cell. Styling `td` alone leaves the column
   label hanging over the left edge of figures that sit at the right one,
   which reads as a misalignment because it is one. */
.p-history .num { text-align: right; white-space: nowrap; }
/* The two addresses take every pixel the other four do not need, and split it
   evenly. Left unsized, a table at width:100% shares the spare width across
   all six in proportion to their content — padding four columns of
   fixed-length figures that were already wide enough, and squeezing the two
   whose content varies. `width: 50%` on each absorbs the slack instead and
   keeps the pair symmetrical, which matters here: «Откуда» and «Куда» are read
   against each other, and a wider origin column reads as the more important
   end of a route that has no more important end. */
.p-history .from, .p-history .to { width: 50%; }
/* On the date itself rather than the cell, so «истекла» can drop below it
   instead of widening the column to hold both on one line. */
.p-history .when { white-space: nowrap; }
/*
 * **The row is the link, not one cell of it.**
 *
 * A dispatcher aims at a line, not at a column. The link still *is* one anchor
 * in one cell — a real link, which Tab reaches and the browser can open in a
 * new tab — and a pseudo-element stretches it over the row. Twelve anchors per
 * row, or a click handler on `<tr>`, would each cost what this keeps.
 *
 * It hangs on the reference now rather than on the addresses. Two columns
 * apiece is what the split cost the old «Откуда → Куда» cell: a link may not
 * span two of them, and picking one would make half a route clickable. The
 * reference is the name of the thing being opened, which is what the design
 * draws as the link and what is read back over the telephone anyway.
 *
 * `position: relative` on `<tr>` is what makes the row the containing block.
 * It is the one part of this that used to be unreliable and now is not; if a
 * row ever stops being clickable outside the reference cell, that declaration
 * is where to look.
 *
 * No underline on hover — the whole row lights instead, which is what the
 * design draws and what a row-sized target should do. Every cell keeps the
 * page's text colour; the heading row is the only muted thing here.
 */
.p-history .table tbody tr { position: relative; cursor: pointer; }
.p-history .ref a:link, .p-history .ref a:visited { color: var(--color-text);
  text-decoration: none; }
.p-history .ref a::after { content: ""; position: absolute; inset: 0; }
/* Keyboard focus lands on the anchor, which is a few words inside one cell —
   so the ring is drawn on the row it actually opens. */
.p-history .table tbody tr:has(a:focus-visible) {
  outline: 2px solid var(--color-neutral-800); outline-offset: -2px; }
.p-history .pager { display: flex; gap: var(--space-6); align-items: baseline;
  margin-top: var(--space-6); font-size: 14px; }
.p-history .footnote { margin-top: var(--space-6); font-size: 12px; }

/*
 * The table sheds columns before it stops being a table.
 *
 * The design drops «Создана» at 900, because the date is the widest thing in
 * the row that is not the thing you came for. It comes back in the list below,
 * where there is a line for it: what is dropped here is a *column*, not a fact.
 *
 * **And 680 is where the columns stop, not where another one goes.** The
 * container queries drop «км» there too, and that was read as a third tier —
 * a four-column table between 441 and 680 with the list below it. Four columns
 * is two long Russian addresses in half a screen each, which is the same «does
 * not survive by any amount of wrapping» the six had, and nobody was ever
 * going to see the list: Chrome will not make a window narrower than about
 * 500px, so every narrow-window reader got the cramped table and none of them
 * got the design. 680 is where the rest of the application becomes phone-shaped
 * — it is where the panel becomes a sheet — and the history agrees with it.
 */
@media (max-width: 900px) {
  .p-history .table { font-size: 13px; }
  .p-history .table th { font-size: 10px; }
  .p-history .table th, .p-history .table td { padding-inline: 8px; }
  .p-history .table th:first-child, .p-history .table td:first-child {
    display: none; }
  /* The reference is the left edge now, so it takes the edge's padding. */
  .p-history .table th:nth-child(2), .p-history .table td:nth-child(2) {
    padding-left: 0; }
}
@media (max-width: 680px) {
  /*
   * **The page is its list.** The design gives the phone the window line and
   * the rows, and nothing else (`1g`): no title, no count, no note under the
   * list. A 25px heading saying «История котировок» above a list of quotes on
   * a 390px screen names what the reader is plainly looking at, and spends a
   * fifth of the first screen doing it.
   *
   * The `<h1>` is hidden rather than dropped, because a page with no heading
   * is a page a screen reader cannot announce — the quote screen already
   * carries its own this way. The note goes because «За последние 30 дней»
   * above the list is the same fact in fewer words, and two statements of one
   * window is one more than a phone has room for.
   *
   * The window line itself stays whole, count and all: a list is scrolled, so
   * the count is the size of the thing being scrolled rather than of the part
   * in front of you, which is worth most on the screen that shows the least of
   * it at a time.
   */
  .p-history .page { margin: var(--page-gutter) 0 22px; }
  .p-history .history-head { margin-bottom: 10px; }
  .p-history .history-head h1 {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
  .p-history .history-head span { font-size: 12px; }
  .p-history .footnote { display: none; }

  .p-history .table thead { display: none; }
  .p-history .table, .p-history .table tbody,
  .p-history .table td { display: block; width: auto; }
  /*
   * **The row is a flex line that wraps, not six stacked blocks.**
   *
   * The artboard puts the date and the reference on one line with the price
   * opposite them, and the two are aligned on their *baselines* — 11px of
   * muted metadata and a 19px price sitting on the same line, which is what
   * makes the pair read as one heading for the row rather than as two things
   * that happen to be level. Stacking the cells and hanging the price off the
   * corner with `position: absolute` got it approximately there and could not
   * get it exactly there: an absolute box is aligned by its edges and a
   * baseline is not an edge.
   *
   * So the row lays its cells out in the order the design reads them, and the
   * three that take a whole line say so with `width: 100%` and wrap.
   */
  .p-history .table tr { display: flex; flex-wrap: wrap; align-items: baseline;
    padding: 13px 2px; border-top: 1px solid var(--color-divider); }
  /* The design closes the list: the rules are the list's own edges, so the one
     under the last row is as much a part of it as the one above the first. On
     the table the last row needs none — the page ends there, and a rule across
     the bottom of a table reads as a total. */
  .p-history .table tbody tr:last-child {
    border-bottom: 1px solid var(--color-divider); }
  .p-history .table td { padding: 0; border: 0; }

  /*
   * The date and the reference, one quiet line. The clock goes: in a list the
   * date separates one quote from the next and the minute separates nothing.
   *
   * **`display` has to be restated.** The two tiers above this one *drop* these
   * columns — «Создана» at 900 and «км» at 680 — and they drop them with the
   * same specificity this block has. What is dropped there is a column; here
   * there are no columns, and the list wants both facts back, so each says so
   * itself rather than relying on being narrower.
   */
  .p-history .table td:first-child { display: block; order: 1; }
  .p-history .table .ref { order: 2; }
  .p-history .table td:first-child, .p-history .table .ref {
    font-size: 11px;
    color: color-mix(in srgb, var(--color-text) 55%, transparent); }
  .p-history .table td:first-child::after { content: "\00a0·\00a0"; }
  .p-history .when .at { display: none; }
  .p-history .table .ref a:link, .p-history .table .ref a:visited {
    color: inherit; }

  /* The price, opposite them and on their baseline — it is what the row is
     *for*, and at the bottom of a four-line block it read as the last piece of
     metadata rather than the first thing to look at. */
  .p-history .table td:last-child { order: 3; margin-left: auto;
    font-family: var(--font-heading);
    font-weight: var(--font-heading-weight); font-size: 19px; line-height: 1.1;
    text-align: right; color: var(--color-text); }
  .p-history .table td:last-child::after { content: "\00a0₽"; }

  /*
   * Which end is which, said with the marks the map and the panel already use
   * rather than with an arrow. The dots are drawn rather than typed so a screen
   * reader is handed the address and not «стрелка вправо» in front of it, and
   * they read the same two custom properties the pins do — one fact about
   * origin and destination, stated once.
   */
  .p-history .table .from { order: 4; margin-top: 5px; }
  .p-history .table .to { order: 5; margin-top: 2px; }
  .p-history .table .from, .p-history .table .to {
    width: 100%; display: flex; gap: 8px;
    font-size: 13px; line-height: 1.35; }
  .p-history .table .from::before, .p-history .table .to::before {
    content: ""; width: 8px; height: 8px; margin-top: 4px; flex: none;
    border-radius: 50%; }
  .p-history .table .from::before { background: var(--pin-origin); }
  .p-history .table .to::before { background: var(--pin-destination); }

  /* The distance, last and quiet: it qualifies the price above it. */
  .p-history .table td:nth-child(5) { display: block; order: 6;
    width: 100%; margin-top: 2px;
    font-size: 12px; text-align: left;
    color: color-mix(in srgb, var(--color-text) 55%, transparent); }
  .p-history .table td:nth-child(5)::after { content: "\00a0км"; }
  .p-history .when { white-space: normal; }
}

/*
 * Phone: the table becomes a list.
 *
 * Six columns of Russian addresses and figures do not survive 390px by any
 * amount of wrapping — the two address columns alone are two long names. Each
 * row is turned into a block instead, with the headings dropped and the labels
 * carried by position, by unit and by the two marks the map already uses.
 *
 * `display: block` on the table parts rather than a second markup path, so the
 * page has one table and one leak-guarded template rather than two.
 *
 * Both columns the tiers above dropped are restated here — this block is
 * narrower than neither of them and comes after both, so the list decides for
 * itself what it shows.
 */

/* --- a service document -------------------------------------------------- */

/*
 * The agreement and the privacy policy (`9a`).
 *
 * **A reading column, not an interface one.** 680px and 14px over 1.6 is a
 * measure for prose; the 780 the rest of the application reads at is a measure
 * for a form with a table under it. A policy is read once, carefully, by
 * somebody deciding whether to agree to it.
 *
 * Three facts before the text: what kind of thing this is, what it is called,
 * and which revision you are reading. The third is the one that matters when a
 * document has been reissued, which is why it is above the body rather than
 * initialled at the bottom.
 */
/* 680 is the *column*, which is what the design states — and `--page-width`
   is the box the gutter is cut out of, so it carries the gutter on top. A
   reading measure written as 680 here would have been 640 on the page. */
.p-doc { --page-width: calc(680px + 2 * var(--page-gutter)); }
.p-doc .doc-kicker { display: block; font-size: 11px; letter-spacing: 0.08em;
  text-transform: uppercase; }
/* The design draws this at `h3`, which is the size a document title wants; the
   element is `h1`, which is what a document title *is*. */
.p-doc h1 { font-size: 25px; margin: 8px 0 6px; }
.p-doc .doc-revision { font-size: 13px; margin: 0 0 28px; }

/*
 * The body: numbered sections, separated by space rather than by rules.
 *
 * A rule between every section of a ten-section policy is nine horizontal
 * lines down a page that is already a column of text — the gap says the same
 * thing and says it more quietly. The heading carries its own number, because
 * an `<ol>` would number them for us and then nobody could cite «§4» when the
 * fourth section moves.
 */
.p-doc .doc-body { display: flex; flex-direction: column; gap: 22px;
  font-size: 14px; line-height: 1.6; }
.p-doc .doc-body h2 { font-family: var(--font-heading);
  font-weight: var(--font-heading-weight); font-size: 13px; line-height: 1.12;
  letter-spacing: 0.08em; text-transform: uppercase; margin: 0 0 var(--space-2); }
.p-doc .doc-body p { margin: 0 0 10px; }
.p-doc .doc-body p:last-child { margin: 0; }
/* `padding-left` rather than a margin, so the markers sit inside the column
   rather than hanging into the gutter the title starts at. */
.p-doc .doc-body ul { display: flex; flex-direction: column; gap: 6px;
  margin: 0; padding-left: 20px; }

/* --- the signed-out pages ------------------------------------------------ */

/*
 * **One card, in the middle of the screen**, on every page reachable without
 * an account: login, the invite, the confirmation wait, the four
 * password-reset steps.
 *
 * These pages have no navigation and nothing else on them, so a column
 * starting under the top edge read as a page whose rest had failed to load.
 * Centred both ways it reads as the one thing being asked for.
 *
 * The centring is a flex column on the body with `flex: 1` on `.page`, rather
 * than a fixed-height box with `place-items: center`: the height here is a
 * *minimum*, and a card taller than the window — the invite form, with two
 * consents and a complaint under three fields — has to push the page down and
 * scroll rather than be centred past the top edge, where the beginning of it
 * would be unreachable. `padding-block` is what keeps room above and below it
 * when that happens; a margin on the card would collapse.
 *
 * 428px is the design's 380 plus 24 either side. The card is a `.blueprint`
 * object like the price card — square, hairline, registration marks — and the
 * marks stand 6px outside its border, so it cannot sit against the gutter.
 *
 * `.card` is a flex column, which is what the mark has to be told about: with
 * `align-items: stretch` the default, a 187×49 logo at `height: 26px; width:
 * auto` is stretched across the card rather than drawn at its own ratio.
 */
.p-auth { --page-width: 428px; }
/* The region is what has a height to centre in, and it has to be at least a
   screen tall in both regimes: above 900px it is already exactly that, and
   below it the shell stops being a frame and this is the only thing left
   saying how tall a page with one card on it should be. */
.p-auth .scroll { display: flex; flex-direction: column;
  min-height: 100vh; min-height: 100dvh; }
/*
 * Centred, and the one page that is: there is no header row here to line up
 * with, and a card floating in the middle of the screen is the whole layout
 * rather than a column of content inside one.
 *
 * **`align-self`, not `margin-inline: auto`.** An auto inline margin on a flex
 * item switches off the cross-axis stretch, so the page stopped being 428px
 * wide and became as wide as whatever was in it — and `max-width` only caps a
 * width, it does not make one. Nothing looked wrong for as long as every
 * card's longest line happened to be wider than the others; deleting one
 * sentence from the sign-in card took that page to 295px while its neighbours
 * stayed at 428. The card below states its own width now, so this decides only
 * where the column sits, but a page that is silently sized by its contents is
 * worth not leaving in place.
 */
.p-auth .page { flex: 1; align-self: center; width: 100%;
  display: flex; flex-direction: column;
  justify-content: center; margin-block: 0;
  padding: var(--space-8) var(--space-6); }
/* **380px, because the design says 380px** — on all four of these cards, and
   the same 380 whichever of them you are looking at. It was left to the
   content, which is how two of them came to differ: a card is an object with
   a size, not a box that happens to be as wide as its longest sentence.
   `max-width` keeps it inside a phone, where 380 plus the gutters is more
   than there is. */
.p-auth .auth { flex: none; align-self: center;
  width: 380px; max-width: 100%;
  gap: 0; padding: 32px 28px 36px;
  background: var(--color-bg); }
.p-auth .auth-brand { height: 26px; width: auto;
  flex: none; align-self: flex-start; margin-bottom: var(--space-6); }
.p-auth h1 { margin: 0 0 var(--space-6); }
.p-auth form { display: flex; flex-direction: column; gap: var(--space-3); }
.p-auth .btn-block { height: 40px; margin-top: var(--space-1); }
.p-auth p { margin: 0 0 var(--space-3); font-size: 14px; }
.p-auth .hint, .p-auth .text-muted { font-size: 12px; }
/*
 * **What this form is for, before the fields.** 13px and full ink, not the
 * 12px muted `.hint` the fields use for their own asides: a card with two
 * sentences and one box is mostly that sentence, and setting it back as though
 * it were a footnote leaves the page looking like it is explaining something
 * optional. Under the heading, which tightens to match — a card with a lede
 * has less room to spare above it than one that goes straight to the fields.
 */
.p-auth .auth-lede { font-size: 13px; line-height: 1.5; margin: 0 0 18px; }
.p-auth h1:has(+ .auth-lede) { margin-bottom: var(--space-2); }
/*
 * **The way out is a row of the form, not a line after it.**
 *
 * «Забыли пароль?» and «Вернуться на страницу входа» are each the one thing to
 * do when the form in front of you is not the one you needed, so they belong
 * in the same column as its fields and its button, at the end — where the eye
 * already is when the button has not worked. They were a paragraph below the
 * form, which reads as a footnote about it.
 *
 * `accent-700` rather than the accent: a link the size of a caption sitting
 * under a solid accent button is the second red thing in an inch, and the
 * deeper step separates them without making it grey.
 */
.p-auth .auth-aside { align-self: flex-start; font-size: 13px; }
.p-auth .auth-aside:link, .p-auth .auth-aside:visited {
  color: var(--color-accent-700); }

/* The two documents, under a rule. Quiet, because nobody arrives here to read
   them — but present on every signed-out page, because the invite page is
   where somebody is asked to agree to them. */
.p-auth .auth-foot { display: flex; gap: var(--space-6);
  margin-top: var(--space-8); padding-top: var(--space-4);
  border-top: 1px solid var(--color-divider); font-size: 12px; }
.p-auth .auth-foot a:link, .p-auth .auth-foot a:visited {
  color: var(--color-neutral-700); }

/*
 * The consents (§10.4).
 *
 * The box and its sentence on one line, the box not shrinking — a checkbox
 * that flexes is a checkbox that becomes a sliver next to two lines of text.
 * The whole thing is a `<label>`, so the sentence is part of the target.
 */
.p-auth .consent { display: flex; gap: 9px; align-items: flex-start;
  font-size: 12.5px; line-height: 1.4; cursor: pointer; margin: 0;
  color: var(--color-text); }
.p-auth .consent input { flex: none; width: 16px; height: 16px; margin: 1px 0 0; }
/* The two documents, named inside the sentence that agrees to them, are ink
   rather than accent. Everything else red on these pages is something to press
   — «Войти», «Сохранить и войти» — and a red phrase in the middle of a consent
   reads as the thing being asked for rather than as a reference. The underline
   is what says it is a link; the design leaves the colour alone. */
.p-auth .consent a:link, .p-auth .consent a:visited { color: var(--color-text); }

/*
 * The confirmation wait: one thing being waited on, so it is grouped — but
 * under a rule rather than in a frame of its own. It was a `.blueprint` card,
 * which was right while the page around it was a plain column; with the page
 * itself now a framed object, a frame inside it read as two things to attend
 * to where there is one.
 */
.p-auth .resend { display: flex; flex-direction: column;
  align-items: center; text-align: center;
  gap: var(--space-2); margin-top: 18px; padding-top: 18px;
  border-top: 1px solid var(--color-divider); }
.p-auth .resend p { margin: 0; font-size: 12px; }

/* --- a field, anywhere --------------------------------------------------- */

/*
 * The complaint sits under the box it is about, in the accent's deep step.
 *
 * Not the accent itself: that pair is tuned to 3:1 against the ground, which
 * carries chrome and large text and not a line of 12px copy. The icon is the
 * same Lucide set at the same 1.5 stroke as everything else.
 */
.field-error { display: flex; gap: 6px; align-items: flex-start;
  margin: 6px 0 0; font-size: 12px; color: var(--color-accent-800); }
.field-error svg { flex: none; margin-top: 1px; }
/*
 * The same mark, one step up, for a refusal about the request rather than a
 * complaint about one box. Two rules and not one variant class, because the
 * design sizes them apart on purpose: a caption under a field is 12px and sits
 * tight to it, and this is 13px with a line height, because it is a sentence
 * somebody has to read rather than a label they have to glance at.
 *
 * No box and no heading — the design's `5b`, chosen over `5a`, which put the
 * same sentence under «Не получилось». The colour and the circle already say
 * «refused»; the heading only said it again, in a narrow column.
 */
.form-error { display: flex; gap: 7px; align-items: flex-start;
  margin: 0; font-size: 13px; line-height: 1.45; color: var(--color-accent-800); }
.form-error svg { flex: none; margin-top: 2px; }

/*
 * A caution, where `.form-error` is a refusal — the design's `5c`.
 *
 * Amber rather than red, and a box rather than a bare line: the colour is what
 * separates «this is a road you may carry on down» from «this road is closed»,
 * and the box is what gathers the sentence, what was resolved and the way
 * onward into one object. A red card with a solid button said «something went
 * wrong» about a route we are perfectly able to price.
 *
 * It was drawn in neutrals first, and that was the wrong end of the same
 * mistake: a grey box beside a red one reads as the one that does not matter.
 */
.notice-box { display: flex; gap: 9px; align-items: flex-start;
  padding: var(--space-4) var(--space-3);
  border: 1px solid var(--color-warning-border);
  background: var(--color-warning-bg); }
.notice-box > svg { flex: none; margin-top: 2px; }
.notice-body { min-width: 0; font-size: 13px; line-height: 1.45; }
.notice-body p { margin: 0; }
/*
 * **A box in trouble says so on its edge**, or a message under a field that
 * looks fine reads as being about the next one. The edge is doubled — a border
 * and an inset shadow of the same colour, which is how this design draws 2px
 * without moving anything by a pixel.
 *
 * A filled triangle inside the box stood here instead for a while, on the
 * argument that a coloured border is the same signal as the coloured border on
 * the message below it, spent twice. What settled it the other way is the ✕:
 * the design has drawn it inside the box since, and a mark beside a ✕ is two
 * objects competing for a 60px strip at the end of an address — in a 440px
 * panel, on the one field somebody is being asked to correct. The edge marks
 * the whole box at no cost in room.
 *
 * **The colour is a property and the edge is drawn once**, because there are
 * two states and three ways in and the alternative is the same two
 * declarations written six times. `input` bare rather than the four spellings
 * of a text box: Django renders the password and reset forms itself and those
 * carry no class of ours, and a field holds one input.
 */
.field.has-error input { --edge: var(--color-accent-700); }
.field.has-warning input { --edge: var(--color-warning-mark); }
/*
 * **And it answers the pointer in its own colour, one step deeper.**
 *
 * Every other box takes 45% ink on hover, and this one may not: an amber or a
 * red edge replaced by a grey one under the cursor says the trouble went away
 * at the moment somebody reached for the box to fix it. Deepening the colour
 * it already has is the same answer in the language the box is already
 * speaking.
 *
 * The ✕ counts as the box, here as everywhere: hovering it darkens the edge of
 * the field it acts on, and that field's edge is this colour.
 */
.field.has-error input:hover,
.p-quote .field.has-error:has(> .clear:hover) input { --edge: var(--color-accent-800); }
.field.has-warning input:hover,
.p-quote .field.has-warning:has(> .clear:hover) input { --edge: var(--color-warning-deep); }
.field.has-error input,
.field.has-warning input {
  border-color: var(--edge);
  box-shadow: inset 0 0 0 1px var(--edge);
}
/*
 * Focused, the halo goes *outside* the edge rather than instead of it: the box
 * is both the one being corrected and the one being typed in, and it has to
 * say both. One `box-shadow` list, which is why the halo is a property.
 *
 * **And it is this box's colour, not ink.** The ink wash says «you are here»,
 * which on a box that is already saying «this one is wrong» is the quieter of
 * two things drawn over the louder one. The trouble colour says both at once,
 * and the edge takes its deep step at the same time — the same step the
 * pointer gets, because arriving at a box is the same kind of event as
 * reaching for it.
 *
 * The two percentages are not a rounding: 22% of the red and 26% of the amber
 * read as the same weight of wash, and amber against this ground needs the
 * extra four.
 */
.field.has-error input:focus-visible {
  --edge: var(--color-accent-800);
  border-color: var(--edge);
  box-shadow: inset 0 0 0 1px var(--edge),
              0 0 0 3px color-mix(in srgb, var(--color-accent-700) 22%, transparent);
}
.field.has-warning input:focus-visible {
  --edge: var(--color-warning-deep);
  border-color: var(--edge);
  box-shadow: inset 0 0 0 1px var(--edge),
              0 0 0 3px color-mix(in srgb, var(--color-warning-mark) 26%, transparent);
}

/* --- the account page ---------------------------------------------------- */

/* The address and the way out on one line, the button against the right edge.
   Wrapping, because a long address at phone width must push the button under
   it rather than be squeezed by it. */
/*
 * A column of three boxes under one heading, and the column is narrower than
 * the page.
 *
 * A password is a short string and the boxes are the only thing here, so at
 * the shell's 780px they would be three lines the width of the screen with
 * nothing beside them. The design holds them at 340.
 */
/*
 * **Scoped to the page, because `form` is not only this page's.**
 *
 * «Выйти» is a POST since Django 5, so the account menu holds a form too —
 * and these rules reached it: on this page and no other, the menu's last row
 * gained 13.6px above it and its form a 340px cap. A page-scoped element
 * selector is a page-scoped element selector everywhere the page's class
 * applies, and the body carries that class over the whole shell.
 *
 * `.page` is the fix rather than a class on the form, and it is the same
 * structural fact the header row relies on: the chrome is a *sibling* of the
 * page, not a child, so nothing scoped this way can reach it by accident.
 */
.p-account .page form { max-width: 340px; }
/* The gap the design gives them, rather than each label carrying its own top
   margin. */
.p-account .page form .field + .field { margin-top: var(--space-3); }
/* **The form's own button, not every button in it.** A descendant selector
   reaches the eye inside a password box too, and a 13.6px top margin on a
   control centred by `margin-block: auto` is what stops it being centred —
   measured, the eye sat exactly `--space-4` below the middle of the field.
   The child combinator says the one this was written for. */
.p-account .page form > button { margin-top: var(--space-4); }
