/* =========================================================================
   cwm.axh.hu -- admin design system
   -------------------------------------------------------------------------
   Vanilla CSS. No Bootstrap, no build step, no preprocessor.

   The visual language follows the Sneat admin template; the palette, radii,
   shadow steps and layout metrics below were read out of that template's
   core.css rather than eyeballed, so they match. Everything else -- the
   markup contract, the class names, the dark theme surfaces -- is ours.

   ORDER MATTERS. The file is layered, and the layers get more specific as
   you go down:

     1. tokens      custom properties, light + dark
     2. base        reset and element defaults
     3. shell       the application frame: sidebar, topbar, content
     4. components  card, button, form, table, badge, dropdown, modal ...
     5. utilities   a deliberately small set, last so they win

   Add new components in section 4, never at the bottom. A component defined
   after the utilities can no longer be overridden by them, and the next
   person will not know why.
   ========================================================================= */


/* =========================================================================
   1. TOKENS
   ========================================================================= */

:root {
   /* --- brand ------------------------------------------------------- */
   --pri:        #1a6e45;
   --pri-hover:  #145736;
   --pri-sub:    #e2f1e8;   /* tinted background for the primary accent   */
   --sec:        #8592a3;
   --suc:        #71dd37;
   --suc-sub:    #e8fadf;
   --inf:        #03c3ec;
   --inf-sub:    #d7f5fc;
   --wrn:        #ffab00;
   --wrn-sub:    #fff2d6;
   --dng:        #ff3e1d;
   --dng-sub:    #ffe0db;

   /* --- surfaces ---------------------------------------------------- */
   /* The ground is deliberately well below the cards. At #f5f5f9 a white
      card stood at 1.087 against it -- a 9% difference, which is to say the
      panes were invisible and the glass had nothing to show through.

      Darkening the ground alone would have COST contrast, not gained it:
      --body is a mid-grey, so a darker ground moves the background toward
      the text rather than away from it. The ink comes down with the ground,
      and the pair together improves every reading while more than doubling
      the separation -- body on the ground 4.78 -> 4.97, body on a card
      5.19 -> 6.02, and a card now stands at 1.212 against the page. */
   --bg:         #e3eaf3;   /* the page behind everything                 */
   --paper:      #ffffff;   /* cards, menus, modals                       */
   --paper-2:    #f8f8fb;   /* table headers, inset panels                */
   --line:       #e4e6e8;
   --line-soft:  #eceef0;

   /* --- ambient ground ----------------------------------------------
      Three dilute pools of colour painted behind the whole page, so the
      translucent surfaces above have something to pick up. Deliberately
      faint: this is a tool people look at all day, not a landing page.

      In the light theme these are pale TINTS, not dilute brand colours,
      and every one of them is LIGHTER than --bg. That is a constraint, not
      a taste: --body on --bg is 4.78:1, so the ground has 0.28 of headroom
      before the .lead and .sub that sit straight on it fall under 4.5. A
      dilute #1a6e45 at 13% took it to 4.14. A mint that is lighter than the
      page cannot take anything below the flat-fill figure by construction,
      and still shows as colour -- roughly ten units of red against seven of
      green, which the eye reads as a tint and the contrast maths does not. */
   /* Still LIGHTER than the ground: these lift the page rather than tint
      it, because the tint the eye actually reads is on the CARDS, not here.
      An earlier attempt put the brand green in these pools and it meant
      lowering --body to pay for it -- a lot of contrast spent on something
      mostly hidden behind the cards anyway. */
   --amb-1:      rgba(198, 239, 217, .90);  /* mint, top left             */
   --amb-2:      rgba(196, 230, 249, .90);  /* sky, top right             */
   --amb-3:      rgba(226, 246, 205, .85);  /* leaf, bottom               */

   /* --- glass -------------------------------------------------------
      Translucent surface fills.

      --glass is the two bars. Its alpha is set by the worst thing that can
      pass behind them, not by taste: a solid --ink code panel scrolling under
      the topbar. At .70 that took the topbar's icons to 3.06:1 in light and
      2.32:1 in dark, and the account name to 4.20:1 in dark. .84 holds the
      name at 6.16:1 and the icons at 3.40:1 -- text above 4.5, icons above
      the 3:1 that non-text contrast asks for -- against that same panel.

      --glass-card is more opaque again, because body text sits on it. */
   --glass:         rgba(255, 255, 255, .84);
   --glass-card:    rgba(255, 255, 255, .86);
   --btn-alpha:     93%;   /* white ink needs a denser fill to stay legible */

   /* --- glass, part two -----------------------------------------------
      A sheen buys the LOOK of glass without spending any contrast, which is
      why it is here and more transparency is not: raising the alpha would
      have cost legibility we already spent once on the buttons.

      On a light theme the sheen works by adding opacity rather than light --
      the card is already near-white, so a white overlay reads as the pane
      being denser at the top, which is exactly how glass catches a room.

      --glass-edge is the top lip only. A uniform 1px hairline all the way
      round is the least glass-like thing a pane can have; real glass is
      bright where the light hits and disappears underneath. */
   --glass-sheen:   linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0) 58%);
   /* A filled button needs its OWN sheen, far weaker than the one the pale
      surfaces use. The same white overlay does almost nothing to a card that
      is already near-white and washes a saturated green to pale mint: at .55
      the top of a light-theme button measured 2.67:1 against its white ink.
      .08 is not a taste call, it is the most the light theme can take --
      white ink needs the fill to stay under about L .183, and the flat fill
      is already at .143. Dark can afford more, because there the sheen
      lightens a fill that carries DARK ink, which helps rather than hurts. */
   --btn-sheen:     linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,0) 62%);

   /* --- buttons: a diagonal two-tone fill --------------------------------
      A gradient across the face rather than a sheen laid over a flat colour.
      The stops are chosen by the text, not by eye: white ink means the
      LIGHTER stop is the constraint, and #208054 is as light as it goes at
      4.91 against white. The dark end is free to go as dark as it likes.

      There is no rim. A 1px inset line around a filled button reads as a
      hard border at every size, which is the thing that looked wrong; the
      shape is carried by the fill and a soft brand-tinted shadow instead.

      The transparent variants are gone. `.ghost` and `.plain` keep their
      class names -- they are used in sixty places -- but are filled now:
      a soft tint with coloured ink, which is a quieter button rather than
      an outline of one. */
   --btn-grad:      linear-gradient(135deg, #208054, #14543a);
   --btn-grad-hov:  linear-gradient(135deg, #1b7049, #10452f);
   --btn-glow:      rgba(20, 84, 58, .30);

   --btn-soft:      linear-gradient(135deg, #eaf5ef, #d9ede2);
   --btn-soft-ink:  #1a6e45;

   --btn-dang:      linear-gradient(135deg, #c93716, #9e2a0d);
   --btn-dang-hov:  linear-gradient(135deg, #b8300f, #8a2409);
   --btn-dsoft:     linear-gradient(135deg, #ffeae6, #ffdcd6);
   /* #b82a10, not #c42d12: on the flat stop the lighter one computes to 4.87,
      but rendered it measured 4.44 -- the antialiasing at the rounded corner
      is lighter than the stop itself, and that is the pixel a reader's eye
      lands next to. Measured beats computed when the two disagree. */
   --btn-dsoft-ink: #b82a10;

   --btn-neutral:   linear-gradient(135deg, #f2f4f6, #e4e8ec);
   --pick-bg:       #f4f6f7;
   --pick-bg-hov:   #eaeef0;
   --pick-line:     #dde3e7;
   --pick-ink:      #364049;
   --btn-neut-ink:  #384551;

   /* The gradient on a CARD, which is a different job from the sheen on a
      message.

      On the dark theme a white sheen over a dark pane is plainly visible and
      does the whole job. On the light theme the pane is already white, so the
      same white sheen is white on white and there is nothing to see -- which
      is exactly how the light side came to look flat next to the dark one.

      So the light card takes a brand-green wash instead -- down from the top
      edge only, the way the dark theme's sheen is, and fading out by about a
      third of the way down. A first attempt ran it diagonally across most of
      the pane at .11 and that was too much: a tint over the whole card reads
      as "this card is green" rather than as light falling on it.

      It darkens rather than lightens, which costs contrast and is therefore
      measured rather than judged by eye: at the top edge the card is
      rgb(240,245,244) against rgb(251,252,253) below, and --body on the
      strongest part reads 5.53 against 5.86 on the plain card.

      Messages keep --glass-sheen. A green wash over an amber warning makes
      the warning look like something else, and the tint is the whole point
      of a message. */
   --card-sheen:    linear-gradient(180deg, rgba(26, 110, 69, .07), rgba(26, 110, 69, 0) 34%);
   --glass-edge:    rgba(255,255,255,.95);
   --glass-line:    rgba(255,255,255,.34);   /* the rest of the rim, fainter */
   --sh-glass:      0 .75rem 2rem -.5rem rgba(34,48,62,.10),
                    0 .125rem .5rem -.125rem rgba(34,48,62,.05);
   --glass-blur:    14px;   /* bars only -- see the card rule for why      */

   /* Severity tints for .msg, translucent so the ground shows through.
      The opaque --*-sub tints stay as they are: pills and badges are small
      and sit on a card, where a see-through fill would only read as muddy. */
   --msg-neutral:   rgba(248, 248, 251, .78);
   --msg-ok:        rgba(232, 250, 223, .78);
   --msg-info:      rgba(215, 245, 252, .78);
   --msg-warn:      rgba(255, 242, 214, .80);
   --msg-err:       rgba(255, 224, 219, .80);

   /* --- text -------------------------------------------------------- */
   --ink:        #384551;   /* headings                                   */
   --body:       #5a646f;   /* running text                               */
   --muted:      #79879b;   /* labels, captions, disabled                 */
   --on-pri:     #ffffff;   /* text on a filled brand colour              */

   /* --- geometry ---------------------------------------------------- */
   --r-sm:       .25rem;
   --r:          .375rem;
   --r-lg:       .5rem;
   --r-xl:       .625rem;
   --r-pill:     50rem;

   /* --- elevation --------------------------------------------------- */
   --sh-xs:      0 .0625rem .3175rem 0 rgba(34,48,62,.06);
   --sh-sm:      0 .125rem .375rem 0 rgba(34,48,62,.08);
   --sh:         0 .1875rem .5rem 0 rgba(34,48,62,.10);
   --sh-lg:      0 .25rem .75rem 0 rgba(34,48,62,.14);
   --sh-xl:      0 .3125rem 1.375rem 0 rgba(34,48,62,.18);

   /* --- layout ------------------------------------------------------ */
   --menu-w:     16.25rem;
   --menu-w-sm:  5.25rem;   /* collapsed: icons only                      */
   --topbar-h:   3.875rem;
   --gap:        1.5rem;    /* the standard gap between cards             */

   /* --- type -------------------------------------------------------- */
   --font:       "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
                 Roboto, "Helvetica Neue", Arial, sans-serif;
   --font-mono:  ui-monospace, SFMono-Regular, Menlo, Consolas,
                 "Liberation Mono", monospace;
   --fs:         .9375rem;  /* 15px, Sneat's body size                    */
   --fs-sm:      .8125rem;
   --fs-xs:      .75rem;
   --lh:         1.53;

   --speed:      .15s;
}

/*
   Dark theme.

   Three states, not two: an explicit choice sets data-theme on <html>, and
   the default -- no attribute -- has to be resolved by prefers-color-scheme.
   Both blocks redefine ONLY tokens, so no component needs to know a theme
   exists.

   Sneat's own dark palette gave us the page and text colours; the card
   surface is ours, chosen one step lighter than the page so that elevation
   still reads without relying on shadow, which barely shows on dark.
*/
@media (prefers-color-scheme: dark) {
   :root:not([data-theme="light"]) {
      --pri:       #4cc38a;
      --pri-hover: #6ad3a3;
      --on-pri:    #10241a;
      --pri-sub:   #123a2a;
      --suc-sub:   #1e3317;
      --inf-sub:   #0b3a45;
      --wrn-sub:   #3d2c08;
      --dng-sub:   #3d1810;

      /* Dark runs the other way round: the ground is dark, so a dilute
         brand colour lightens it and light text gains rather than loses.
         Measured worst on the ground is 5.51:1 for --body. */
      --amb-1:      rgba(76, 195, 138, .12);
      --amb-2:      rgba(3, 195, 236, .09);
      --amb-3:      rgba(113, 221, 55, .05);

      --glass:      rgba(56, 69, 81, .84);
      --glass-card: rgba(56, 69, 81, .86);
      --glass-line: rgba(255, 255, 255, .08);
      --btn-alpha:  88%;
      --glass-sheen: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,0) 55%);
      --btn-sheen:   linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0) 58%);
      /* Dark keeps the white sheen it already had: a light pane edge on a
         dark card is visible on its own and wanted no changing. */
      --card-sheen:  linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,0) 55%);
      --btn-grad:      linear-gradient(135deg, #5ad197, #3fb27b);
      --btn-grad-hov:  linear-gradient(135deg, #6ddca6, #4bbd86);
      --btn-glow:      rgba(0, 0, 0, .40);
      --btn-soft:      linear-gradient(135deg, #17422f, #102f22);
      --btn-soft-ink:  #4cc38a;
      --btn-dang:      linear-gradient(135deg, #d6472d, #a8331c);
      --btn-dang-hov:  linear-gradient(135deg, #e05336, #b83a20);
      --btn-dsoft:     linear-gradient(135deg, #46201a, #341410);
      --btn-dsoft-ink: #ff8368;
      --btn-neutral:   linear-gradient(135deg, #46545f, #38454f);
      --pick-bg:      #2c3740;
      --pick-bg-hov:  #34414b;
      --pick-line:    #3d4a55;
      --pick-ink:     #d6dee5;
      --btn-neut-ink:  #ffffff;
      --glass-edge:  rgba(255,255,255,.16);
      --sh-glass:    0 .75rem 2rem -.5rem rgba(0,0,0,.42),
                     0 .125rem .5rem -.125rem rgba(0,0,0,.28);
      --msg-neutral: rgba(63, 77, 90, .76);
      --msg-ok:      rgba(30, 51, 23, .76);
      --msg-info:    rgba(11, 58, 69, .76);
      --msg-warn:    rgba(61, 44, 8, .80);
      --msg-err:     rgba(61, 24, 16, .80);

      --bg:        #2f3a44;
      --paper:     #384551;
      --paper-2:   #3f4d5a;
      --line:      #4a5866;
      --line-soft: #43505d;

      --ink:       #ffffff;
      --body:      #bdc1c5;
      --muted:     #97a2ad;

      --sh-xs:     0 .0625rem .3175rem 0 rgba(0,0,0,.20);
      --sh-sm:     0 .125rem .375rem 0 rgba(0,0,0,.24);
      --sh:        0 .1875rem .5rem 0 rgba(0,0,0,.30);
      --sh-lg:     0 .25rem .75rem 0 rgba(0,0,0,.38);
      --sh-xl:     0 .3125rem 1.375rem 0 rgba(0,0,0,.46);
   }
}

:root[data-theme="dark"] {
   --pri:       #4cc38a;
   --pri-hover: #6ad3a3;
   --on-pri:    #10241a;
   --pri-sub:   #123a2a;
   --suc-sub:   #1e3317;
   --inf-sub:   #0b3a45;
   --wrn-sub:   #3d2c08;
   --dng-sub:   #3d1810;

   /* Dark runs the other way round: the ground is dark, so a dilute
      brand colour lightens it and light text gains rather than loses.
      Measured worst on the ground is 5.51:1 for --body. */
   --amb-1:      rgba(76, 195, 138, .12);
   --amb-2:      rgba(3, 195, 236, .09);
   --amb-3:      rgba(113, 221, 55, .05);

   --glass:      rgba(56, 69, 81, .84);
   --glass-card: rgba(56, 69, 81, .86);
   --glass-line: rgba(255, 255, 255, .08);
   --btn-alpha:  88%;
   --glass-sheen: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,0) 55%);
   --btn-sheen:   linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0) 58%);
   /* Dark keeps the white sheen it already had: a light pane edge on a
      dark card is visible on its own and wanted no changing. */
   --card-sheen:  linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,0) 55%);
   --btn-grad:      linear-gradient(135deg, #5ad197, #3fb27b);
   --btn-grad-hov:  linear-gradient(135deg, #6ddca6, #4bbd86);
   --btn-glow:      rgba(0, 0, 0, .40);
   --btn-soft:      linear-gradient(135deg, #17422f, #102f22);
   --btn-soft-ink:  #4cc38a;
   --btn-dang:      linear-gradient(135deg, #d6472d, #a8331c);
   --btn-dang-hov:  linear-gradient(135deg, #e05336, #b83a20);
   --btn-dsoft:     linear-gradient(135deg, #46201a, #341410);
   --btn-dsoft-ink: #ff8368;
   --btn-neutral:   linear-gradient(135deg, #46545f, #38454f);
   --pick-bg:      #2c3740;
   --pick-bg-hov:  #34414b;
   --pick-line:    #3d4a55;
   --pick-ink:     #d6dee5;
   --btn-neut-ink:  #ffffff;
   --glass-edge:  rgba(255,255,255,.16);
   --sh-glass:    0 .75rem 2rem -.5rem rgba(0,0,0,.42),
                  0 .125rem .5rem -.125rem rgba(0,0,0,.28);
   --msg-neutral: rgba(63, 77, 90, .76);
   --msg-ok:      rgba(30, 51, 23, .76);
   --msg-info:    rgba(11, 58, 69, .76);
   --msg-warn:    rgba(61, 44, 8, .80);
   --msg-err:     rgba(61, 24, 16, .80);

   --bg:        #2f3a44;
   --paper:     #384551;
   --paper-2:   #3f4d5a;
   --line:      #4a5866;
   --line-soft: #43505d;

   --ink:       #ffffff;
   --body:      #bdc1c5;
   --muted:     #97a2ad;

   --sh-xs:     0 .0625rem .3175rem 0 rgba(0,0,0,.20);
   --sh-sm:     0 .125rem .375rem 0 rgba(0,0,0,.24);
   --sh:        0 .1875rem .5rem 0 rgba(0,0,0,.30);
   --sh-lg:     0 .25rem .75rem 0 rgba(0,0,0,.38);
   --sh-xl:     0 .3125rem 1.375rem 0 rgba(0,0,0,.46);
}


/* =========================================================================
   2. BASE
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }

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

/* --- display size --------------------------------------------------
   Everything on this site is sized in rem or em -- there are exactly three
   px font-sizes in this file and two of them are inside an SVG viewBox --
   so the root font size is a single knob that scales the type, the
   padding, the gaps and the radii together. Scaling the text alone would
   have made bigger words in the same small boxes.

   A PERCENTAGE and not a pixel value, because a percentage on <html>
   resolves against the browser's own default. Somebody who has already
   told their browser they want larger text keeps that, multiplied; a flat
   `16px` here would quietly overrule them, which is the one thing a size
   control must not do. "Normál" sets no attribute at all and is therefore
   exactly what the browser asked for.

   The media queries do not move: `rem` in a media query resolves against
   the INITIAL font size, never the root element's. So a 600px screen is
   still a 600px screen and the layout keeps the shape it had -- only the
   container queries follow, which is right, because a list with bigger
   type genuinely does need more room before it stops stacking. */
:root[data-size="l"]   { font-size: 112.5%; }
:root[data-size="xl"]  { font-size: 125%; }
:root[data-size="xxl"] { font-size: 137.5%; }

/*
   The page is not a flat fill. Three very dilute radial pools sit on top of
   --bg, which is what gives the translucent surfaces above something to pick
   up -- glass only reads when there is something behind it to blur.

   It is on <body> rather than a fixed pseudo-element so that every layout
   gets it for nothing: the application shell, the centred .authbox of the
   login screens, and the error pages all share this rule. The fill is fixed
   to the viewport, so the pools stay put while the content scrolls past.
*/
body {
   margin: 0;
   background-color: var(--bg);
   background-image:
      radial-gradient(58rem 34rem at  6% -6%, var(--amb-1), transparent 62%),
      radial-gradient(50rem 30rem at 98%  8%, var(--amb-2), transparent 64%),
      radial-gradient(46rem 32rem at 55% 96%, var(--amb-3), transparent 62%);
   background-attachment: fixed;
   background-repeat: no-repeat;
   color: var(--body);
   font-family: var(--font);
   font-size: var(--fs);
   line-height: var(--lh);
   -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
   margin: 0;
   color: var(--ink);
   font-weight: 500;
   line-height: 1.28;
}
h1 { font-size: 1.375rem; }
h2 { font-size: 1.125rem; }
h3 { font-size: 1rem; }
h4 { font-size: .9375rem; }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

a { color: var(--pri); text-decoration: none; }
a:hover { color: var(--pri-hover); text-decoration: underline; }

hr { border: 0; border-top: 1px solid var(--line); margin: 1.25rem 0; }

code, .mono, kbd { font-family: var(--font-mono); font-size: .85em; }
code {
   background: var(--paper-2);
   border: 1px solid var(--line);
   border-radius: var(--r-sm);
   padding: .1em .35em;
}

img { max-width: 100%; height: auto; }

/* One visible focus style for everything, so nothing is reachable by
   keyboard without being visible. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
   outline: 2px solid var(--pri);
   outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
   *, *::before, *::after {
      animation-duration: .001ms !important;
      transition-duration: .001ms !important;
   }
}


/* =========================================================================
   3. SHELL -- sidebar, topbar, content
   -------------------------------------------------------------------------
   .layout is a two-column grid. The sidebar is a grid column on desktop and
   a fixed off-canvas panel below 992px; the switch is the only place the
   two behaviours meet, which keeps the collapse logic from leaking into
   every component.
   ========================================================================= */

.layout {
   display: grid;
   grid-template-columns: var(--menu-w) minmax(0, 1fr);
   min-height: 100vh;
   transition: grid-template-columns var(--speed) ease;
}
.layout.is-collapsed { grid-template-columns: var(--menu-w-sm) minmax(0, 1fr); }

/* --- sidebar ------------------------------------------------------- */

.sidebar {
   position: sticky;
   top: 0;
   align-self: start;
   height: 100vh;
   display: flex;
   flex-direction: column;
   background: var(--glass);
   -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
           backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
   background-image: var(--card-sheen);
   /* The hairline is an inset shadow, not a border, so the column's width is
      untouched and the grid track does not move by a pixel. */
   box-shadow: var(--sh-sm), inset -1px 0 0 0 var(--glass-line);
   z-index: 30;
}

.sidebar-brand {
   display: flex;
   align-items: center;
   gap: .625rem;
   height: var(--topbar-h);
   padding: 0 1.25rem;
   flex: none;
   color: var(--ink);
   font-weight: 600;
   font-size: 1.05rem;
   letter-spacing: -.01em;
   text-decoration: none;
}
.sidebar-brand:hover { color: var(--ink); text-decoration: none; }
.sidebar-brand .logo {
   width: 1.9rem; height: 1.9rem; flex: none;
   display: grid; place-items: center;
   background: var(--pri); color: var(--on-pri);
   border-radius: var(--r-lg);
   /* Three capitals in a 1.9rem box: .82rem overflowed the rounded square at
      2x, which only showed up in a device-scaled screenshot. Sized to fit with
      room to spare, and clipped as a backstop if the wordmark ever grows. */
   font-size: .58rem; font-weight: 700; letter-spacing: -.01em;
   overflow: hidden;
}

.sidebar-nav {
   flex: 1 1 auto;
   overflow-y: auto;
   overflow-x: hidden;
   padding: .5rem .875rem 1.5rem;
}

.nav-head {
   margin: 1.1rem 0 .35rem;
   padding: 0 .5rem;
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-transform: uppercase;
   letter-spacing: .04em;
   white-space: nowrap;
}
.nav-head:first-child { margin-top: .35rem; }

.nav-item {
   display: flex;
   align-items: center;
   gap: .75rem;
   padding: .55rem .75rem;
   margin-bottom: .125rem;
   border-radius: var(--r-lg);
   color: var(--body);
   font-size: var(--fs);
   text-decoration: none;
   white-space: nowrap;
   transition: background var(--speed), color var(--speed);
}
.nav-item:hover { background: var(--paper-2); color: var(--ink); text-decoration: none; }
.nav-item.is-on { background: var(--pri-sub); color: var(--pri); font-weight: 500; }
.nav-item .ico { flex: none; width: 1.25rem; text-align: center; font-size: 1.05rem; }
.nav-item .label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; }
.nav-item .badge { flex: none; }

/* --- the unread count ----------------------------------------------
   One element, two shapes. Open, it sits at the end of the row where a
   count belongs; collapsed, the row is a 44px icon and the same element
   pins itself to its corner -- which is the only place left, and the
   place every operating system puts it.

   position:relative on the item is what the collapsed shape hangs off,
   and it is set unconditionally: a relative box with no absolute child
   costs nothing and a rule that only applies while collapsed would be a
   second place to remember. */
.nav-item { position: relative; }

.nav-count {
   flex: none;
   min-width: 1.35rem;
   padding: 0 .35rem;
   border-radius: var(--r-pill);
   background: var(--pri);
   color: var(--on-pri);
   font-size: var(--fs-xs);
   font-weight: 600;
   line-height: 1.35rem;
   text-align: center;
   font-variant-numeric: tabular-nums;
}

/* Collapsed: onto the icon's top-right corner. The label beside it is
   visually hidden rather than removed, so the item is still exactly as
   wide as the icon and the corner is where it looks. */
.layout.is-collapsed .nav-count {
   position: absolute;
   top: .1rem;
   right: .55rem;
   min-width: 1.05rem;
   line-height: 1.05rem;
   font-size: .65rem;
   /* A ring in the sidebar's own colour, so a digit landing on the dark
      stroke of the icon still reads as a separate thing. */
   box-shadow: 0 0 0 2px var(--paper);
}

/* EVERYTHING waiting, on the button that opens the menu. Shown at every
   width: below 992px the sidebar is off-canvas and the items are not on
   the screen, and above it a collapsed sidebar shows a ten-pixel digit in
   the corner of an icon. The button is where somebody looks to find out
   whether there is anything at all. */
.icon-count {
   position: absolute;
   top: .15rem;
   right: .15rem;
   min-width: 1.05rem;
   padding: 0 .25rem;
   border-radius: var(--r-pill);
   background: var(--pri);
   color: var(--on-pri);
   font-size: .65rem;
   font-weight: 600;
   line-height: 1.05rem;
   text-align: center;
   font-variant-numeric: tabular-nums;
   pointer-events: none;
}


/* Collapsed: icons only. Labels are hidden rather than removed so that
   screen readers still announce them. */
.layout.is-collapsed .sidebar-brand .name,
.layout.is-collapsed .nav-head,
.layout.is-collapsed .nav-item .label,
.layout.is-collapsed .nav-item .badge {
   position: absolute;
   width: 1px; height: 1px;
   overflow: hidden;
   clip: rect(0 0 0 0);
   white-space: nowrap;
}
.layout.is-collapsed .nav-item { justify-content: center; padding-inline: 0; }
.layout.is-collapsed .sidebar-brand { justify-content: center; padding-inline: 0; }

/* The second grid column. min-width:0 is load-bearing: without it a wide
   table inside makes the grid track grow instead of the table scrolling,
   and the whole page gets a horizontal scrollbar. */
.main { display: flex; flex-direction: column; min-width: 0; }

/* --- topbar -------------------------------------------------------- */

.topbar {
   position: sticky;
   top: 0;
   z-index: 20;
   display: flex;
   align-items: center;
   gap: 1rem;
   height: var(--topbar-h);
   padding: 0 var(--gap);
   margin: .75rem var(--gap) 0;
   background: var(--glass);
   -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
           backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
   background-image: var(--card-sheen);
   border-radius: var(--r-lg);
   background-image: var(--glass-sheen);
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px var(--glass-line);
}
.topbar .spacer { flex: 1 1 auto; }

.icon-btn {
   position: relative;   /* the unread count pins to its corner */
   display: inline-grid;
   place-items: center;
   width: 2.25rem; height: 2.25rem;
   flex: none;
   border: 0;
   background: transparent;
   color: var(--body);
   border-radius: var(--r-pill);
   font-size: 1.15rem;
   cursor: pointer;
   transition: background var(--speed), color var(--speed);
}
.icon-btn:hover { background: var(--paper-2); color: var(--ink); }

/* --- content ------------------------------------------------------- */

.content {
   padding: var(--gap);
   display: flex;
   flex-direction: column;
   gap: var(--gap);
   min-width: 0;
}

.page-head {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   gap: .75rem 1rem;
}
.page-head h1 { margin: 0; }
.page-head .spacer { flex: 1 1 auto; }
.page-head .sub { width: 100%; margin: -.4rem 0 0; color: var(--muted); font-size: var(--fs-sm); }

.crumbs { display: flex; flex-wrap: wrap; gap: .35rem; font-size: var(--fs-sm); color: var(--muted); }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--pri); }
.crumbs span::before { content: "/"; margin-right: .35rem; opacity: .5; }
.crumbs > :first-child::before { content: none; }

/* --- backdrop shared by the off-canvas sidebar and modals ---------- */

.backdrop {
   position: fixed;
   inset: 0;
   background: rgba(34, 48, 62, .5);
   opacity: 0;
   visibility: hidden;
   transition: opacity var(--speed) ease, visibility var(--speed) ease;
   z-index: 40;
}
.backdrop.is-open { opacity: 1; visibility: visible; }

/* --- below 992px: the sidebar becomes an off-canvas panel ---------- */

@media (max-width: 991px) {
   /* The off-canvas sidebar and its backdrop are position:fixed, and a fixed
      element sized with inset:0 takes the LAYOUT viewport -- which on a phone
      grows to whatever the page overflows to, so the backdrop then measures
      wider than the screen and the page scrolls sideways by that much. The
      site's genuinely wide content (tables, listings) scrolls inside its own
      .dlistwrap / .tablewrap, so nothing is lost by refusing it here. */
   html, body { overflow-x: hidden; }

   .layout, .layout.is-collapsed { grid-template-columns: minmax(0, 1fr); }
   .sidebar {
      position: fixed;
      inset: 0 auto 0 0;
      width: var(--menu-w);
      transform: translateX(-100%);
      transition: transform var(--speed) ease;
      box-shadow: var(--sh-xl);
      z-index: 50;
   }
   .layout.is-open .sidebar { transform: none; }
   /* Collapse is a desktop idea; on a phone the panel is either open or not. */
   .layout.is-collapsed .sidebar-brand .name,
   .layout.is-collapsed .nav-head,
   .layout.is-collapsed .nav-item .label,
   .layout.is-collapsed .nav-item .badge {
      position: static; width: auto; height: auto; clip: auto;
   }
   .layout.is-collapsed .nav-item,
   .layout.is-collapsed .sidebar-brand { justify-content: flex-start; padding-inline: .75rem; }
   /* ...and so is the count: the panel is open or it is not, so the corner
      shape has nothing to sit on. */
   .layout.is-collapsed .nav-count {
      position: static;
      min-width: 1.35rem; line-height: 1.35rem;
      font-size: var(--fs-xs);
      box-shadow: none;
   }
   .topbar { margin-inline: 1rem; padding-inline: 1rem; }
   .content { padding: 1rem; }
}


/* =========================================================================
   4. COMPONENTS
   ========================================================================= */

/* --- card ----------------------------------------------------------
   The unit everything else sits in. Borderless with a soft shadow, which
   is what makes the Sneat look: separation comes from elevation, not from
   lines.
   ------------------------------------------------------------------- */

/*
   The card is translucent and picks up the ambient pools underneath. It has
   NO backdrop-filter, and that is a measurement rather than an oversight.

   A card never overlaps anything but the page ground, and the page ground is
   three smooth gradients -- blurring a smooth gradient returns the gradient.
   Measured on /ui, 19 cards, 1280x820: blur(8px) against no filter at all
   differed by 50 pixels out of 1,049,600, none by more than 11/255, while
   scrolling went from a steady 16.7ms frame to an 18.2ms median and a 20.7ms
   95th percentile -- dropped frames, bought with nothing.

   The bars are the opposite case and do keep their blur: real content scrolls
   underneath the topbar, so there is detail there to soften.
*/
.card {
   --card-pad: 1.5rem;
   background: var(--glass-card);
   border-radius: var(--r);
   /* The lit edge is an inset shadow rather than a border on purpose. A real
      border would grow the box, and .tablewrap / .dlistwrap bleed to the card
      edge with a negative margin read from --card-pad -- which knows nothing
      about a border and would leave a line showing down both sides. */
   background-image: var(--card-sheen);
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px var(--glass-line);
   padding: var(--card-pad);
   min-width: 0;
}
/*
   A card is padded by default, because most of them are just a box with
   content in. A card BUILT from .card-head / .card-body / .card-foot manages
   its own padding instead, so the card's own has to go or everything inside is
   inset twice. :has() decides that without needing a class, which is what lets
   the views written before those parts existed keep working untouched.
*/
.card:has(> .card-head),
.card:has(> .card-body),
.card:has(> .card-foot),
.card.flush { --card-pad: 0; }
.card.pad { --card-pad: 1.5rem; }

/* ...and a card that keeps its own padding must not have its head pad again.
   
   `.pad` exists for the case this rule is written for: a card with a
   .card-head -- for the flex row, the spacer and the pill at its right --
   whose remaining content is written straight into the card rather than
   into a .card-body. Without this the heading is inset twice and the
   content once, which reads as a heading that has slipped sideways.
   
   Found the other way round: six cards built this session used .card-head
   with no .card-body, so :has(> .card-head) set --card-pad to 0 and every
   field in them sat flush against the card's edge on a 900px screen. */
.card.pad > .card-head { padding: 0 0 .75rem; }
.card.pad > .card-foot { padding-inline: 0; }

/*
   The glass contract, and the two ways out of it.

   .sidebar, .topbar and .card are all translucent; the two bars additionally
   blur what passes behind them. Cards are filled at a much higher alpha than
   the bars because body text sits on them and has to stay above 4.5:1 -- see
   /docs/page/design-system for the measured numbers.

   Two fallbacks, and both must stay AFTER the three rules above, because a
   media query adds no specificity and only source order decides:

   1. backdrop-filter unsupported. The bars would still be readable -- the
      fill alone is doing most of the work -- but a bar that only tints what
      is behind it instead of softening it is worse than an opaque one, so
      they go solid.
   2. the reader has asked for less transparency. Honour it, everywhere.

   Both land on --paper, which is exactly what these surfaces used to be.
*/
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
   .sidebar, .topbar { background: var(--paper); }
}
@media (prefers-reduced-transparency: reduce) {
   .sidebar, .topbar, .card {
      background: var(--paper);
      -webkit-backdrop-filter: none;
              backdrop-filter: none;
   }
}

/*
   A table is different, and getting this wrong was a real bug.

   The first version also zeroed the padding for any card containing a
   .tablewrap. But a card very often holds a heading, a paragraph AND a table:

       <div class="card">
          <h2>...</h2>
          <p class="sub">...</p>
          <div class="tablewrap">...

   Zeroing the card then left the heading and paragraph jammed against the
   edge, because nothing else was supplying their inset.

   So the padding stays, and the table breaks out of it by exactly that much.
   The amount is read from --card-pad, so in a card that is already unpadded
   the negative margin is zero and this does nothing.
*/
.card > .tablewrap        { margin-inline: calc(var(--card-pad) * -1); }
.card > .tablewrap:first-child { margin-top: calc(var(--card-pad) * -1); }
.card > .tablewrap:last-child  { margin-bottom: calc(var(--card-pad) * -1); }

/* The last row now meets the card's rounded bottom, so its hover background
   has to be rounded too or the corner squares off under the cursor. */
.card > .tablewrap:last-child table.grid tr:last-child td:first-child { border-bottom-left-radius: var(--r); }
.card > .tablewrap:last-child table.grid tr:last-child td:last-child  { border-bottom-right-radius: var(--r); }
.card-head {
   display: flex;
   /* wrap is load-bearing: .card-head .sub is width:100% so that it drops onto
      its own line under the heading. Without wrapping it is just another flex
      item and gets squeezed in beside the title instead. */
   flex-wrap: wrap;
   align-items: center;
   gap: .75rem;
   padding: 1.25rem 1.5rem .25rem;
}
.card-head h2, .card-head h3 { margin: 0; flex: 1 1 auto; min-width: 0; }
.card-head .sub { width: 100%; margin: .15rem 0 0; color: var(--muted); font-size: var(--fs-sm); }
.card-body { padding: 1.5rem; }
.card-head + .card-body { padding-top: 1rem; }
.card-foot {
   padding: 1rem 1.5rem;
   border-top: 1px solid var(--line-soft);
   display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
}
/* A table is flush with the card edge; its own padding does the spacing. */
.card > .tablewrap { border-radius: inherit; }
.card > .tablewrap:first-child table.grid tr:first-child th:first-child { border-top-left-radius: inherit; }
.card > .tablewrap:first-child table.grid tr:first-child th:last-child { border-top-right-radius: inherit; }

.cards {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
   gap: var(--gap);
}

/* --- stat tile ----------------------------------------------------- */

.tile { display: flex; align-items: center; gap: 1rem; padding: 1.25rem 1.5rem; }
.tile-ico {
   width: 2.75rem; height: 2.75rem; flex: none;
   display: grid; place-items: center;
   border-radius: var(--r-lg);
   background: var(--pri-sub); color: var(--pri);
   font-size: 1.25rem;
}
.tile-label { display: block; color: var(--muted); font-size: var(--fs-sm); }
.tile-value { display: block; color: var(--ink); font-size: 1.5rem; font-weight: 600; line-height: 1.2; }

/* --- button --------------------------------------------------------
   .btn is the filled primary. Modifiers: .ghost (outline), .plain (text),
   .sm, plus the semantic colours. Kept compatible with the markup already
   in the views: .btn.sm, .btn.ghost and .btn.danger all still mean what
   they meant.
   ------------------------------------------------------------------- */

.btn {
   display: inline-flex;
   align-items: center;
   justify-content: center;
   gap: .45rem;
   padding: .5rem 1.1rem;
   border: 0;
   border-radius: var(--r);
   /* The fill IS the gradient -- see --btn-grad. No rim: a 1px inset line
      around a filled button reads as a hard border at every size. The shape
      comes from the fill and a soft shadow tinted with the brand colour,
      which is a glow only in the sense that it is coloured; it is kept small
      on purpose. */
   background: var(--btn-grad);
   box-shadow: 0 .125rem .375rem -.0625rem var(--btn-glow);
   /* --on-pri, not #fff. The dark theme's primary is a LIGHT green and takes
      dark ink; hardcoding white here measured 1.95 against it, which is the
      exact trap --on-pri was introduced to close when the brand went green.
      Written down because it has now been walked into twice. */
   color: var(--on-pri);
   font-family: inherit;
   font-size: var(--fs-sm);
   font-weight: 500;
   line-height: 1.45;
   text-decoration: none;
   white-space: nowrap;
   cursor: pointer;
   transition: background var(--speed), box-shadow var(--speed),
               transform var(--speed), color var(--speed);
}
.btn:hover {
   background: var(--btn-grad-hov);
   box-shadow: 0 .3125rem .75rem -.125rem var(--btn-glow);
   color: var(--on-pri);
   text-decoration: none;
}
.btn:active { box-shadow: 0 .0625rem .1875rem -.0625rem var(--btn-glow); }
.btn:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
/* The disabled LOOK is further down, after the variants -- see the rule
   below .btn.success. Only the behaviour belongs here. */
.btn[disabled], .btn.is-disabled { pointer-events: none; }

.btn.sm    { padding: .3rem .75rem; font-size: var(--fs-xs); }
.btn.lg    { padding: .65rem 1.4rem; font-size: var(--fs); }
.btn.block { display: flex; width: 100%; }

/* --- the quieter ones -------------------------------------------------
   `.ghost` and `.plain` used to be transparent. They are filled now, and
   keep their class names because sixty views name them: a soft tint with
   coloured ink reads as a quieter button, where an outline read as an
   unfinished one. */
.btn.ghost {
   background: var(--btn-soft);
   color: var(--btn-soft-ink);
   box-shadow: 0 .0625rem .25rem -.0625rem var(--btn-glow);
}
.btn.ghost:hover { background: var(--btn-soft); color: var(--btn-soft-ink); filter: brightness(.97); }

.btn.plain {
   background: var(--btn-neutral);
   color: var(--btn-neut-ink);
   box-shadow: 0 .0625rem .25rem -.0625rem var(--btn-glow);
}
.btn.plain:hover { background: var(--btn-neutral); color: var(--btn-neut-ink); filter: brightness(.97); }

/* danger keeps white ink: its fill is a dark red in both themes,
   measured 5.37 light and 4.57 dark. */
.btn.danger  { background: var(--btn-dang); color: #fff; }
.btn.danger:hover { background: var(--btn-dang-hov); color: #fff; }
.btn.danger.ghost {
   background: var(--btn-dsoft);
   color: var(--btn-dsoft-ink);
}
.btn.danger.ghost:hover { background: var(--btn-dsoft); color: var(--btn-dsoft-ink); filter: brightness(.97); }

.btn.success { background: var(--btn-grad); color: var(--on-pri); }
.btn.success:hover { background: var(--btn-grad-hov); color: var(--on-pri); }

/* A disabled button still has to be READ.

   This used to be opacity: .55, which fades the fill and the ink by the same
   amount -- but the fill is a saturated green and the ink is white, so the
   fill washes out to near-white while the white ink has nowhere to go. The
   kiosk's "Tovabb" measured 1.6:1 in that state, and that is the state the
   recording form OPENS in: the one button the washer is looking for is the
   one they cannot read. Dark is the same story with the colours swapped.

   So a disabled button changes colour instead of thinning: the neutral fill
   .plain already uses, with muted ink. It reads as switched off because it
   is the only grey button on the screen, not because it is faint.

   It has to come after every variant -- .danger and .ghost have the same
   specificity and would otherwise paint their own fill back over it. */
.btn[disabled], .btn.is-disabled,
.btn[disabled]:hover, .btn.is-disabled:hover {
   background: var(--btn-neutral);
   color: var(--muted);
   box-shadow: none;
   filter: none;
}

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }

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

.field { margin-bottom: 1.1rem; }
.field > label, .field > .lbl {
   display: block;
   margin-bottom: .35rem;
   color: var(--ink);
   font-size: var(--fs-sm);
   font-weight: 500;
}
/* A question needs air above it when something else was just said.

   A field, or a row of them, sitting flush under a block of buttons, a
   picture or a hint reads as part of that block rather than as the next
   thing being asked -- most visibly on the recording form, where the plate
   label sat directly against the lane buttons above it.

   Only after something that is NOT itself a field: two questions in a row
   already have .field's bottom margin between them, and adding to both
   would double the gap. */
:not(.field, .row2, .formgrid) + :is(.field, .row2, .formgrid) { margin-top: .9rem; }
/* A hint is a hint wherever it is written. It used to be styled only
   inside a .field, so one next to a total or under a card heading inherited
   the page's own size and colour -- which on the kiosk, where the body is
   20px, made "Euróban ma kb. € 58" read louder than the total above it. */
.hint { margin-top: .3rem; color: var(--muted); font-size: var(--fs-xs); }
.field .hint { display: block; }
.field .bad  { display: block; margin-top: .3rem; color: var(--dng); font-size: var(--fs-xs); }

input[type=text], input[type=email], input[type=password], input[type=date],
input[type=number], input[type=search], input[type=tel], input[type=url],
select, textarea {
   display: block;
   width: 100%;
   padding: .5rem .8rem;
   background: var(--paper);
   border: 1px solid var(--line);
   border-radius: var(--r);
   color: var(--ink);
   font-family: inherit;
   font-size: var(--fs);
   line-height: 1.5;
   transition: border-color var(--speed), box-shadow var(--speed);
}
textarea { min-height: 6rem; resize: vertical; }

/* The date in a date box sits LEFT, like every other value on the site.
   -------------------------------------------------------------------------
   Safari on iOS lays the value out in a shadow element of its own and
   centres it inside the field, so the week picker on the roster read
   "2026. Oct 12." marooned in the middle of a full-width box with a select
   above it and a button below it, both left-aligned. Nothing in this
   stylesheet asked for that and no rule on the input can reach it: the
   alignment belongs to ::-webkit-date-and-time-value, which is the one
   handle the browser gives.

   Harmless everywhere else -- desktop WebKit and Blink already draw it left,
   and a browser without the pseudo-element ignores the rule. */
input[type=date]::-webkit-date-and-time-value,
input[type=month]::-webkit-date-and-time-value,
input[type=week]::-webkit-date-and-time-value,
input[type=time]::-webkit-date-and-time-value,
input[type=datetime-local]::-webkit-date-and-time-value {
   text-align: left;
}

/* --- brand mark -----------------------------------------------------
   The logo replaces the lettered square, so it is sized to the same optical
   height rather than to its own pixel dimensions: the square was 1.9rem in
   the sidebar and 2.1rem on the auth screens, and a wordmark reads at about
   the same weight when its HEIGHT matches and its width is free.

   object-fit: contain, because the file's aspect ratio belongs to whoever
   drew it and not to this stylesheet.
   -------------------------------------------------------------------- */

.logo-img {
   display: block;
   height: 1.9rem;
   width: auto;
   max-width: 11rem;
   object-fit: contain;
   flex: none;
}
/* Standing alone on the auth screens, the mark carries the whole header, so
   it is given the size the wordmark-plus-text used to occupy between them. */
.authbox .logo-img { height: 2.6rem; max-width: 14rem; }

/* --- the addwash box grid -------------------------------------------
   Copied from manage.vedox.hu's addwash_new, deliberately and closely: the
   people who will use this screen have used that one for years, and the
   layout is the part they know. A row (.boxes) holds one or two cells
   (.box), split 30/70, 40/60 or 50/50; the label sits in the narrow cell and
   the control in the wide one.

   Below 40rem every row collapses to one column, because a 30/70 split on a
   phone is a label with no room and a field with less.
   -------------------------------------------------------------------- */

.boxes {
   display: grid;
   grid-template-columns: 1fr;
   gap: .35rem 1rem;
   align-items: start;
   padding: .55rem 0;
   border-bottom: 1px solid var(--line-soft);
}
.boxes:last-of-type { border-bottom: 0; }
.boxes.center { justify-items: center; }
.box { min-width: 0; }
.box > label, .box > .lbl {
   display: block;
   margin-bottom: .25rem;
   color: var(--ink);
   font-size: var(--fs-sm);
   font-weight: 500;
}
.box .hint { margin-top: .25rem; }

@media (min-width: 40rem) {
   .boxes.s30 { grid-template-columns: 30% 1fr; }
   .boxes.s40 { grid-template-columns: 40% 1fr; }
   .boxes.s50 { grid-template-columns: 1fr 1fr; }
   .boxes.s70 { grid-template-columns: 70% 1fr; }
   /* The label cell reads as a label: right-aligned against its field, the
      way the original has it, so the eye runs down one edge. */
   .boxes.s30 > .box:first-child,
   .boxes.s40 > .box:first-child { padding-top: .45rem; text-align: right; }
}

.plates { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr)); gap: .5rem; }

/* --- pick buttons ------------------------------------------------------
   A short list of mutually exclusive answers, shown all at once instead of
   hidden behind a dropdown. This form is filled in at a washpoint, often on
   a tablet: a <select> costs a tap to open, a second smaller tap to choose,
   and shows nothing until the first one. Four buttons cost one tap and are
   readable from a standing distance.

   The unchosen ones are NEUTRAL, not tinted. Six groups of green pills down
   one form reads as noise and gives the eye nothing to land on; with a grey
   field there is exactly one green thing per row and it is the answer. The
   colour is the state, so it has to be spent on the state.

   They sit in a grid rather than a wrapping flex line, so a group that runs
   to two rows lines up instead of straggling. Equal columns are the reason
   the form reads as one thing rather than a pile of buttons.

   The radio itself is the state -- visually hidden, not display:none, so it
   stays focusable and arrow keys still move between the options. The chrome
   lives on the <span> beside it so `input:checked + span` can carry the
   selected look without :has().

   a.pick is the same chrome for the links that change washpoint or network,
   where the "state" is which page you are on and .is-on says so.
   --------------------------------------------------------------------- */

/* The UA stylesheet hides [hidden] with `display: none` at zero specificity,
   which ANY component rule setting display beats -- .picks is display:grid and
   .req is display:inline-block, so both stayed on screen while carrying the
   attribute that says they are off. One !important here is cheaper than
   remembering the trap at every component that sets display. */
[hidden] { display: none !important; }

.picks {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr));
   gap: .35rem;
   align-items: stretch;
}
.pick { margin: 0; cursor: pointer; }

/* A network's own mark on the button that picks it.
   
   Capped by HEIGHT and left to find its own width: the logos are whatever
   shape somebody uploaded, and a fixed width turns a wordmark into a smear.
   `object-fit: contain` so a tall one shrinks rather than being cropped
   through the middle.
   
   The name stays beside it. Two marks on a wet screen are not a choice
   somebody can make at a glance, and a network whose logo has been deleted
   renders with the name alone -- \Cwm\Org::hasLogo() checks the file, not
   just the column. */
.netlogo {
   height: 1.25rem;
   width: auto;
   max-width: 5rem;
   object-fit: contain;
   margin-inline-end: .4rem;
   flex: none;
}
.pick.haslogo > span,
a.pick.haslogo { padding-inline: .55rem; }

/* While the other network's form is on its way. A network change is a
   round trip and the button has to say something is happening -- without
   it the only feedback is the whole row changing a moment later, which
   reads as a mis-tap on a slow connection at a bay. */
a.pick.is-busy { opacity: .55; pointer-events: none; }
.pick input {
   position: absolute; width: 1px; height: 1px;
   margin: -1px; padding: 0; border: 0;
   clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden;
}
.pick > span,
a.pick,
button.pick {
   display: flex;
   align-items: center;
   justify-content: center;
   min-height: 2.4rem;
   height: 100%;
   padding: .4rem .7rem;
   border: 0;
   border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   color: var(--pick-ink);
   font-family: inherit;
   font-size: var(--fs-sm);
   font-weight: 500;
   line-height: 1.25;
   text-align: center;
   text-decoration: none;
   cursor: pointer;
   /* The label is an anonymous flex item, whose minimum width is the longest
      WORD in it. Hungarian vehicle names are compounds -- "Gabonaszallito",
      "Tehergepjarmu", "Szemelygepkocsi" -- and on a kiosk, where the face
      takes 1.05em of a raised root, those are wider than a 9rem track: they
      ran straight out of the tile and were cut off mid-word by the one
      beside it. min-width lets the item shrink, overflow-wrap lets the word
      break, and between them a name is always whole even if it takes two
      lines to be. */
   min-width: 0;
   overflow-wrap: anywhere;
   transition: background var(--speed), box-shadow var(--speed), color var(--speed);
}
.pick:hover > span,
a.pick:hover {
   background: var(--pick-bg-hov);
   color: var(--pick-ink);
   text-decoration: none;
}

.pick input:checked + span,
a.pick.is-on {
   background: var(--btn-grad);
   box-shadow: 0 .125rem .375rem -.0625rem var(--btn-glow);
   color: var(--on-pri);
   font-weight: 600;
}
.pick input:checked + span:hover,
a.pick.is-on:hover { background: var(--btn-grad-hov); color: var(--on-pri); }

.pick input:focus-visible + span,
a.pick:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

/* --- partner search ----------------------------------------------------
   Partners are the one list here that is not short -- a network can hold
   well over a thousand. The buttons are this washpoint's regulars, which
   answer almost every wash; the box below asks the server for the rest and
   appends what comes back as more buttons.
   --------------------------------------------------------------------- */

/* The search box, the buttons under it and the results under those are
   three stacked things and had nothing between them: the input sat flush on
   the first row of suggestions. A column gap rather than a margin on each,
   so every part of the picker is spaced the same whatever is showing. */
.pickfind { display: flex; flex-direction: column; gap: .4rem; }
/* Theirs would add to the gap and make one seam wider than the others. */
.pickfind > .picks-found { margin-top: 0; }

.pickbar {
   display: flex; align-items: center; gap: .5rem;
   margin-top: .4rem;
}
/* Full width. It was capped at 22rem, which left a search box floating in
   the middle of a card the width of the screen and reading as a stray
   field rather than as the way to find a partner. */
.pickbar input { flex: 1 1 100%; width: 100%; max-width: none; }
.pickstate { color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; }
.picks-found { margin-top: .35rem; }
.picks-found .pick > span { box-shadow: inset 0 0 0 1px var(--pri-sub); }

/* the little "kötelező" tag the driver row raises for some partners */
.req {
   display: inline-block; margin-left: .4rem;
   color: var(--btn-dsoft-ink); font-size: var(--fs-xs); font-weight: 600;
}

/* A partner that cannot be billed to its account is shown and not offered.
   The tag sits INSIDE the button's span, not beside it: `.pick > span` is
   the chrome, so a second child there would draw a second button. */
.picktag {
   margin-left: .35rem;
   color: var(--btn-dsoft-ink); font-size: var(--fs-xs); font-weight: 600;
}
.pick.is-off { cursor: not-allowed; }
.pick input:disabled + span,
.pick.is-off:hover > span { background: var(--pick-bg); opacity: .6; cursor: not-allowed; }

/* A URL is one long word, and one word cannot wrap. Left alone, a stream
   address pushed the camera listing wider than the phone it was read on. */
.sub.mono, .dlist .mono { overflow-wrap: anywhere; word-break: break-word; }

/* --- the monitor wall --------------------------------------------------
   Tiles that hold a 16:9 box whether or not a picture has arrived, so the
   wall does not jump as the cameras come in one by one.
   --------------------------------------------------------------------- */

.wall {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(21rem, 100%), 1fr));
   gap: .75rem;
}
.wall.one { grid-template-columns: minmax(0, 32rem); }

/* A column, so the caption takes the slack. The tile used to be a plain
   block on a dark ground: a grid row is as tall as its tallest tile, and a
   camera whose name fitted on one line got the difference as a black band
   under its caption. */
.tilecam {
   position: relative;
   display: flex;
   flex-direction: column;
   margin: 0;
   border-radius: var(--r);
   overflow: hidden;
   background: var(--glass-card);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.tilecam img,
.tilecam .tilecam-wait,
.tilecam .tilecam-why {
   display: block;
   width: 100%;
   aspect-ratio: 16 / 9;
   object-fit: cover;
   background: #11171b;
}
.tilecam img { opacity: 0; transition: opacity var(--speed); }
.tilecam img.is-live { opacity: 1; }
.tilecam img.is-live + .tilecam-wait { display: none; }

/* Before the first frame, and when there will never be one. */
.tilecam .tilecam-wait,
.tilecam .tilecam-why {
   position: absolute; inset: 0 0 auto 0;
   display: flex; align-items: center; justify-content: center;
   padding: 1rem;
   color: #8b979e;
   font-size: var(--fs-sm);
   text-align: center;
}
.tilecam.is-dead .tilecam-why { color: #b98d63; }

.tilecam figcaption {
   display: flex; align-items: baseline; gap: .4rem; flex-wrap: wrap;
   flex: 1 0 auto;
   align-content: flex-start;
   padding: .45rem .6rem;
   background: var(--glass-card);
}
.tilecam-name { font-size: var(--fs-sm); font-weight: 600; }
.tilecam figcaption .sub { font-size: var(--fs-xs); }
.tilecam-age { margin-left: auto; color: var(--muted); font-size: var(--fs-xs); }

/* The thumbnail column must not stretch.
   .dlist lays its tracks out as repeat(N, auto), and an `auto` track absorbs
   free space -- so on a wide screen the 7.5rem picture sat in a 22rem column
   and the name started a third of the way across the card. Naming the tracks
   for this one list gives the slack to the NAME, which is the column that can
   use it, and pins the rest to their content.

   max-content for the picture and `auto` for the rest: the slack is then
   shared among the text columns only, and they can still COMPRESS. Pinning
   them to max-content instead fixed the gap and bought an overflow -- a
   max-content track does not shrink, so a long location pushed the page
   sideways at 1100px.

   Inside the same container queries the stacking rules use, so a narrow list
   still collapses to one column rather than fighting this. */
@container dlw (min-width: 34rem) {
   .dlist.camlist[data-cols="5"] { --dl-tpl: max-content repeat(4, auto); }
}
@container dlw (min-width: 40rem) {
   .dlist.camlist[data-cols="6"] { --dl-tpl: max-content repeat(5, auto); }
}

/* A camera's thumbnail in the inventory list. Fixed 16:9 so the rows keep a
   rhythm whether or not a picture has arrived, and small enough that a long
   list is still a list rather than a wall. */
/* The BOX is on the picture, not on the cell. A list cell carries the
   column padding, and a background on it fills that padding too -- so the
   dark rectangle ran to the card's edge and the list lost its left margin. */
.camthumb { display: block; }
.camthumb img,
.camthumb .camthumb-none {
   display: block;
   width: 7.5rem;
   aspect-ratio: 16 / 9;
   border-radius: calc(var(--r) - 2px);
   background: #11171b;
   box-shadow: inset 0 0 0 1px var(--pick-line);
   object-fit: cover;
}
/* opacity, not display: the box keeps its size while the first picture is on
   its way. A failed load stays at 0 and the empty box shows through, which is
   what a camera that is not sending should look like -- the alternative was a
   row with a hole in it where the picture would be. */
.camthumb img { opacity: 0; transition: opacity var(--speed); }
.camthumb img.is-live { opacity: 1; }
.camthumb img.is-dark { opacity: .12; }
.camthumb .camthumb-none {
   display: flex; align-items: center; justify-content: center;
   background: var(--pick-bg);
   color: var(--muted); font-size: var(--fs-sm);
}

/* --- the vSec fleet ----------------------------------------------------
   The operations screen: one card per box, the worst news at the top of it.
   Meters rather than numbers alone, because "87%" means nothing until you
   have seen where 87 sits on the bar.
   --------------------------------------------------------------------- */

/* min(26rem, 100%) and not 26rem. A bare minmax() minimum is a FLOOR the
   track cannot go below, so on a phone the column stayed 416px wide, pushed
   past the container and made the whole page scroll sideways -- the header
   then looked narrower than the cards, which is how it was spotted. Wrapping
   the minimum in min(…, 100%) lets it collapse to the container on a narrow
   screen and changes nothing on a wide one. Every auto-fill track on this
   sheet is written that way for the same reason. */
.fleet { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(26rem, 100%), 1fr)); gap: 1rem; }
.fleet .card { margin: 0; }

/* A stripe down the edge, so a wall of cards can be read at a glance and
   from across a room. */
.fleet-card { position: relative; overflow: hidden; }
.fleet-card::before {
   content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
}
.fleet-card.is-ok::before   { background: var(--pri); }
.fleet-card.is-warn::before { background: #c8940c; }
.fleet-card.is-bad::before  { background: var(--btn-dsoft-ink); }

.mons { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr)); gap: .6rem; }
.mon {
   display: flex; flex-direction: column; gap: .15rem;
   padding: .55rem .7rem;
   border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.mon-label { color: var(--muted); font-size: var(--fs-xs); }
.mon-val   { font-size: 1.15rem; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.1; }
.mon-note  { color: var(--muted); font-size: var(--fs-xs); }
.mon-bar   { display: block; height: .3rem; margin-top: .2rem; border-radius: 999px; background: var(--pick-line); overflow: hidden; }
.mon-fill  { display: block; height: 100%; background: var(--pri); }

.mon.warn .mon-val  { color: #8a6508; }
.mon.warn .mon-fill { background: #c8940c; }
.mon.bad  .mon-val  { color: var(--btn-dsoft-ink); }
.mon.bad  .mon-fill { background: var(--btn-dsoft-ink); }

/* One chip per camera, the dark ones first. */
.camchips { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .7rem; }
.camchip {
   padding: .12rem .5rem;
   border-radius: 999px;
   font-size: var(--fs-xs);
   white-space: nowrap;
}
.camchip.on  { background: var(--pri-sub); color: var(--btn-soft-ink); }
.camchip.off { background: var(--btn-dsoft); color: var(--btn-dsoft-ink); font-weight: 600; }

/* --- the template editor ------------------------------------------------
   A toolbar over a contenteditable surface. The surface is styled like the
   PAGE it will become -- a white sheet, serif-free, with the same headings
   and table treatment \Cwm\Doc::document() puts around the content -- so
   what is on screen is close to what comes out of the PDF engine.
   --------------------------------------------------------------------- */

.wyswrap {
   border-radius: var(--r);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   overflow: hidden;
}
.wystools {
   display: flex; align-items: center; flex-wrap: wrap; gap: .2rem;
   padding: .35rem .4rem;
   background: var(--pick-bg);
   border-bottom: 1px solid var(--pick-line);
}
.wystools select {
   width: auto; min-width: 9rem;
   padding: .25rem .5rem;
   font-size: var(--fs-xs);
}
.wysbar { width: 1px; height: 1.3rem; margin: 0 .25rem; background: var(--pick-line); }
.wysbtn {
   min-width: 2rem; height: 2rem;
   padding: 0 .4rem;
   border: 0; border-radius: calc(var(--r) - 2px);
   background: transparent;
   color: var(--pick-ink);
   font: inherit; font-size: var(--fs-sm); line-height: 1;
   cursor: pointer;
}
.wysbtn:hover { background: var(--pick-bg-hov); }
.wysbtn:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }
.wysbtn[data-wys-source] { font-size: var(--fs-xs); padding: 0 .6rem; }

/* The sheet. White in both themes on purpose: it is a page, and a contract
   author is looking at the thing they will print rather than at the site. */
.wysedit {
   min-height: 24rem;
   max-height: 60vh;
   overflow-y: auto;
   padding: 1.4rem 1.6rem;
   background: #ffffff;
   color: #1c2529;
   font-family: var(--font);
   font-size: .95rem;
   line-height: 1.5;
}
.wysedit:focus { outline: 0; }
.wysedit > :first-child { margin-top: 0; }
.wysedit h1 { font-size: 1.35rem; margin: 0 0 .6rem; }
.wysedit h2 { font-size: 1.1rem;  margin: 1rem 0 .4rem; }
.wysedit h3 { font-size: 1rem;    margin: .9rem 0 .3rem; }
.wysedit p  { margin: 0 0 .6rem; }
.wysedit ul, .wysedit ol { margin: 0 0 .6rem; padding-left: 1.4rem; }
.wysedit a { color: #1a6e45; }
.wysedit hr { border: 0; border-top: 1px solid #c8d0d5; margin: 1rem 0; }
.wysedit table { width: 100%; border-collapse: collapse; margin: .6rem 0; }
.wysedit th, .wysedit td { border: 1px solid #9aa5ab; padding: .3rem .45rem; text-align: left; }
.wysedit th { background: #e2f1e8; }

/* The token as it sits in the text: visible enough to spot, quiet enough to
   read around. */
.wysedit { tab-size: 2; }

/* --- document templates ------------------------------------------------
   The token palette above the editor, and the editor itself. A template is
   HTML in a monospace box: what is stored is what was typed, which is what
   lets a rich editor arrive later without a migration.
   --------------------------------------------------------------------- */

.tokens { margin-bottom: .6rem; }
.pick-btn {
   display: inline-flex; align-items: center; gap: .3rem;
   min-height: 2rem;
   padding: .3rem .7rem;
   border: 0; border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   color: var(--pick-ink);
   font: inherit; font-size: var(--fs-xs);
   cursor: pointer;
   transition: background var(--speed);
}
.pick-btn:hover { background: var(--pick-bg-hov); }
.pick-btn:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.pick-btn .sub { font-size: .7em; opacity: .7; }

textarea.mono {
   font-family: var(--font-mono);
   font-size: var(--fs-sm);
   line-height: 1.5;
   tab-size: 2;
   white-space: pre;
   overflow-wrap: normal;
   overflow-x: auto;
}

/* --- equipment ---------------------------------------------------------
   The photograph of an item, and the two upload rows under its documents.
   --------------------------------------------------------------------- */

.eqimg {
   display: block;
   max-width: 22rem; width: 100%; height: auto;
   margin-bottom: .5rem;
   border-radius: var(--r);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.upl {
   display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
   margin-bottom: .5rem;
}
.upl input[type="file"] {
   flex: 1 1 16rem; min-width: 0;
   padding: .35rem .5rem;
   font-size: var(--fs-sm);
}
.mt-s { margin-top: .4rem; }

/* --- reference list furniture ------------------------------------------
   The up/down pair on an ordered list, the switch marks that say what a
   wash type does beyond its name, and the group headings the wash type
   list is drawn under.
   --------------------------------------------------------------------- */

.moves { display: inline-flex; gap: .2rem; vertical-align: middle; }
.moves form { display: inline; }
.mv {
   width: 1.9rem; height: 1.9rem;
   padding: 0;
   border: 0; border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   color: var(--pick-ink);
   font: inherit; line-height: 1;
   cursor: pointer;
}
.mv:hover { background: var(--pick-bg-hov); }
.mv:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.mv[disabled] { opacity: .35; cursor: default; }

.flags { display: inline-flex; flex-wrap: wrap; gap: .3rem; }
.flag {
   padding: .1rem .45rem;
   border-radius: 999px;
   font-size: var(--fs-xs);
   white-space: nowrap;
}
.flag.on  { background: var(--pri-sub); color: var(--btn-soft-ink); }
.flag.off { background: var(--pick-bg); color: var(--muted); text-decoration: line-through; }

.grouphead {
   display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
   padding: .7rem 1rem .4rem;
   border-top: 1px solid var(--line-soft);
}
.grouphead:first-of-type { border-top: 0; }
.grouphead h3 { margin: 0; font-size: var(--fs); }

/* a number with its unit spelled out beside it */
.withsuffix { display: flex; align-items: center; gap: .5rem; }
.withsuffix input { width: 6rem; flex: 0 0 auto; }
.withsuffix .suffix { color: var(--muted); font-size: var(--fs-sm); }
.withsuffix .suffix strong { margin-left: .4rem; color: var(--ink); }

/* The heading over one category of wash type. It sits under the field's own
   label, so it must not look like a second one -- full ink, its own case,
   and a hairline running out to the right so it reads as a divider between
   groups rather than as another caption. */
.wtgroup { margin-top: 1rem; }
.wtgroup:first-child { margin-top: .1rem; }
.wtgroup > h4 {
   display: flex;
   align-items: center;
   gap: .6rem;
   margin: 0 0 .4rem;
   color: var(--ink);
   font-size: var(--fs-sm);
   font-weight: 600;
}
.wtgroup > h4::after {
   content: "";
   flex: 1 1 auto;
   height: 1px;
   background: var(--line-soft);
}

/* What the plate lookup found. Quiet when it is information, loud when it
   is a warning that this vehicle has already been through today -- the one
   case where the note exists to stop somebody rather than help them. */
.platenote {
   margin: -.6rem 0 1.1rem;
   color: var(--muted);
   font-size: var(--fs-sm);
}
.platenote.warn {
   color: var(--btn-dsoft-ink);
   font-weight: 600;
}

/* THE SHAPE OF A YEAR, twelve marks in a row.

   A washpoint 2% out every month is a rate set wrong; one 40% out in March
   is an invoice somebody should look at, and a single summed figure cannot
   tell those apart. Each mark carries its month and its difference as a
   title, and the month as visually-hidden text, so the strip is readable
   without a pointer. */
.monthstrip { display: inline-flex; gap: 2px; align-items: center; }
.mbit {
   width: .55rem;
   height: 1.1rem;
   border-radius: 2px;
   background: var(--line);
}
.mbit.is-ok    { background: var(--suc); }
.mbit.is-under { background: var(--wrn); }
.mbit.is-over  { background: var(--dng); }

/* A slot claim whose vehicle has already been through today.

   Tinted, not struck through and not removed: it is still the day's list,
   and somebody asking "did the Opel come in" has to FIND the row rather
   than lose it. The cells carry it and not .dlist-row, because the row is
   a subgrid container with no painted area of its own.

   The e-mail address goes on its own line under the telephone number: on a
   phone the two run together into one unreadable string, and the number is
   the one somebody actually uses at a bay. */
.dlist-row.is-done > * { background: var(--suc-sub); }
.slotday-mail { display: block; font-size: var(--fs-sm); }

/* THE PARTNER'S WEEK -- /fleet/week.

   Seven day cards, not the booking week's positioned grid. A claim is a
   place in a named stretch of the morning and carries no start time, so
   there is nothing to position; and because there is no shared time axis,
   this one may simply STACK on a phone. The booking week could not -- a
   stack there throws away the gap you went to look for.

   auto-fit rather than seven fixed columns: at a tablet width four across
   and three below reads better than seven 80px slivers, and the same rule
   gives one column on a phone with nothing to switch between. */
.slw {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
   gap: .6rem;
   margin: 0 0 1.1rem;
}
.slw-day {
   display: flex;
   flex-direction: column;
   gap: .4rem;
   padding: .6rem .7rem .7rem;
   border-radius: var(--r);
   background: var(--glass-card);
   background-image: var(--card-sheen);
   box-shadow: var(--sh-sm), inset 0 0 0 1px var(--glass-line);
}
.slw-day.is-weekend { background: var(--paper-2); background-image: none; }
.slw-day.is-shut    { opacity: .72; }
/* Today is marked with the primary edge rather than a border, for the
   reason .card gives: a border grows the box and the day cards sit in a
   grid where one taller column would drag its whole row. */
.slw-day.is-today   { box-shadow: var(--sh-sm), inset 0 0 0 2px var(--pri); }

.slw-head {
   display: flex;
   align-items: baseline;
   gap: .4rem;
   padding-bottom: .35rem;
   border-bottom: 1px solid var(--line);
}
.slw-name { font-weight: 600; }
.slw-date { color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

.slw-left { display: flex; align-items: center; gap: .35rem; margin: 0; flex-wrap: wrap; }
.slw-site { color: var(--muted); font-size: var(--fs-sm); }
.slw-none { margin: 0; color: var(--muted); font-size: var(--fs-sm); }

.slw-list { list-style: none; margin: 0; padding: 0; display: flex;
            flex-direction: column; gap: .35rem; }
.slw-item {
   display: flex;
   align-items: center;
   gap: .35rem;
   flex-wrap: wrap;
   padding: .35rem .45rem;
   border-radius: var(--r-sm);
   background: var(--paper-2);
}
.slw-day.is-weekend .slw-item { background: var(--paper); }
.slw-item.is-off { opacity: .6; }
.slw-plate { font-weight: 600; letter-spacing: .02em; }
.slw-when, .slw-where { color: var(--muted); font-size: var(--fs-sm); }

/* A line that has to stop somebody reading past it: a door two partners
   hold, a plate two fleets claim. Not a flash -- nothing just happened --
   and not a hint either, so it carries the warning colour and a rule down
   the side the way the error page does. */
.warnline {
   margin: 0;
   padding: .5rem .7rem;
   border-left: 3px solid var(--wrn);
   border-radius: var(--r-sm);
   background: var(--wrn-sub);
   /* The tint carries the warning and --wrn-sub is redefined for dark, so
      the text stays the body ink rather than a colour that only reads on
      one of the two grounds. */
   color: var(--ink);
   font-size: var(--fs-sm);
}

/* The remembered driver's name, offered under the box rather than put in
   it -- and only once what has been typed is the start of that name, so by
   the time this appears it is a completion and not a proposal.

   A button and not a dropdown list, on purpose: the thing it replaces was
   the browser's own list of every name ever typed here, which is a stack of
   small targets over the field somebody is typing into. This is one target,
   under the field, the size of a button. */
.namepick {
   margin-top: .4rem;
   max-width: 100%;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   display: inline-flex;
}

/* --- figures on a dashboard --------------------------------------------
   A number and what it is. Big enough to read from a step back, because the
   person at a washpoint is looking at this between two cars and not sitting
   down to it. Auto-fill rather than a fixed count: three figures and five
   both have to look deliberate.
   --------------------------------------------------------------------- */

.figures {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
   gap: 1rem 1.5rem;
   margin: 0 0 .25rem;
}
.fig { min-width: 0; }
.fig .num {
   display: block;
   font-size: 1.6rem;
   font-weight: 600;
   line-height: 1.1;
   font-variant-numeric: tabular-nums;
   white-space: nowrap;
}
.fig .cap {
   display: block;
   margin-top: .15rem;
   color: var(--muted);
   font-size: var(--fs-xs);
   letter-spacing: .04em;
   text-transform: uppercase;
}

/* --- a form of many short answers ------------------------------------
   A personnel record is thirty fields and most of them are short. In one
   column that is a page nobody reaches the bottom of, with a 700px box
   waiting for "2" in it. Laid out as a grid it is three columns on a desk
   and one on a phone, and the eye still runs down each column.

   Free text that can run long -- a textarea -- takes the full width, because
   a three-line comment box squeezed into a third of the row is a box people
   stop writing in.
   --------------------------------------------------------------------- */

.formgrid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(17rem, 100%), 1fr));
   gap: 0 1.25rem;
}
.formgrid > .field { min-width: 0; }
.formgrid > .field.wide { grid-column: 1 / -1; }

/* --- a two-state toggle inside a list row ------------------------------
   The personnel settings screen is thirty-four rows of "do we keep this?",
   and nearly every answer is yes. Rendered as .btn the screen is a wall of
   solid green with the four interesting rows -- the ones switched OFF --
   hiding in it. A pill carries the state quietly and spends the contrast on
   the difference instead.
   --------------------------------------------------------------------- */

.pilltog {
   border: 0;
   font: inherit;
   cursor: pointer;
   transition: box-shadow var(--speed), background var(--speed);
}
.pilltog:hover { box-shadow: inset 0 0 0 1px var(--pri); }
.pilltog:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

/* One grid for all six cards, so the columns line up down the whole page.
   Six lists each sizing their own max-content tracks put the middle column in
   six different places, which reads as six unrelated tables. */
.dlist.hrfields { grid-template-columns: minmax(9rem, 1fr) 9rem 9rem; }

/* The recovery code, shown once and never again. Large, spaced and
   selectable, because somebody is copying it onto paper or into a password
   manager and a mis-read character costs them the data. */
.bigcode {
   margin: 1rem 0;
   padding: .9rem 1rem;
   border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
   font-size: 1.15rem;
   letter-spacing: .08em;
   text-align: center;
   word-break: break-all;
   user-select: all;
}

/* --- read-only facts, and the headings over them -----------------------
   A personnel record is thirty short values. Stacked as .field blocks that is
   a page nobody scrolls to the bottom of; as a two-column definition grid it
   is one screen. The label sits above its value for the same reason the
   recording form does it that way -- one column for the eye to run down.
   --------------------------------------------------------------------- */

.facts {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
   gap: .85rem 1.25rem;
   margin: 0 0 .25rem;
}
/* Each pair is one cell. Without the wrapper the dt and the dd take a cell
   each and the label ends up beside a value it does not belong to -- two
   columns of alternating labels and values, which reads as nonsense. */
.facts > div { min-width: 0; }
.facts dt {
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 600;
   letter-spacing: .05em;
   text-transform: uppercase;
}
/* A value here can be one unbroken token longer than its column: a partner's
   invoice address is a comma-separated list with no spaces in it, and at
   390px a single long address is already wider than the card. Without this
   it runs out past the card's edge rather than wrapping. `anywhere` and not
   `break-word`, because the list has no break opportunity to prefer. */
.facts dd { margin: .1rem 0 0; overflow-wrap: anywhere; }

/* The heading over one group of them, on a form or on a page of facts. It is
   an h3 doing the work of a section break, so it needs the rule to separate
   it from what came before -- without one, twenty fields under six headings
   read as one undifferentiated list. */
h3.formgroup {
   display: flex;
   align-items: center;
   gap: .6rem;
   margin: 1.5rem 0 .75rem;
   font-size: var(--fs-sm);
   font-weight: 600;
}
h3.formgroup:first-child { margin-top: 0; }
h3.formgroup::after {
   content: "";
   flex: 1 1 auto;
   height: 1px;
   background: var(--line-soft);
}

.req { color: var(--btn-dsoft-ink); font-weight: 600; }

/* --- the recording form ------------------------------------------------
   addwash_new's shape. Labels sit ABOVE their field in small caps -- the old
   page's `fwlabel` -- so the eye travels down one column instead of
   zig-zagging between a right-aligned label and a left-aligned box. The
   people filling this in have read that layout for years.
   --------------------------------------------------------------------- */

.washform .field > label,
.washform .field > .lbl,
.washform .askrow .lbl {
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 600;
   letter-spacing: .05em;
   text-transform: uppercase;
}
.washform .field { margin-bottom: 1.35rem; }
.washform .actions { margin-top: .5rem; }

/* Room under the Mentés button for the on-screen keyboard.

   A keyboard is drawn OVER the page, not beside it: the viewport does not
   shrink, so the browser cannot scroll a field out from under it if there
   is nothing below to scroll to. The last question on this form is the
   comment, and on a tablet at a bay it sat behind the keys with the save
   button under them.

   300px is the owner's number and it is about right -- a phone keyboard is
   260-310px in portrait. It is empty space at the END of a form somebody
   scrolls down anyway, so it costs a pointer nothing.

   MARGIN and not padding: padding would stretch the card's white box 300px
   past its last control, which reads as a panel that failed to load
   something. The room belongs under the card, not inside it. */
.washform { margin-bottom: 300px; }

/* The one row that keeps the old half-and-half split: the question on the
   left, the three answers on the right. */
.washform .askrow { border-bottom: 0; padding-bottom: .35rem; }

/* The time is a VALUE with a button, not an open date field. It is right
   almost every time, and an input sitting open invites a stray tap. */
.whenrow { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.whenval { font-size: 1.05rem; font-variant-numeric: tabular-nums; }
.whenrow input[type="datetime-local"] { width: auto; }

/* --- wash types ---------------------------------------------------------
   One wide row each: the name on the left, the count and what it comes to on
   the right, all inside a single button. Two columns where there is room.
   The count only appears on a row that is on -- a stepper beside an unchosen
   wash type invites filling it in and wondering why nothing happened -- but
   its SPACE is always reserved, or selecting one would reflow the grid and
   move the row under the thumb.
   --------------------------------------------------------------------- */

.washtypes {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(21rem, 100%), 1fr));
   gap: .4rem;
}
.wtrow {
   display: flex;
   align-items: stretch;
   gap: .5rem;
   padding: .5rem .7rem;
   border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   color: var(--pick-ink);
   transition: background var(--speed), box-shadow var(--speed), color var(--speed);
}
.wtrow:hover { background: var(--pick-bg-hov); }
.wtrow.is-on {
   background: var(--btn-grad);
   box-shadow: 0 .125rem .375rem -.0625rem var(--btn-glow);
   color: var(--on-pri);
}

/* The tile IS the label now, so the pointer belongs on the whole of it and
   the inner span is just layout. */
.wtrow { cursor: pointer; }
.wtpick { display: flex; align-items: center; flex: 1 1 auto; min-width: 0; margin: 0; }
/* The stepper is the one part that is not a way to tick the box. */
.wtqty { cursor: auto; }
.wtpick input {
   position: absolute; width: 1px; height: 1px;
   margin: -1px; padding: 0; border: 0;
   clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden;
}
.wtname { font-size: var(--fs-sm); font-weight: 500; line-height: 1.25; }
.wtrow.is-on .wtname { font-weight: 600; }

.wtside { display: flex; align-items: center; gap: .5rem; flex: 0 0 auto; }
.wtprice {
   min-width: 5rem;
   font-size: var(--fs-sm); font-weight: 600;
   font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap;
}
.wtprice .bad { color: var(--btn-dsoft-ink); }

.wtqty { display: flex; align-items: center; gap: .2rem; visibility: hidden; pointer-events: none; }
.wtrow.is-on .wtqty { visibility: visible; pointer-events: auto; }
/* A type that cannot be counted has no stepper at all -- not a hidden one,
   because the space it reserves is only worth reserving where something will
   appear in it. */
.wtqty[hidden] { display: none !important; }
.wtunit { font-size: var(--fs-xs); opacity: .85; margin-left: .15rem; }
.wtqty input {
   width: 2.8rem; min-width: 0;
   padding: .15rem .2rem;
   text-align: center;
   font-variant-numeric: tabular-nums;
}
.wtstep {
   flex: 0 0 auto;
   width: 1.6rem; height: 1.6rem;
   padding: 0;
   border: 0; border-radius: calc(var(--r) - 3px);
   background: rgba(255, 255, 255, .22);
   color: inherit;
   font: inherit; font-size: var(--fs-sm); line-height: 1;
   cursor: pointer;
}
.wtrow:not(.is-on) .wtstep { background: var(--pick-line); }
.wtstep:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

.wtsum {
   display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap;
   margin-top: .9rem; padding-top: .75rem;
   border-top: 1px solid var(--line-soft);
}
.wtsum strong { font-size: 1.15rem; font-variant-numeric: tabular-nums; }

@media (max-width: 30rem) {
   .wtrow { grid-template-columns: 1fr 4.5rem; }
   .wtprice { grid-column: 1 / -1; text-align: left; }
}

/* --- password reveal ------------------------------------------------
   The button sits inside the field rather than beside it, so revealing a
   password cannot reflow the form. The input gets padding on the end side
   to keep the text from running under the button, and the button is
   padded to a 44px touch target while drawing a 20px icon -- the reveal
   is most wanted on a phone, which is exactly where a 20px tap target is
   a nuisance.
   -------------------------------------------------------------------- */

.pw { position: relative; }
.pw > input { padding-inline-end: 2.9rem; }
.pw-eye {
   position: absolute;
   inset-block: 0;
   inset-inline-end: 0;
   display: grid;
   place-items: center;
   width: 2.75rem;
   padding: 0;
   border: 0;
   background: none;
   color: var(--muted);
   cursor: pointer;
   border-radius: var(--r);
}
.pw-eye:hover { color: var(--ink); }
.pw-eye:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }
.pw-eye svg {
   width: 1.25rem; height: 1.25rem;
   fill: none; stroke: currentColor; stroke-width: 1.6;
   stroke-linecap: round; stroke-linejoin: round;
}
/* One icon at a time; which one is a function of the button's own state,
   so the script sets aria-pressed and the stylesheet does the rest. */
.pw-eye .eye-off,
.pw-eye[aria-pressed="true"] .eye-on { display: none; }
.pw-eye[aria-pressed="true"] .eye-off { display: block; }
.pw-eye[aria-pressed="true"] { color: var(--pri); }
select {
   appearance: none;
   padding-right: 2.2rem;
   /* A chevron drawn as a background image: no icon font, no inline SVG in
      the markup, and it still recolours with the text. */
   background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                     linear-gradient(135deg, currentColor 50%, transparent 50%);
   background-position: calc(100% - 1.1rem) center, calc(100% - .75rem) center;
   background-size: .35rem .35rem, .35rem .35rem;
   background-repeat: no-repeat;
}
:where(input, select, textarea):focus {
   border-color: var(--pri);
   box-shadow: 0 0 0 .15rem var(--pri-sub);
   outline: none;
}
:where(input, select, textarea).err { border-color: var(--dng); }
:where(input, select, textarea).err:focus { box-shadow: 0 0 0 .15rem var(--dng-sub); }
:where(input, select, textarea)[disabled] { background: var(--paper-2); color: var(--muted); }
::placeholder { color: var(--muted); opacity: 1; }

fieldset { border: 0; margin: 0 0 1.25rem; padding: 0; }
legend { padding: 0; margin-bottom: .6rem; color: var(--ink); font-size: var(--fs); font-weight: 500; }

.checks { display: flex; flex-direction: column; gap: .5rem; }
.checks label { display: flex; align-items: flex-start; gap: .55rem; font-size: var(--fs-sm); cursor: pointer; }
.checks input { width: auto; margin: .2rem 0 0; accent-color: var(--pri); }
.checks .flag { color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-xs); }

form.inline { display: inline; }
.row2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); gap: 0 1.25rem; }

/* A card that holds more than one thing needs the things to be apart.
   .card-head already pads itself, so a heading or a form that FOLLOWS
   something inside a card is what needs the space -- which is why this is a
   sibling rule rather than a margin on the elements themselves: the first
   child of a card must still sit against the card's own padding. */
.card > * + h3,
.card > * + form,
.card > * + .dlistwrap { margin-top: 1.25rem; }
.card > h3 { margin-bottom: .6rem; }

/* An inline form is inline so its controls sit on one line; it is not a
   reason for the next thing to crowd it. */
.card > form.inline + * { margin-top: 1.1rem; }
.card > form.inline .field { margin-bottom: 0; }
.card > form.inline { display: flex; align-items: flex-end; gap: .75rem; flex-wrap: wrap; }
.card > form.inline .actions { margin: 0; }
.stack { display: flex; flex-direction: column; gap: var(--gap); }

/* --- table ---------------------------------------------------------- */

.tablewrap { overflow-x: auto; }
table.grid { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.grid th {
   padding: .75rem 1.5rem;
   background: var(--paper-2);
   border-bottom: 1px solid var(--line);
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-align: left;
   text-transform: uppercase;
   letter-spacing: .04em;
   white-space: nowrap;
}
table.grid td {
   padding: .8rem 1.5rem;
   border-bottom: 1px solid var(--line-soft);
   color: var(--body);
   vertical-align: middle;
}
table.grid tr:last-child td { border-bottom: 0; }
table.grid tbody tr { transition: background var(--speed); }
table.grid tbody tr:hover { background: var(--paper-2); }
table.grid th.right, table.grid td.right { text-align: right; }
table.grid td strong, table.grid td a { color: var(--ink); }
table.grid td a:hover { color: var(--pri); }
table.grid td.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* --- badge / pill --------------------------------------------------
   Soft-tinted rather than filled: Sneat's "label" badges. .on/.off/.lvl
   are kept because the existing views use them.
   ------------------------------------------------------------------- */

.pill, .badge {
   display: inline-block;
   padding: .2rem .55rem;
   border-radius: var(--r-sm);
   background: var(--paper-2);
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   line-height: 1.45;
   white-space: nowrap;
}
.pill.primary, .badge.primary { background: var(--pri-sub); color: var(--pri); }
.pill.on,  .pill.success, .badge.success { background: var(--suc-sub); color: #4a9421; }
.pill.info,    .badge.info    { background: var(--inf-sub); color: #0b8fab; }
.pill.warn,    .badge.warn    { background: var(--wrn-sub); color: #a67200; }
.pill.off, .pill.danger,  .badge.danger  { background: var(--dng-sub); color: #c22d12; }
.pill.lvl { background: var(--pri-sub); color: var(--pri); font-family: var(--font-mono); }
.pill.round, .badge.round { border-radius: var(--r-pill); }

/* A wrapping row of pills -- the wash types one partner bought, and the same
   across all of them. The count rides INSIDE the pill: "Külső mosás 42" is
   one fact and splitting it into two columns would be two. */
.chips { display: flex; flex-wrap: wrap; gap: .3rem; }
.chips .pill b { margin-inline-start: .3rem; font-weight: 700; }

/* --- sparkline ------------------------------------------------------
   One person's last few months, drawn in a cell. Deliberately tiny and
   deliberately unlabelled: it is there to say "step" or "drift" at a
   glance, and the numbers either side of it in the same row are the
   answer to "how much". The values ride in the <title>.

   currentColor, so it follows the row -- including inside a .key cell and
   in the dark palette, with no second set of values to keep in step.
   ------------------------------------------------------------------- */

.spark {
   display: block;
   overflow: visible;
   color: var(--pri);
}
.spark polyline {
   fill: none;
   stroke: currentColor;
   stroke-width: 1.5;
   stroke-linejoin: round;
   stroke-linecap: round;
}

/* --- avatar --------------------------------------------------------- */

.avatar {
   width: 2.25rem; height: 2.25rem; flex: none;
   display: inline-grid; place-items: center;
   border-radius: var(--r-pill);
   background: var(--pri-sub);
   color: var(--pri);
   font-size: var(--fs-sm);
   font-weight: 600;
   text-transform: uppercase;
   overflow: hidden;
}
.avatar.sm { width: 1.75rem; height: 1.75rem; font-size: var(--fs-xs); }
.avatar.lg { width: 3rem; height: 3rem; font-size: var(--fs); }

.who { display: flex; align-items: center; gap: .625rem; min-width: 0; }
/* The same lockup as a button, for the topbar account menu. */
.who-btn {
   padding: .2rem;
   border: 0;
   background: none;
   border-radius: var(--r-pill);
   font-family: inherit;
   font-size: inherit;
   text-align: left;
   cursor: pointer;
}
.who-btn .name { max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 575px) { .who-btn .name { display: none; } }
.who .name { display: block; color: var(--ink); font-weight: 500; line-height: 1.3; }
.who .meta { display: block; color: var(--muted); font-size: var(--fs-xs); line-height: 1.3; }

/* --- dropdown -------------------------------------------------------
   Markup contract:

     <div class="dropdown">
       <button data-dropdown aria-expanded="false">...</button>
       <div class="dropdown-menu">
         <a class="dropdown-item" href="...">...</a>
       </div>
     </div>

   The menu is in the flow of a positioned parent, so it needs no JS to
   place it. app.js only toggles .is-open and the aria-expanded state.
   ------------------------------------------------------------------- */

.dropdown { position: relative; display: inline-block; }
.dropdown-menu {
   position: absolute;
   top: calc(100% + .35rem);
   right: 0;
   min-width: 11rem;
   padding: .35rem;
   background: var(--paper);
   border-radius: var(--r-lg);
   box-shadow: var(--sh-lg);
   z-index: 25;
   opacity: 0;
   visibility: hidden;
   transform: translateY(-.35rem);
   transition: opacity var(--speed) ease, transform var(--speed) ease, visibility var(--speed);
}
.dropdown.is-open > .dropdown-menu { opacity: 1; visibility: visible; transform: none; }
.dropdown-menu.left { right: auto; left: 0; }

.dropdown-item {
   display: flex;
   align-items: center;
   gap: .6rem;
   width: 100%;
   padding: .5rem .7rem;
   border: 0;
   border-radius: var(--r);
   background: transparent;
   color: var(--body);
   font-family: inherit;
   font-size: var(--fs-sm);
   text-align: left;
   text-decoration: none;
   cursor: pointer;
}
.dropdown-item:hover { background: var(--paper-2); color: var(--ink); text-decoration: none; }
.dropdown-item.is-on { background: var(--pri-sub); color: var(--pri); font-weight: 500; }
.dropdown-item.danger { color: var(--dng); }
.dropdown-item.danger:hover { background: var(--dng-sub); color: var(--dng); }
.dropdown-sep { height: 1px; margin: .35rem .25rem; background: var(--line-soft); }
.dropdown-head { padding: .5rem .7rem .35rem; color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; }

/* The four display sizes, two to a line: four full-width rows would make
   the account menu taller than most of the pages it opens over. */
.sizepick { display: grid; grid-template-columns: 1fr 1fr; gap: .15rem; }

/* --- modal ----------------------------------------------------------
   .modal carries [hidden] when closed, so it is inert to assistive tech
   and to tabbing without any JS help. app.js removes [hidden], then adds
   .is-open one frame later so the transition has something to animate
   from.
   ------------------------------------------------------------------- */

.modal {
   position: fixed;
   inset: 0;
   z-index: 60;
   display: grid;
   place-items: start center;
   padding: 1.5rem 1rem 3rem;
   overflow-y: auto;
   background: rgba(34, 48, 62, .5);
   opacity: 0;
   transition: opacity var(--speed) ease;
}
.modal[hidden] { display: none; }
.modal.is-open { opacity: 1; }

.modal-card {
   width: 100%;
   max-width: 32rem;
   margin-top: 4vh;
   background: var(--paper);
   border-radius: var(--r-lg);
   box-shadow: var(--sh-xl);
   transform: translateY(-.75rem) scale(.98);
   transition: transform var(--speed) ease;
}
.modal.is-open .modal-card { transform: none; }
.modal-card.wide  { max-width: 46rem; }
.modal-card.slim  { max-width: 24rem; }

.modal-head {
   display: flex; align-items: center; gap: .75rem;
   padding: 1.25rem 1.5rem .75rem;
}
.modal-head h3 { flex: 1 1 auto; margin: 0; }
.modal-body { padding: .5rem 1.5rem 1.25rem; }
.modal-foot {
   display: flex; justify-content: flex-end; flex-wrap: wrap; gap: .5rem;
   padding: 1rem 1.5rem;
   border-top: 1px solid var(--line-soft);
}

/* --- the letter editor ---------------------------------------------
   A contenteditable with four buttons, not a library: the CSP admits no
   external script, so an editor is either vendored into this repository
   or it is written. Four buttons is what a letter from a bay needs, and
   whatever is pasted into it is rebuilt by HTMLPurifier on the way out --
   which is what makes a crude editor safe enough to be a small one.

   It is `hidden` in the markup and shown by app.js, with the textarea
   underneath it as the no-script fallback. */
.rtx {
   border: 1px solid var(--line);
   border-radius: var(--r-sm);
   background: var(--paper);
   overflow: hidden;
}
.rtx:focus-within { outline: 2px solid var(--pri); outline-offset: 1px; }

.rtxbar {
   display: flex;
   flex-wrap: wrap;
   gap: .15rem;
   padding: .3rem;
   background: var(--paper-2);
   border-bottom: 1px solid var(--line-soft);
}
.rtxb {
   min-width: 2rem;
   height: 2rem;
   padding: 0 .45rem;
   border: 0;
   border-radius: var(--r-sm);
   background: transparent;
   color: var(--body);
   font: inherit;
   font-size: var(--fs-sm);
   line-height: 1;
   cursor: pointer;
}
.rtxb:hover { background: var(--paper); color: var(--ink); }
.rtxb:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }

.rtxbody {
   min-height: 14rem;
   max-height: 60vh;
   overflow-y: auto;
   padding: .75rem .9rem;
   color: var(--ink);
   /* 16px where the pointer is coarse, for the reason the touch rule at
      the end of this sheet gives: anything smaller makes iOS zoom the
      page in the moment the caret lands. */
   font-size: var(--fs);
   line-height: 1.5;
}
.rtxbody:focus { outline: 0; }
.rtxbody p { margin: 0 0 .7em; }
.rtxbody blockquote {
   margin: 0 0 .7em .8em;
   padding-left: .8em;
   border-left: 2px solid var(--line);
   color: var(--muted);
}
.rtxbody ul, .rtxbody ol { margin: 0 0 .7em; padding-left: 1.3rem; }

@media (pointer: coarse) {
   .rtxbody { font-size: 16px; }
}

/* The signature, shown under the editor and not editable there: it is put
   on by the server, and the company half is not the writer's to change. */
.signprev {
   padding: .6rem .8rem;
   border-radius: var(--r-sm);
   background: var(--paper-2);
   color: var(--muted);
   font-size: var(--fs-sm);
}
.signprev img { max-width: 10rem; height: auto; }
.signprev p { margin: 0 0 .4em; }
.signprev :last-child { margin-bottom: 0; }
/* The block the NETWORK wrote may carry its own colour -- it is markup for
   a mail client, where there is one background and it is white. On a dark
   card that colour is a dark grey on a dark grey. The preview is this
   site's, so it takes this site's ink.

   !important because the colour is an inline style on the author's own
   markup, and there is nothing else that beats one. Only inside .signprev,
   and only for the preview: what goes OUT keeps every colour it was
   given. */
:root[data-theme="dark"] .signprev,
:root[data-theme="dark"] .signprev * { color: var(--muted) !important; }
:root[data-theme="dark"] .signprev a { color: var(--pri) !important; }

@media (prefers-color-scheme: dark) {
   :root:not([data-theme="light"]) .signprev,
   :root:not([data-theme="light"]) .signprev * { color: var(--muted) !important; }
   :root:not([data-theme="light"]) .signprev a { color: var(--pri) !important; }
}

/* --- a field with a picker beside it -------------------------------
   The input keeps the whole line and the button sits on the end of it,
   so the field still reads as one thing. The button does not shrink:
   "Címjegyzék" wrapping to two lines inside a 2rem button was the first
   attempt at this. */
.withpick { display: flex; align-items: center; gap: .5rem; }
.withpick > input { flex: 1 1 auto; min-width: 0; }
.withpick > .btn { flex: 0 0 auto; }

/* --- the address book ----------------------------------------------
   A scrolling list inside the modal, so the search box and the Kész
   button stay where they are however many names come back. */
.ablist {
   max-height: 50vh;
   overflow-y: auto;
   margin-top: .5rem;
   border-top: 1px solid var(--line-soft);
}
.abrow {
   display: flex;
   align-items: center;
   gap: .5rem .75rem;
   width: 100%;
   padding: .55rem .25rem;
   border: 0;
   border-bottom: 1px solid var(--line-soft);
   background: transparent;
   color: var(--body);
   font: inherit;
   text-align: left;
   cursor: pointer;
}
.abrow:hover { background: var(--paper-2); color: var(--ink); }
.abrow .name { font-weight: 500; color: var(--ink); }
.abrow .addr { color: var(--muted); font-size: var(--fs-sm); overflow-wrap: anywhere; }
/* Addresses stay out of sight until the box above the list is ticked. */
.ablist:not(.show-addr) .abrow .addr { display: none; }
.abrow .spacer { flex: 1 1 auto; }
/* A tick, not a disabled row: picking the same person twice is a thing
   somebody tries, and a row that stops answering looks broken. */
.abrow .on { flex: 0 0 auto; color: var(--pri); font-weight: 700; visibility: hidden; }
.abrow.is-on .on { visibility: visible; }

/* --- message / alert ------------------------------------------------ */

.msg {
   padding: .8rem 1.1rem;
   border-radius: var(--r);
   /* No left rule, and no border at all: the same lit inset edge the cards
      carry, so a message reads as another pane of the same glass rather than
      a flagged strip. Severity is carried by the tint alone, which is why the
      tints are the strongest the palette allows while keeping --ink above
      4.5:1 on the page ground. No backdrop-filter, for the reason given on
      .card -- a message only ever overlaps a smooth gradient. */
   background: var(--msg-neutral);
   background-image: var(--glass-sheen);
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px var(--glass-line);
   color: var(--ink);
   font-size: var(--fs-sm);
}
/* A row that says something is waiting and offers the door to it. A .msg
   because that is what it is -- the difference is that it has an action on
   the end, so it lays itself out as a row and keeps the button out of the
   sentence. */
.msg.alertrow {
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   gap: .5rem .75rem;
   padding-block: .6rem;
}
.msg.alertrow .spacer { flex: 1 1 auto; }

.msg ul { margin: .4rem 0 0; padding-left: 1.1rem; }
.msg li + li { margin-top: .2rem; }
.msg-ok   { background: var(--msg-ok); }
.msg-err  { background: var(--msg-err); }
.msg-warn { background: var(--msg-warn); }
.msg-info { background: var(--msg-info); }

/* --- impersonation banner ------------------------------------------
   Deliberately loud and full-bleed at the very top of the document. The
   commonest impersonation accident is forgetting you are in one.
   ------------------------------------------------------------------- */

.imp { background: var(--wrn); color: #3a2a00; }
.imp .row {
   display: flex; align-items: center; flex-wrap: wrap; gap: .75rem;
   max-width: 90rem; margin: 0 auto; padding: .5rem 1.5rem;
   font-size: var(--fs-sm);
}
.imp strong { font-weight: 600; }
.imp .btn.danger { background: #3a2a00; }
.imp .btn.danger:hover { background: #241a00; }

/* --- empty state ----------------------------------------------------- */

.empty {
   padding: 3rem 1.5rem;
   text-align: center;
   color: var(--muted);
   font-size: var(--fs-sm);
}
.empty .big { display: block; margin-bottom: .5rem; font-size: 2rem; opacity: .45; }

/* --- filter bar -------------------------------------------------------
   A row of controls above a list: a select, a search box, a submit. It
   wraps rather than scrolls, because on a phone a filter you cannot see is
   a filter that does not exist.
   ------------------------------------------------------------------- */

.filterbar {
   display: flex; flex-wrap: wrap; align-items: flex-end; gap: .75rem;
}
.filterbar .field { margin-bottom: 0; }
.filterbar .field.grow { flex: 1 1 12rem; min-width: 10rem; }
.filterbar .field > label { margin-bottom: .25rem; }

/* --- pager ------------------------------------------------------------
   Previous / next and a position, not a numbered list: the catalogue is
   ordered by id and nobody navigates it by page number.
   ------------------------------------------------------------------- */

.pager {
   display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
   color: var(--muted); font-size: var(--fs-sm);
}
.pager .pos { margin-inline: .25rem; }

/* --- translation row --------------------------------------------------
   The source cell is read-only text and the target cell is an input, side
   by side in a .dlist. The input fills its cell so the two columns line up
   even when the source wraps to three lines.
   ------------------------------------------------------------------- */

.tsrc {
   font-size: var(--fs-sm);
   overflow-wrap: anywhere;
}
.tsrc code { font-family: var(--font-mono); font-size: var(--fs-xs); }
/* HOW THE MONEY STANDS, read before anything on the row is.
   -------------------------------------------------------------------------
   A stripe down the left edge of an invoice row: red when it is past its
   deadline, yellow when it falls due within the week, green when it is in.
   An open invoice with time left gets NOTHING, which is the point -- a
   colour on every row is a colour that says nothing, and the eye wants the
   two or three rows that need an answer today.

   50px and then transparent, so it reads as a mark on the row rather than a
   highlight of it: the text keeps the plain background it has everywhere
   else on the site and stays legible without a second set of colours.

   THE SOFT TOKENS, not the solid ones. It was var(--dng) and the first few
   pixels were a bar of pure #ff3e1d, which reads as an error state rather
   than as a note in the margin -- a register where a third of the rows are
   paid would have been striped like a warning light. The --*-sub three are
   the site's own light variants of the same semantic colours and they are
   redefined for dark mode, so this needs no second set and no hand-mixed
   alpha that would only be right on a white background.

   background-image, not background: the row's own background -- the zebra,
   the hover -- is set elsewhere and has to survive this.

   \Cwm\Invoice::due() decides which, and the Fizetés filter on the same
   screen asks for the same four words, so the stripe and the filter cannot
   come to disagree about what "lejárt" means. */
.dlist-row.is-due-late { background-image: linear-gradient(to right, var(--dng-sub) 0, transparent 50px); }
.dlist-row.is-due-soon { background-image: linear-gradient(to right, var(--wrn-sub) 0, transparent 50px); }
.dlist-row.is-due-paid { background-image: linear-gradient(to right, var(--suc-sub) 0, transparent 50px); }

/* Nothing is needed for the stacked layout: the row is one box there too,
   so the stripe runs down the side of the whole card by itself. */

/* An icon-only control: the glyph, nothing around it, and the word still
   there for anybody who cannot see it. */
.icobtn { display: inline-grid; place-items: center; padding: .3rem .45rem; }
.ico-dl { width: 1.05rem; height: 1.05rem; display: block; }

.dlist-row .tin { width: 100%; }
/* A rate typed inside a row: wide enough for "12.5" and no wider, so the
   figures beside it keep their column. The form is inline, so the box and
   its button sit on one line. */
.dlist-row .tin.tiny { width: 4.5rem; text-align: right; }
.dlist-row form.inline { display: inline-flex; gap: .3rem; align-items: center; }
.dlist-row textarea.tin { min-height: 2.4rem; }

/* --- auth screen -----------------------------------------------------
   .plainbox is the old name and still works; .authbox is the new one.
   ------------------------------------------------------------------- */

/* The honeypot of a public form: off-screen rather than display:none, which a
   bot that reads computed styles would skip. */
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Cards stacked on a bare page (the sign-in window, the device approval) sit
   in a plain block, not in .content's gap, and touched each other. */
.authbox .card + .card { margin-top: var(--gap); }
body.plain { display: grid; place-items: center; min-height: 100vh; padding: 1.5rem; }
.plainbox, .authbox { width: 100%; max-width: 25rem; }
.plainbox .card, .authbox .card { box-shadow: var(--sh-lg); }
.authbox .brand {
   display: flex; align-items: center; justify-content: center; gap: .625rem;
   margin-bottom: 1.5rem;
   color: var(--ink); font-size: 1.2rem; font-weight: 600; text-decoration: none;
}
.authbox .brand .logo {
   width: 2.1rem; height: 2.1rem;
   display: grid; place-items: center;
   background: var(--pri); color: var(--on-pri);
   border-radius: var(--r-lg);
   font-size: .64rem; font-weight: 700; letter-spacing: -.01em;
   overflow: hidden;
}
/* The door to the predecessor, at the top of the login screen for as long
   as the changeover lasts. Deliberately quieter than the login card it sits
   above -- it is the way OUT of this system, and should not look like the
   main business of the page. */
.oldsys { --card-pad: 1.25rem; }
.oldsys-mark {
   display: block; margin: 0 auto .5rem;
   width: 3.25rem; height: 3.25rem;
   border-radius: var(--r-sm, .375rem);
}
.oldsys .hint { margin-bottom: 0; }

.plainbox .foot, .authbox .foot {
   margin-top: 1.25rem; text-align: center;
   color: var(--muted); font-size: var(--fs-sm);
}
.authbox .foot a { color: var(--pri); }

/* --- the partner's doorway tiles -------------------------------------
   /partnersite/{partner} is one address handed to one partner, and its page
   is a tile per thing they may do: the predecessor's picture, a heading and
   a subtitle, as it did it (the two photographs are its own files).

   ONE COLUMN, because the box it sits in is .authbox at 25rem and because
   the owner judges these screens on a telephone standing at a bay. A tile
   is a whole row and the whole row is the target: a 44px heading inside a
   card is a link people miss, and the thing being chosen here is "which of
   the two am I here for".
   -------------------------------------------------------------------- */

.doorways { display: grid; gap: .75rem; }
.doorway {
   display: flex; align-items: center; gap: 1rem;
   padding: 1.1rem 1.25rem;
   color: var(--ink); text-decoration: none;
   /* The .card recipe, verbatim: a tile here IS a card, and the lit edge is
      an inset shadow rather than a border for the reason .card gives --
      a border grows the box. */
   background: var(--glass-card);
   background-image: var(--card-sheen);
   border-radius: var(--r);
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge),
               inset 0 0 0 1px var(--glass-line);
   transition: box-shadow var(--speed), transform var(--speed);
}
.doorway:hover {
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge),
               inset 0 0 0 1px var(--pri);
}
.doorway:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.doorway:active { transform: translateY(1px); }
/* The picture tile, side by side. The photograph fills the card and the text
   rides on it in a frosted panel -- the one place a card DOES get a
   backdrop-filter, because here something real sits behind it (the card rule
   explains why a card over the smooth page ground does not need one). The
   panel is --glass, .84 opaque, so the heading keeps its contrast whatever
   the photograph is doing underneath. Type is larger than the site's body
   size on purpose: this page is read standing at a bay, on a telephone. */
.authbox.authbox-wide { max-width: 46rem; }
.authbox-wide .doorways {
   grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
   gap: 1rem;
}
.doorway { position: relative; display: block; padding: 0; overflow: hidden; }
.doorway-pic {
   display: block; width: 100%; height: auto;
   aspect-ratio: 4 / 3; object-fit: cover;
   transition: transform .4s ease;
}
.doorway:hover .doorway-pic { transform: scale(1.04); }
.doorway-text {
   position: absolute; left: .6rem; right: .6rem; bottom: .6rem;
   min-width: 0; padding: .8rem 1rem .9rem;
   background: var(--glass);
   -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
           backdrop-filter: blur(var(--glass-blur)) saturate(1.5);
   border-radius: var(--r);
   box-shadow: inset 0 1px 0 0 var(--glass-edge), inset 0 0 0 1px var(--glass-line);
}
.doorway-name {
   display: block;
   font-size: 1.35rem; font-weight: 600; line-height: 1.25;
   overflow-wrap: anywhere;
}
.doorway-sub {
   display: block; margin-top: .3rem;
   color: var(--muted); font-size: 1.1rem; line-height: 1.35;
   overflow-wrap: anywhere;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
   .doorway-text { background: var(--paper); }
}
@media (prefers-reduced-transparency: reduce) {
   .doorway-text { background: var(--paper); -webkit-backdrop-filter: none; backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
   .doorway-pic { transition: none; }
   .doorway:hover .doorway-pic { transform: none; }
}
/* The two forms behind the tiles (booking, cleaning report). Same reason as
   the tiles: read on a telephone at a bay. 17px in a field also keeps iOS
   from zooming the page when one is focused. A select gets an explicit
   height -- a native one crops its own text when the font grows without it. */
.authbox:has(.pform) { max-width: 32rem; }
.pform h1 { font-size: 1.9rem; }
.pform .lead { font-size: 1.2rem; }
.pform label, .pform .lbl { font-size: 1.1rem; font-weight: 500; }
.pform input[type=text], .pform input[type=email], .pform input[type=tel],
.pform select, .pform textarea { font-size: 1.0625rem; padding: .6rem .9rem; }
.pform input[type=text], .pform input[type=email], .pform input[type=tel],
.pform select { height: 3.1rem; }
.pform select { padding-right: 2.4rem; }
.pform textarea { min-height: 8rem; }
.pform .hint, .pform .field .hint { font-size: 1rem; line-height: 1.4; }
.pform .pick > span { font-size: 1.05rem; min-height: 2.9rem; }
.pform .btn.lg { font-size: 1.15rem; padding: .8rem 1.6rem; }
.pform .row2 { grid-template-columns: 1fr; }
.pshome h1 { font-size: 1.9rem; }
.pshome .lead { font-size: 1.2rem; }
.pshome .hint { font-size: 1.05rem; line-height: 1.45; }


/* =========================================================================
   5. UTILITIES
   -------------------------------------------------------------------------
   Small on purpose. If you find yourself needing a fifth spacing helper,
   the component is missing a rule -- add it in section 4 instead.
   ========================================================================= */

.sub, .muted { color: var(--muted); font-size: var(--fs-sm); }
.lead        { color: var(--body); font-size: 1.02rem; }
.right       { text-align: right; }
.center      { text-align: center; }
.nowrap      { white-space: nowrap; }
.truncate    { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Text somebody typed into a textarea, shown as they typed it. Used by the
   cleaning-report list, where a complaint about three rooms is three lines
   and running them together loses the only structure it had. Not on the
   .dlist cell itself: those are flex containers and a newline in an
   anonymous flex item is not reliably a newline. */
.pre         { white-space: pre-wrap; overflow-wrap: anywhere; }

.flex        { display: flex; align-items: center; gap: .5rem; }
.flex-between{ display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.spacer      { flex: 1 1 auto; }

/* A value that did not come from this system -- the payroll bureau's own
   word for a workplace, where we have never posted the person anywhere.
   Dimmer than .sub, and carrying a star, so it cannot be mistaken for one
   of our washpoints. */
.dim { opacity: .65; font-style: italic; }

.mt-0 { margin-top: 0; }      .mb-0 { margin-bottom: 0; }
.mt-1 { margin-top: .5rem; }  .mb-1 { margin-bottom: .5rem; }
.mt-2 { margin-top: 1rem; }   .mb-2 { margin-bottom: 1rem; }
.mt-3 { margin-top: 1.5rem; } .mb-3 { margin-bottom: 1.5rem; }

.hide { display: none !important; }
.sr {
   position: absolute; width: 1px; height: 1px;
   padding: 0; margin: -1px; overflow: hidden;
   clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The old content container, kept so that anything still using it lays out
   sensibly rather than going full-bleed. */
.wrap { max-width: 90rem; margin: 0 auto; padding: var(--gap); }


/* =========================================================================
   6. STYLE GUIDE
   -------------------------------------------------------------------------
   Presentation for /ui only. It lives here rather than in its own file so
   that the guide cannot load a stylesheet the application does not: if these
   rules work and the page still looks wrong, the fault is in section 4.
   ========================================================================= */

.demo {
   padding: 1.5rem;
   background: var(--paper-2);
   border: 1px dashed var(--line);
   border-radius: var(--r) var(--r) 0 0;
}
.demo + .demo-code {
   margin: 0 0 1.5rem;
   padding: 1rem 1.25rem;
   background: var(--ink);
   border-radius: 0 0 var(--r) var(--r);
   overflow-x: auto;
}
.demo-code code {
   background: none; border: 0; padding: 0;
   color: #d8dee9;
   font-size: var(--fs-xs);
   line-height: 1.65;
   white-space: pre;
}
.swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr)); gap: .75rem; }
.swatch { border-radius: var(--r); overflow: hidden; box-shadow: var(--sh-xs); background: var(--paper); }
.swatch .chip { height: 3.25rem; }
.swatch .meta { padding: .5rem .65rem; font-size: var(--fs-xs); }
.swatch .meta b { display: block; color: var(--ink); font-weight: 500; }
.swatch .meta span { color: var(--muted); font-family: var(--font-mono); }


/* =========================================================================
   7. ERROR PAGES
   -------------------------------------------------------------------------
   Rendered by \C\error into the layout named by core.ini [error].layout.
   ========================================================================= */

.card.center { text-align: center; }
.card .code {
   margin: 0 0 .25rem;
   color: var(--pri);
   font-size: 4rem;
   font-weight: 600;
   line-height: 1;
   letter-spacing: -.03em;
}
.actions.center { justify-content: center; }
.left { text-align: left; }


/* =========================================================================
   7b. COLUMN ICONS
   -------------------------------------------------------------------------
   One mechanism for both places a column name appears: the header row of a
   wide list, and the label in front of each value once the list stacks. The
   header is display:none when stacked, so an icon put only there would be
   invisible on exactly the screen where space is tightest.

   Drawn with mask-image rather than an <img> or an inline <svg>: it inherits
   currentColor so it themes itself, it needs no change to the row markup
   beyond one attribute, and CSS generated content cannot hold an element
   anyway -- content: attr() can only produce text.

   A column with no icon here keeps its words. The ones deliberately left as
   words are the abstract columns ("Mire szol", "Valojaban", "Kioszthatja?"),
   where a picture is a riddle and a word is an answer.

   Every icon-only header still carries its name in a .vh span, and a title,
   so a screen reader and a hover both read the column out in full.
   ========================================================================= */

.vh {
   position: absolute; width: 1px; height: 1px;
   margin: -1px; padding: 0; border: 0;
   clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* --ico is defined here as well as per key, so that an icon name with no
   rule of its own degrades to a neutral dot. Without it mask: var(--ico)
   resolves to nothing, the mask is dropped, and the element paints as a
   solid 16px block of currentColor -- which is exactly what a missing icon
   looked like the first time one went astray. */
[data-icon]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='3.4' fill='black'/></svg>"); }

.dlist-head > *[data-icon]::before {
   content: '';
   display: inline-block;
   width: 1rem; height: 1rem;
   background: currentColor;
   -webkit-mask: var(--ico) center / contain no-repeat;
           mask: var(--ico) center / contain no-repeat;
   vertical-align: -.18em;
}
.dlist-head > *[data-icon] { color: var(--muted); }

/* ...and on the VALUE cells too, not only the header.
   
   The stacked layout has always put the column's icon in front of its value,
   because there is no header to look up to. Wide, the eye had to travel back
   to the top of the list to remember which column it was in -- so the same
   mark now sits in front of the value at every width, which is what the
   owner asked for: "like on mobile".
   
   Quieter than the header's: this is a repeated mark down a column and it
   must not compete with the value beside it.
   
   The stacked rules further down redefine both of these and come later in
   the file, so they still win where the list has reflowed. */
.dlist-row > *[data-icon]::before {
   content: '';
   display: inline-block;
   flex: none;
   width: .85rem; height: .85rem;
   margin-inline-end: .35rem;
   /* The brand's green rather than the text colour at 40%. A grey mark beside
      grey text is furniture the eye steps over; a coloured one is a landmark
      it can aim at -- which is what an icon on a stacked row is FOR, since
      there is no column heading left to say what you are reading.
      --pri, so it follows the theme: the dark palette's primary is a light
      green, and a hardcoded dark one would disappear into the card. */
   background: var(--pri);
   opacity: .8;
   -webkit-mask: var(--ico) center / contain no-repeat;
           mask: var(--ico) center / contain no-repeat;
   vertical-align: -.08em;
}
/* A column of figures reads down its digits, so the mark goes on the far
   side of the number rather than between the label and it. */
.dlist-row > *[data-icon].right::before { margin-inline: 0 .35rem; }

/* ...and the cell holding one must not wrap its flex lines.

   A wrapping flex container collects items into lines by their MAX-content
   width and only shrinks them afterwards, so a value longer than the space
   left beside the icon was pushed onto a second line whole -- leaving the
   icon sitting alone on the first one. Every long partner name and wash-type
   list in the wash list read that way: a green mark, then a blank line, then
   the words. Without wrapping the value stays beside its mark and wraps
   inside itself, which is what it already does everywhere else (the cell
   carries overflow-wrap: anywhere and min-width: 0). The stacked layout sets
   display:grid on these cells further down, where flex-wrap does nothing. */
.dlist-row > *[data-icon] { flex-wrap: nowrap; }

/* lucide circle-plus, for the one button at a washpoint. Same construction as
   every other entry here: a mask, so it takes the colour it is set in. */
[data-icon="addwash"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><path d='M8 12h8'/><path d='M12 8v8'/></svg>"); }
[data-icon="name"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M4 7h16M4 12h10M4 17h7'/></svg>"); }
[data-icon="status"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='8'/><circle cx='12' cy='12' r='3' fill='black'/></svg>"); }
[data-icon="level"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M5 19V11M12 19V5M19 19v-6'/></svg>"); }
[data-icon="email"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='5' width='18' height='14' rx='2'/><path d='m3 7 9 6 9-6'/></svg>"); }
[data-icon="address"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M12 21s7-5.6 7-11a7 7 0 1 0-14 0c0 5.4 7 11 7 11Z'/><circle cx='12' cy='10' r='2.5'/></svg>"); }
[data-icon="calendar"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='5' width='18' height='16' rx='2'/><path d='M8 3v4M16 3v4M3 10h18'/></svg>"); }
[data-icon="time"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='8'/><path d='M12 7.5V12l3 2'/></svg>"); }
[data-icon="expires"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M7 3h10M7 21h10M8 3c0 4 8 5 8 9s-8 5-8 9'/></svg>"); }
[data-icon="granted"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='5' width='18' height='16' rx='2'/><path d='M3 10h18M8 3v4M16 3v4M9 15l2 2 4-4'/></svg>"); }
[data-icon="valid"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='5' width='18' height='16' rx='2'/><path d='M3 10h18M8 3v4M16 3v4'/></svg>"); }
[data-icon="ip"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='8'/><path d='M4 12h16M12 4c2.5 2.6 2.5 12.8 0 16M12 4c-2.5 2.6-2.5 12.8 0 16'/></svg>"); }
[data-icon="account"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9'/><circle cx='12' cy='10' r='3'/><path d='M6.2 18.5a6.5 6.5 0 0 1 11.6 0'/></svg>"); }
[data-icon="who"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='8' r='3.5'/><path d='M5 20c0-3.6 3.1-5.5 7-5.5s7 1.9 7 5.5'/></svg>"); }
[data-icon="role"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3 5 6v6c0 4.4 3 7.7 7 9 4-1.3 7-4.6 7-9V6l-7-3Z'/></svg>"); }
[data-icon="permissions"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='8' cy='12' r='3.5'/><path d='M11.5 12H21M18 12v3M15 12v2'/></svg>"); }
[data-icon="network"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='5' r='2.5'/><circle cx='5' cy='19' r='2.5'/><circle cx='19' cy='19' r='2.5'/><path d='M12 7.5 6.5 16.8M12 7.5l5.5 9.3M7.5 19h9'/></svg>"); }
[data-icon="sites"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M12 3s6 6.4 6 10.5a6 6 0 0 1-12 0C6 9.4 12 3 12 3Z'/></svg>"); }
[data-icon="partner"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='7' width='18' height='13' rx='2'/><path d='M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2'/></svg>"); }
[data-icon="taxid"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M5 9h14M5 15h14M10 4 8 20M16 4l-2 16'/></svg>"); }
[data-icon="action"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='5' cy='12' r='1.6' fill='black'/><circle cx='12' cy='12' r='1.6' fill='black'/><circle cx='19' cy='12' r='1.6' fill='black'/></svg>"); }
[data-icon="link"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M10 14a4 4 0 0 0 5.7 0l3-3A4 4 0 0 0 13 5.3l-1.7 1.7'/><path d='M14 10a4 4 0 0 0-5.7 0l-3 3A4 4 0 0 0 11 18.7l1.7-1.7'/></svg>"); }
[data-icon="scope"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='8'/><circle cx='12' cy='12' r='3.2'/><path d='M12 2v3M12 19v3M2 12h3M19 12h3'/></svg>"); }
[data-icon="contracts"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z'/><path d='M14 3v4h4M8 13h8M8 17h5'/></svg>"); }
/* Read and unread, as the two states of one object: a closed envelope and
   an opened one. Lucide's `mail` and `mail-open`. The pair is the point --
   a single icon that appears only on unread rows reads as a flag somebody
   set, while two states of the same shape read as a fact about the
   letter. */
[data-icon="mail-unread"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'><rect x='2' y='4' width='20' height='16' rx='2'/><path d='m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7'/></svg>"); }
[data-icon="mail-read"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M21.2 8.4c.5.38.8.97.8 1.6v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8c0-.63.3-1.22.8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z'/><path d='m22 10-9 5.7a2 2 0 0 1-2.06 0L2 10'/></svg>"); }

/* lucide paperclip, for the mail list.

   Painted by a rule of its OWN rather than by the .dlist-row cell rules
   above, which reach a direct child of the row only: this mark sits INSIDE
   the subject cell, in front of the link, because it describes the row and
   does not label a column. Same construction as every other icon here -- a
   mask, so it takes the colour it is set in. */
[data-icon="clip"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48'/></svg>"); }

/* lucide star, filled -- a starred letter on the mail list. Filled rather
   than outlined because it is a state and not a control: the thing that
   turns it on and off is a button on the message page. */
[data-icon="star"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M11.5 2.8a.6.6 0 0 1 1 0l2.4 5 5.4.8a.6.6 0 0 1 .3 1l-3.9 3.8.9 5.4a.6.6 0 0 1-.9.6L12 16.9l-4.8 2.5a.6.6 0 0 1-.9-.6l.9-5.4-3.9-3.8a.6.6 0 0 1 .3-1l5.4-.8z'/></svg>"); }

/* The star keeps the clip's geometry -- same size, same gap, same baseline
   -- so a row with both reads as two marks on one line rather than two
   different ideas. Its own colour, though: starring is somebody's own mark
   on a letter and the brand green is what every other icon here uses. */
.clip.star::before { background: var(--wrn); opacity: 1; }

/* The opening of a letter belongs to the PHONE's two-line row and nowhere
   else: wide, the list has a subject column and a date column and the
   first hundred characters of the body would push both around. It is in
   the markup at every width because the stylesheet cannot go and fetch it,
   so it is hidden here and shown in the stacking query. */
.mprev { display: none; }
.mmarks:empty { display: none; }

/* The bar above the mail list: mailbox, folder, search, scope and the button on
   ONE line, wrapping only when the screen is genuinely too narrow. Margins and
   not `gap`, which a flex container on an old WebView ignores. */
.mailbar { display: flex; flex-wrap: wrap; align-items: center; margin: 0 -.2rem -.4rem; }
.mailbar > * { margin: 0 .2rem .4rem; }
.mailbar > select,
.mailbar > input[type=search] { display: block; width: auto; min-width: 0; }
.mailbar > select { flex: 0 1 11rem; max-width: 100%; }
.mailbar > input[type=search] { flex: 1 1 10rem; }
.mailbar > .btn { flex: 0 0 auto; }
/* One message: the action bar and the sender block, compact. */
.mailtools { padding-top: .6rem; padding-bottom: .2rem; }
.mailtools .mtrow { display: flex; flex-wrap: wrap; align-items: center; margin: 0 -.2rem .4rem; }
.mailtools .mtrow > * { margin: 0 .2rem .4rem; }
.mailtools .mtrow > .spacer { flex: 1 1 0; min-width: 0; margin: 0; }
.mailtools select { display: block; width: auto; min-width: 0; max-width: 12rem; padding-top: .3rem; padding-bottom: .3rem; font-size: var(--fs-sm); }
.mailtools form.inline { display: inline-flex; align-items: center; }
.mailtools form.inline > select { margin-right: .3rem; }
.mailmeta { padding-top: .6rem; padding-bottom: .6rem; font-size: var(--fs-sm); }
.mailmeta > div { display: flex; padding: .15rem 0; }
.mailmeta .lbl { flex: 0 0 4.5rem; color: var(--muted); }
.mailmeta > div > span:last-child { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/* A phone cannot hold five controls on a line: the two pick-lists share the
   first, the search box and its button the second. */
@media (max-width: 40rem) {
   .mailbar > select { flex: 1 1 7rem; }
   .mailbar > input[type=search] { flex: 1 1 8rem; }
}

.clip::before {
   content: '';
   display: inline-block;
   flex: none;
   width: .85rem; height: .85rem;
   margin-inline-end: .3rem;
   background: var(--pri);
   opacity: .8;
   -webkit-mask: var(--ico) center / contain no-repeat;
           mask: var(--ico) center / contain no-repeat;
   vertical-align: -.08em;
}

[data-icon="owner"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='m4 18 1.6-9L10 13l2-8 2 8 4.4-4L20 18Z'/></svg>"); }

/* =========================================================================
   8. DATA LIST  (.dlist) -- replaces <table> for tabular data
   -------------------------------------------------------------------------
   Markup:

     <div class="dlistwrap">
       <div class="dlist" data-cols="4">
         <div class="dlist-head">
           <span>Szint</span><span>Név</span><span>Azonosító</span><span>Jog.</span>
         </div>
         <div class="dlist-row">
           <span data-label="Szint">...</span>
           ...
         </div>
       </div>
     </div>

   Two things this does that a <table> could not:

   1. It REFLOWS instead of scrolling. Below the breakpoint every row becomes a
      stacked block, each cell on its own line with its column name in front --
      taken from data-label, so the label is written once per cell and never
      drifts from the header.

   2. The breakpoint is a CONTAINER query, not a viewport one. The list stacks
      when the space it has is too narrow, which is the actual question. A list
      inside a narrow column stacks on a wide screen, and the rule keeps
      working if the layout around it changes.

   3. There is no single breakpoint, because there is no single answer. How
      much room a row needs depends on how many columns it has, so the
      threshold is written once per column count below.

   .dlistwrap carries the containment. It is deliberately NOT on .card: a
   container establishes a containing block for fixed-position descendants, and
   a modal inside a card would stop covering the viewport.
   ========================================================================= */

.dlistwrap {
   /* Named, so the width queries below cannot accidentally resolve against
      some other size container that turns up between here and the list. */
   container: dlw / inline-size;
   /* break out of the card's padding exactly as .tablewrap does */
   margin-inline: calc(var(--card-pad, 0rem) * -1);
   /* A list that cannot compress any further scrolls INSIDE ITSELF rather
      than pushing the page sideways -- the same contract .tablewrap has, and
      the one 36-design-system states for every piece of wide content. It was
      missing here: a column that will not shrink below its content, like an
      actions column holding a button, moved the whole document by the
      difference. Three pixels at 1100px is how it was noticed. */
   overflow-x: auto;
}
.card > .dlistwrap:first-child { margin-top: calc(var(--card-pad, 0rem) * -1); }
.card > .dlistwrap:last-child  { margin-bottom: calc(var(--card-pad, 0rem) * -1); }

/*
   .dlist is itself a container -- a STYLE one, carrying --stack. The width
   queries below set that flag on the list; everything the stacked form needs
   to change lives on the list's descendants, so one style query can restyle
   the whole thing without the stacked rules having to be written out once per
   column count. container-type stays "normal": a style container establishes
   no containing block and no containment, so the modal caveat above does not
   apply to it.
*/
.dlist {
   container-name: dl;
   display: grid;
   grid-template-columns: var(--dl-tpl, repeat(var(--cols, 4), auto));
   font-size: var(--fs-sm);
   --dl-x: 1.5rem;   /* the cells' inline padding; tightened when it is snug */
}

/* Column count. Declared with an attribute rather than an inline style so the
   views stay free of CSS. */
.dlist[data-cols="1"] { --cols: 1; }  .dlist[data-cols="2"] { --cols: 2; }
.dlist[data-cols="3"] { --cols: 3; }  .dlist[data-cols="4"] { --cols: 4; }
.dlist[data-cols="5"] { --cols: 5; }  .dlist[data-cols="6"] { --cols: 6; }
.dlist[data-cols="7"] { --cols: 7; }  .dlist[data-cols="8"] { --cols: 8; }

/* subgrid is what keeps every row's columns aligned without any width being
   declared anywhere: each row borrows the tracks the list already worked out. */
.dlist-head,
.dlist-row {
   display: grid;
   grid-column: 1 / -1;
   grid-template-columns: subgrid;
   align-items: center;
}

.dlist-head > * {
   padding: .75rem var(--dl-x);
   /* An empty header cell -- the one above an actions column -- has no line
      box, so under align-items:center it was a 24px stub and the header band
      visibly broke off before the last column. Stretching it costs nothing
      when every header is one line, which is the normal case. */
   align-self: stretch;
   background: var(--paper-2);
   border-bottom: 1px solid var(--line);
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-transform: uppercase;
   letter-spacing: .04em;
   white-space: nowrap;
}
.dlist-row > * {
   padding: .8rem var(--dl-x);
   border-bottom: 1px solid var(--line-soft);
   color: var(--body);
   min-width: 0;
   /* a long unbroken value wraps rather than widening the whole list */
   overflow-wrap: anywhere;
   /* STRETCH, and centre the content inside instead.
      The row is align-items:center, so each cell used to be exactly as tall
      as its own content -- which is invisible while every cell is one line
      of text, and obvious the moment one holds a camera thumbnail: that
      cell's bottom border sat below its neighbours' and the hover band was
      ragged. Stretched cells all end on the same line and the flex centring
      keeps the text where it was. */
   align-self: stretch;
   display: flex;
   align-items: center;
   flex-wrap: wrap;
   gap: .1rem .4rem;
   align-content: center;
}
/* An unread letter, in the two ways every mail client has said it for
   thirty years: the row is bold and the envelope beside it is closed.

   Both, and not one of them. The weight is what the eye catches scanning a
   list of thirty; the envelope is what says WHY this row is heavier, and on
   a stacked row -- where the column headings are gone and there is no
   neighbouring row at normal weight to compare against -- it is the only
   one of the two that still works. */
.dlist-row.is-new > * { font-weight: 600; color: var(--ink); }
.dlist-row.is-new > *[data-icon]::before { opacity: 1; }

.dlist-row:last-child > * { border-bottom: 0; }
.dlist-row:hover > * { background: var(--paper-2); }

/* --- the column a table is actually about --------------------------
   A franchise month is eight columns wide and only two of them are the
   question: what the washes were worth, and what the invoice said. Marked
   .key, they carry the weight and a tint of the page behind them, so the
   pair reads as one comparison while scanning -- and on a phone, where the
   row is stacked and the headings are gone, the tint is the only thing
   left that says which two lines to read first. */
.dlist-head > .key { color: var(--ink); }
.dlist-row > .key {
   font-weight: 600;
   color: var(--ink);
   background: var(--paper-2);
}
.dlist-row:hover > .key { background: var(--pri-sub); }

/* --- a row that opens something ------------------------------------
   Set by app.js, never by a view: it marks the rows where exactly one
   plain link was found, which is the same test the click handler uses.
   A class and not a :has() selector because the rule is "exactly one",
   and CSS can ask whether a link is present but not how many.

   The pointer is the whole affordance. The row already lights up on
   hover, the link inside it already looks like a link, and a second
   highlight would only say the same thing louder. */
.dlist-row.is-link { cursor: pointer; }
.dlist-row.is-link:hover a:not(.btn) { color: var(--pri); }

.dlist-row > strong, .dlist-row a { color: var(--ink); }
.dlist-row a:hover { color: var(--pri); }

/* --- a second destination inside a row that already has one ---------
   The invoice register's rows open the viewer, and the counterparty's
   name inside them goes somewhere else entirely: this same register
   narrowed to that supplier or partner. The rule above is deliberately
   ink-coloured, because in most lists the only link IS the row -- so
   this one has to say for itself that it is a link, and say it WITHOUT
   a hover: these screens get read on a phone at a bay, where there is
   no such thing.

   A dotted underline rather than a colour, so the column still reads as
   a column of names and not as a column of buttons.
   ------------------------------------------------------------------ */
.dlist-row a.party {
   text-decoration: underline dotted var(--muted);
   text-underline-offset: .2em;
}
.dlist-row a.party:hover { color: var(--pri); text-decoration-style: solid; }
/*
   Right alignment moves the CONTENT, never the box.

   justify-self:end shrinks a grid item to its content width, so a right
   aligned cell stopped painting its own background partway across the track:
   the header band broke off before the last column and left it floating on
   its own. Aligning the text inside a full-width cell gives the same result
   and keeps the row continuous.
*/
.dlist .right { text-align: right; justify-content: flex-end; }
.dlist .num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

.card > .dlistwrap:last-child .dlist-row:last-child > *:first-child { border-bottom-left-radius: var(--r); }
.card > .dlistwrap:last-child .dlist-row:last-child > *:last-child  { border-bottom-right-radius: var(--r); }

/* --- when to stack -------------------------------------------------
   How much room a row needs depends on how many columns it has, so the
   threshold is written once per column count rather than once for the site.

   The old single 48rem breakpoint was measured wrong. At 1280px a listing
   card is 60.8rem wide and gets rows -- but at 1024px, a laptop, the sidebar
   takes its 16.25rem and the same card is only 44.8rem. Every listing on the
   site was therefore stacking on a laptop, one label-and-value pair per line,
   which is most of why the dashboard looked so much thinner than it is. Two
   cards side by side at 1280px come out at 29.6rem and had the same problem.

   So four columns and fewer now keep real rows down to 28rem -- narrow enough
   to cover a half-width card, wide enough that a 360px phone (20.5rem) still
   stacks. Wider lists need proportionally more room before rows are worth
   having: seven columns crammed into 46rem is not a table, it is one letter
   per line, which was checked rather than assumed.

        cols   stacks below      a 7-column list at 768px is still stacked,
        <= 4   28rem             and that is the right answer for it.
        5      34rem
        6      40rem
        7-8    48rem
   ------------------------------------------------------------------- */

@container dlw (max-width: 28rem) { .dlist { --dl-tpl: 1fr; --stack: 1; } }
@container dlw (max-width: 34rem) { .dlist[data-cols="5"] { --dl-tpl: 1fr; --stack: 1; } }
@container dlw (max-width: 40rem) { .dlist[data-cols="6"] { --dl-tpl: 1fr; --stack: 1; } }
@container dlw (max-width: 48rem) {
   .dlist[data-cols="7"],
   .dlist[data-cols="8"] { --dl-tpl: 1fr; --stack: 1; }
}

/* Between stacking and comfortable, buy the columns some width back from the
   gutters rather than letting every value wrap. */
@container dlw (max-width: 44rem) { .dlist { --dl-x: .875rem; } }

/* --- stacked: one row per block, each cell labelled -----------------
   Keyed off --stack, not off a width, so the rules are written once and the
   list itself decides which of the thresholds above applied to it. */

/* A wash type list breaks BETWEEN names, never before the comma. As two
   sibling spans the comma was its own inline box and could land alone at the
   start of a line; as a ::before on the following name it travels with the
   name it precedes. Outside the stacking query because the wide table draws
   the same separator. */
.wname + .wname::before { content: ', '; color: var(--muted); }

/* ONE flex item, not one per wash type. A cell is a flex row of icon and
   value with a gap between them, so loose spans inside it each became an item
   and the gap landed before every comma -- "GYORS külső mosás , HACCP". The
   names are inline text and belong in one box. */
.wnames { min-width: 0; }

@container dl style(--stack: 1) {
   .dlist-head { display: none; }

   .dlist-row {
      /* FIXED TRACKS, with an escape for the one value that needs the line.
      
         Flowing them read beautifully on a row of short values and fell
         apart the moment a partner turned up: "WSZL kft. /// Waberer's
         Szemerédi..." took 232px, everything after it landed somewhere
         different from the row above, and a list whose columns move every
         line is a list nobody can scan down.
      
         So two equal tracks, and they are sized for the two values that
         actually set the width -- two plates ("AADW402, WCZ395") and a
         six-figure price ("123 456 Ft"). Everything else is shorter than
         both. A wash type is longer than both and spans the pair instead;
         see below. */
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      align-items: baseline;
      column-gap: .75rem;
      row-gap: .1rem;
      /* THE INSET LIVES ON THE BLOCK, not on each cell inside it.
         It used to be on the cells, which meant a 414px phone spent 48px of
         every 189px track on padding that was only there to line the values
         up with the card edge -- and the alignment it bought cost the width
         that made "Személygépkocsi" and "2026-10-06 20:00" wrap onto second
         lines. Measured: a row was 119px tall for four short values. Put the
         same inset on the block and the alignment is identical while each
         cell gets its 48px back. */
      padding: .35rem var(--dl-x);
      border-bottom: 1px solid var(--line);
   }
   .dlist-row:last-child { border-bottom: 0; }

   /* The hover follows the inset: with the padding on the block, a highlight
      drawn on the cells would stop short of both edges. */
   .dlist-row:hover { background: var(--paper-2); }
   .dlist-row:hover > * { background: transparent; }

   .dlist-row > * {
      /* A GRID of icon and value, not a flex row.
      
         As flex the value was an item that sized itself to its own content
         and pushed the icon off the line when it was long: a partner's name
         ran past the card's edge with its briefcase stranded on the line
         above it. A track of minmax(0, 1fr) cannot be pushed out of -- the
         value wraps or clips inside it, and the icon stays where it was put. */
      display: grid;
      grid-template-columns: max-content minmax(0, 1fr);
      align-items: baseline;
      /* .45rem between an icon and its value: at this width every 16px is
         most of a word. */
      gap: .45rem;
      min-width: 0;
      padding: .2rem 0;
      border-bottom: 0;
      text-align: left;
   }

   /* A cell with no icon has nothing to put in the first track. */
   .dlist-row > *:not([data-icon]) { grid-template-columns: minmax(0, 1fr); }

   /* .withico declares display:flex further down this sheet at the SAME
      specificity as the rule above, so source order handed the car-type cell
      back its flex box -- and with it the stacking that rule exists to stop:
      "Tehergépjármű 12t felett" sat under its own lorry instead of beside it.
      Named explicitly so it wins on specificity rather than on position. */
   .dlist-row > *.withico {
      display: grid;
      grid-template-columns: max-content minmax(0, 1fr);
      align-items: baseline;
      gap: .45rem;
   }

   /* A list of plates is one line or it is nothing: wrapped, it doubles the
      row's height for the one wash in ten that has two. Three plates still
      do not fit anywhere, so it ends in an ellipsis rather than running out
      over the card's edge -- the wash's own page has all of them. */
   .dlist-row .plate > :last-child,
   .dlist-row .plate { overflow: hidden; text-overflow: ellipsis; }

   /* A date is one thing and must read as one line. The year is dropped
      because at this width it is the least informative four characters on
      the row -- the predecessor drops it too, and nobody ever looked at a
      washlist wondering which year. */
   .dlist-row .ydrop { display: none; }
   /* The column name, written once on the cell it belongs to. */
   .dlist-row > *[data-label]::before {
      content: attr(data-label);
      color: var(--muted);
      font-size: var(--fs-xs);
      font-weight: 500;
   }
   /* A WORD label is six characters of furniture before the value starts, so
      a cell carrying one keeps the line to itself. An icon label costs a
      glyph and shares. */
   .dlist-row > *:not([data-icon]) { grid-column: 1 / -1; }

   /* THE ONE VALUE THAT IS LONGER THAN A TRACK.
      A wash type runs to four words and a wash can carry two of them. Given
      half a row it set "GYORS külső mosás, HACCP raktérfertőtlenítés" four
      lines deep beside a plate using a third of its own track. It takes the
      pair, which costs one line and reads in one. */
   .dlist-row > *:has(.wnames) { grid-column: 1 / -1; }

   /* ---------------------------------------- THE INVOICE REGISTER, stacked

      The generic stacked layout gives every cell a word label and most of
      them a line to themselves, which is right for a table somebody reads
      one row of. An invoice register is read fifty rows at a time looking
      for one, and that layout spent six lines and six grey captions --
      SZÁMLASZÁM, PARTNER, KELTE, HATÁRIDŐ, BRUTTÓ, ÁLLAPOT -- per invoice.
      The captions say nothing a number in a box and a date do not say
      themselves.

      So two lines and no captions:

          [VEDOX-2026-1219]  Kanizsa Szerviz Kft
          (cal) 2026-09-30 · 2026-10-15   12 700 Ft  (dl)

      The state pill is gone entirely. The row already carries its state as
      the gradient down its left edge, and the viewer behind the row says it
      in words -- a third copy, in a pill, cost a whole line.

      Explicit grid areas rather than flow: the four values on the second
      line are four separate cells, and letting them flow put the money
      somewhere different on every row. A register whose columns move each
      line is one nobody can scan. */
   .dlist.invreg .dlist-row {
      grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
      column-gap: .4rem;
   }

   .dlist.invreg .dlist-row > *::before { content: none; }
   .dlist.invreg .dlist-row > * { grid-template-columns: minmax(0, 1fr); }
   /* The one cell that keeps its glyph: it is the only thing left saying
      that the two numbers after it are dates. */
   .dlist.invreg .dlist-row > .inv-from {
      grid-template-columns: max-content minmax(0, 1fr);
   }
   .dlist.invreg .dlist-row > .inv-from::before { content: ""; }

   .dlist.invreg .dlist-row .inv-state { display: none; }

   .dlist.invreg .dlist-row .inv-num   { grid-area: 1 / 1 / 2 / 2; }
   .dlist.invreg .dlist-row .inv-party { grid-area: 1 / 2 / 2 / 5; }
   .dlist.invreg .dlist-row .inv-from  { grid-area: 2 / 1 / 3 / 2; }
   .dlist.invreg .dlist-row .inv-due   { grid-area: 2 / 2 / 3 / 3; }
   .dlist.invreg .dlist-row .inv-gross { grid-area: 2 / 3 / 3 / 4; justify-self: end; }
   .dlist.invreg .dlist-row .inv-pdf   { grid-area: 2 / 4 / 3 / 5; justify-self: end; }

   /* A DOT, not a dash: a dash between two dates reads as a range -- "from
      the 30th to the 15th" -- and these are two different facts about one
      invoice, written and due. */
   .dlist.invreg .dlist-row > .inv-due {
      /* Two tracks, or the dot below becomes a grid ITEM of its own and
         sits on the line above the date it belongs to. Every cell here is
         a grid -- that is what draws an icon beside a value -- so a
         ::before is a track, not an inline mark. */
      grid-template-columns: max-content minmax(0, 1fr);
      column-gap: .3rem;
   }

   .dlist.invreg .dlist-row .inv-due::before {
      content: "·";
      /* The alternative text is empty on purpose: a screen reader
         announcing "middle dot" between two dates is noise. */
      content: "·" / "";
      color: var(--muted);
   }

   /* The number earns its box; nothing else on the line competes with it. */
   .dlist.invreg .dlist-row .inv-num code { font-size: var(--fs-sm); }
   .dlist.invreg .dlist-row .inv-party { font-weight: 500; }
   .dlist.invreg .dlist-row .inv-from,
   .dlist.invreg .dlist-row .inv-due { color: var(--muted); font-size: var(--fs-sm); }
   /* No box around it either. A bordered button at the end of every row is
      fifty rectangles down the screen competing with the one mark that is
      meant to be seen -- the stripe at the other edge. The glyph alone is
      still a 24px target, which is what the padding is for. */
   .dlist.invreg .dlist-row .inv-pdf .icobtn {
      padding: .2rem;
      border: 0;
      background: transparent;
      color: var(--muted);
   }
   .dlist.invreg .dlist-row .inv-pdf .icobtn:active { color: var(--ink); }

   /* TWO ICONS FOR ONE FACT IS ONE TOO MANY.
      A car type that has its own drawing shows it; the generic car beside it
      says nothing the specific one does not, and at this size the pair reads
      as a single smudge. The column header still carries the generic -- there
      is no custom icon to use there. */
   .dlist-row > *[data-icon]:has(.tico)::before { content: none; }

   /* A partner's legal name runs to six words and is the one value here that
      is recognised from its first two. Cut it at the track rather than let it
      set the width of the whole list. */
   /* On the SPAN, not on the cell. An ellipsis clips the box it is set on,
      and the cell's box holds the icon AND the value as separate children --
      the name was one of them and sailed straight past it. */
   .dlist-row .pname > span {
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }

   .dlist-row > * > * { min-width: 0; }
   /* break-word, not anywhere: only break a word that genuinely cannot fit,
      rather than breaking every one at the track edge. */
   .dlist-row > * { overflow-wrap: break-word; }

   /* ...and where the column has an icon, that replaces the word. This has
      to come after the rule above: both selectors have the same specificity,
      so source order is what decides which ::before wins. */
   .dlist-row > *[data-icon]::before {
      content: '';
      display: inline-block;
      width: 1rem; height: 1rem;
      /* The brand's green, not grey. A grey mark beside grey text is
         furniture the eye steps over; a coloured one is a landmark it can aim
         at, which is what an icon on a stacked row is FOR -- there is no
         column heading left to say what you are reading. --pri follows the
         theme: the dark palette's primary is a LIGHT green, and a hardcoded
         dark one would disappear into the card. */
      background: var(--pri);
      opacity: .8;
      align-self: center;
      -webkit-mask: var(--ico) center / contain no-repeat;
              mask: var(--ico) center / contain no-repeat;
   }

   /* A cell with nothing in it is noise once it has a label in front of it. */
   .dlist-row > *:empty { display: none; }

   /* An actions column has no header text, so there is no label to show and
      no reason to reserve the label track for it. */
   .dlist-row > *[data-label=""] { grid-template-columns: 1fr; }
   .dlist-row > *[data-label=""]::before { content: none; }
   .dlist .right { text-align: left; }

   /* --- THE MAIL LIST READS AS TWO LINES, ON A PHONE ONLY ---------------

      Three labelled cells stacked down a phone is three lines and two
      words of furniture per letter: thirty of them is a page nobody
      scrolls. A mail client's row is two lines, and this is that row --
      sender and when it came on the first, what it is about on the
      second. Wide, the list keeps its three columns exactly as before;
      everything here is inside the same --stack flag every other list
      uses, so the two shapes are one piece of markup.

      Areas rather than order, so the DOM stays in the order it reads in:
      sender, subject, date. */
   .dlist.maillist .dlist-row {
      grid-template-columns: minmax(0, 1fr) max-content;
      grid-template-areas: "who when" "what what";
      align-items: center;
      column-gap: .5rem;
      row-gap: .05rem;
   }
   /* The cell padding the stacked layout gives every value is a third of
      a line each here, and there are four of them in a two-line row. */
   .dlist.maillist .dlist-row > * { padding: 0; }
   .dlist.maillist .dlist-row { padding-block: .45rem; }

   .dlist.maillist .dlist-row > .mwho  { grid-area: who; }
   .dlist.maillist .dlist-row > .mwhen { grid-area: when; }
   .dlist.maillist .dlist-row > .mwhat { grid-area: what; }

   /* No labels. "Feladó" in front of a sender's name says nothing the
      shape of the row does not -- which is the whole saving. */
   .dlist.maillist .dlist-row > .mwhen::before,
   .dlist.maillist .dlist-row > .mwhat::before { content: none; }
   .dlist.maillist .dlist-row > .mwhen { grid-template-columns: minmax(0, 1fr); }

   /* The second line is TWO tracks -- what it is about, and the marks at
      the end of it. Named at this weight deliberately: the stacked rules
      above give every cell one track or an icon track, and at a lighter
      weight they put the clip on a third line of its own. */
   .dlist.maillist .dlist-row > .mwhat {
      display: grid;
      grid-template-columns: minmax(0, 1fr) max-content;
      align-items: center;
      column-gap: .4rem;
      padding: 0;
   }

   /* The sender's name is the line, so it takes what is left and cuts
      rather than wrapping under its own envelope. */
   .dlist.maillist .mwho {
      min-width: 0;
      font-weight: 500;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }
   .dlist.maillist .mwho > * { overflow: hidden; text-overflow: ellipsis; }

   /* When it came, small and quiet at the end of the first line. */
   .dlist.maillist .mwhen {
      justify-self: end;
      white-space: nowrap;
      font-size: var(--fs-xs);
      color: var(--muted);
      padding: 0;
   }

   /* The second line: what it is about on the left, the marks on the
      right. One line, cut -- a subject and the opening of a letter is
      more than a phone has room for and the point is the first few
      words of each. */
   .dlist.maillist .mtext {
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }
   .dlist.maillist .mprev {
      display: inline;
      color: var(--muted);
      font-size: var(--fs-xs);
   }
   /* An em dash between the subject and the letter, so the two do not
      read as one sentence. */
   .dlist.maillist .mprev::before { content: ' — '; }
   .dlist.maillist .mmarks { display: inline-flex; gap: .25rem; align-items: center; }

   /* An unread letter is bolder, not merely tinted: the tint is a card
      background on a phone held in daylight. */
   .dlist.maillist .dlist-row.is-new .mwho,
   .dlist.maillist .dlist-row.is-new .mtext a { font-weight: 600; }

   /* --- THE SHIFT LIST READS AS TWO LINES, ON A PHONE ------------------

      Seven labelled values stacked down a phone is six or seven lines a shift.
      Here it is two: who and how long on the first, then where, from when to
      when, and the marks on the second. A flex row that wraps rather than a
      grid, so a long washpoint name or an overtime note pushes the marks to a
      third line instead of being cut. Margins and no `gap`: the tablet's
      WebView has none on flexbox. */
   .dlist.hourlist .dlist-row {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      padding: .45rem var(--dl-x);
   }
   .dlist.hourlist .dlist-row > * {
      display: block;
      padding: 0;
      margin: 0 .6rem 0 0;
      min-width: 0;
      text-align: left;
      order: 2;
   }
   .dlist.hourlist .dlist-row > *::before { content: none; }
   .dlist.hourlist .dlist-row::after { content: ''; order: 1; flex: 0 0 100%; height: 0; }
   .dlist.hourlist .dlist-row > .hwho { order: 0; flex: 1 1 0; font-weight: 500; margin-right: .6rem; }
   .dlist.hourlist .dlist-row > .hlen { order: 0; flex: 0 0 auto; margin-right: 0; font-weight: 500; }
   .dlist.hourlist .dlist-row > .hsite,
   .dlist.hourlist .dlist-row > .hstart,
   .dlist.hourlist .dlist-row > .hend,
   .dlist.hourlist .dlist-row > .hot,
   .dlist.hourlist .dlist-row > .hsrc { font-size: var(--fs-xs); color: var(--muted); }
   .dlist.hourlist .dlist-row > .hend::before { content: '\2013\00a0'; margin-left: -.35rem; }
   .dlist.hourlist .dlist-row > .hend .hsame { display: none; }
   .dlist.hourlist .dlist-row > .hot > .sub { display: none; }
   .dlist.hourlist .dlist-row > .hnil,
   .dlist.hourlist .hy { display: none; }
   .dlist.hourlist .pill { padding: .05rem .4rem; font-size: var(--fs-xs); }

}

/* --- the same stacked form, for engines that cannot do the above -----------
   The width queries and the --stack style query above need container queries,
   style queries and subgrid (Chrome 117). The bay tablet is Android 8.1 with a
   WebView from before any of them, and there the list drew its header cells
   as a stray column and ran the row's cells together. Where subgrid is
   missing, every list is stacked, always: a row of labelled values reads fine
   on a 7-inch screen, and it is the one form that needs none of the modern
   machinery.

   GENERATED from the block above by copying its body, so the two cannot drift
   apart in intent -- if the stacked rules change, copy them again. */
@supports not (grid-template-columns: subgrid) {
   .dlistwrap { margin-left: 0; margin-right: 0; }
   .dlist { display: block; --dl-x: .25rem; }
   .dlist-head { display: none; }


   .dlist-row {
      /* FIXED TRACKS, with an escape for the one value that needs the line.
      
         Flowing them read beautifully on a row of short values and fell
         apart the moment a partner turned up: "WSZL kft. /// Waberer's
         Szemerédi..." took 232px, everything after it landed somewhere
         different from the row above, and a list whose columns move every
         line is a list nobody can scan down.
      
         So two equal tracks, and they are sized for the two values that
         actually set the width -- two plates ("AADW402, WCZ395") and a
         six-figure price ("123 456 Ft"). Everything else is shorter than
         both. A wash type is longer than both and spans the pair instead;
         see below. */
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      align-items: baseline;
      column-gap: .75rem;
      row-gap: .1rem;
      /* THE INSET LIVES ON THE BLOCK, not on each cell inside it.
         It used to be on the cells, which meant a 414px phone spent 48px of
         every 189px track on padding that was only there to line the values
         up with the card edge -- and the alignment it bought cost the width
         that made "Személygépkocsi" and "2026-10-06 20:00" wrap onto second
         lines. Measured: a row was 119px tall for four short values. Put the
         same inset on the block and the alignment is identical while each
         cell gets its 48px back. */
      padding: .35rem var(--dl-x);
      border-bottom: 1px solid var(--line);
   }
   .dlist-row:last-child { border-bottom: 0; }

   /* The hover follows the inset: with the padding on the block, a highlight
      drawn on the cells would stop short of both edges. */
   .dlist-row:hover { background: var(--paper-2); }
   .dlist-row:hover > * { background: transparent; }

   .dlist-row > * {
      /* A GRID of icon and value, not a flex row.
      
         As flex the value was an item that sized itself to its own content
         and pushed the icon off the line when it was long: a partner's name
         ran past the card's edge with its briefcase stranded on the line
         above it. A track of minmax(0, 1fr) cannot be pushed out of -- the
         value wraps or clips inside it, and the icon stays where it was put. */
      display: grid;
      grid-template-columns: max-content minmax(0, 1fr);
      align-items: baseline;
      /* .45rem between an icon and its value: at this width every 16px is
         most of a word. */
      gap: .45rem;
      min-width: 0;
      padding: .2rem 0;
      border-bottom: 0;
      text-align: left;
   }

   /* A cell with no icon has nothing to put in the first track. */
   .dlist-row > *:not([data-icon]) { grid-template-columns: minmax(0, 1fr); }

   /* .withico declares display:flex further down this sheet at the SAME
      specificity as the rule above, so source order handed the car-type cell
      back its flex box -- and with it the stacking that rule exists to stop:
      "Tehergépjármű 12t felett" sat under its own lorry instead of beside it.
      Named explicitly so it wins on specificity rather than on position. */
   .dlist-row > *.withico {
      display: grid;
      grid-template-columns: max-content minmax(0, 1fr);
      align-items: baseline;
      gap: .45rem;
   }

   /* A list of plates is one line or it is nothing: wrapped, it doubles the
      row's height for the one wash in ten that has two. Three plates still
      do not fit anywhere, so it ends in an ellipsis rather than running out
      over the card's edge -- the wash's own page has all of them. */
   .dlist-row .plate > :last-child,
   .dlist-row .plate { overflow: hidden; text-overflow: ellipsis; }

   /* A date is one thing and must read as one line. The year is dropped
      because at this width it is the least informative four characters on
      the row -- the predecessor drops it too, and nobody ever looked at a
      washlist wondering which year. */
   .dlist-row .ydrop { display: none; }
   /* The column name, written once on the cell it belongs to. */
   .dlist-row > *[data-label]::before {
      content: attr(data-label);
      color: var(--muted);
      font-size: var(--fs-xs);
      font-weight: 500;
   }
   /* A WORD label is six characters of furniture before the value starts, so
      a cell carrying one keeps the line to itself. An icon label costs a
      glyph and shares. */
   .dlist-row > *:not([data-icon]) { grid-column: 1 / -1; }

   /* THE ONE VALUE THAT IS LONGER THAN A TRACK.
      A wash type runs to four words and a wash can carry two of them. Given
      half a row it set "GYORS külső mosás, HACCP raktérfertőtlenítés" four
      lines deep beside a plate using a third of its own track. It takes the
      pair, which costs one line and reads in one. */
   .dlist-row > *:has(.wnames) { grid-column: 1 / -1; }

   /* ---------------------------------------- THE INVOICE REGISTER, stacked

      The generic stacked layout gives every cell a word label and most of
      them a line to themselves, which is right for a table somebody reads
      one row of. An invoice register is read fifty rows at a time looking
      for one, and that layout spent six lines and six grey captions --
      SZÁMLASZÁM, PARTNER, KELTE, HATÁRIDŐ, BRUTTÓ, ÁLLAPOT -- per invoice.
      The captions say nothing a number in a box and a date do not say
      themselves.

      So two lines and no captions:

          [VEDOX-2026-1219]  Kanizsa Szerviz Kft
          (cal) 2026-09-30 · 2026-10-15   12 700 Ft  (dl)

      The state pill is gone entirely. The row already carries its state as
      the gradient down its left edge, and the viewer behind the row says it
      in words -- a third copy, in a pill, cost a whole line.

      Explicit grid areas rather than flow: the four values on the second
      line are four separate cells, and letting them flow put the money
      somewhere different on every row. A register whose columns move each
      line is one nobody can scan. */
   .dlist.invreg .dlist-row {
      grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
      column-gap: .4rem;
   }

   .dlist.invreg .dlist-row > *::before { content: none; }
   .dlist.invreg .dlist-row > * { grid-template-columns: minmax(0, 1fr); }
   /* The one cell that keeps its glyph: it is the only thing left saying
      that the two numbers after it are dates. */
   .dlist.invreg .dlist-row > .inv-from {
      grid-template-columns: max-content minmax(0, 1fr);
   }
   .dlist.invreg .dlist-row > .inv-from::before { content: ""; }

   .dlist.invreg .dlist-row .inv-state { display: none; }

   .dlist.invreg .dlist-row .inv-num   { grid-area: 1 / 1 / 2 / 2; }
   .dlist.invreg .dlist-row .inv-party { grid-area: 1 / 2 / 2 / 5; }
   .dlist.invreg .dlist-row .inv-from  { grid-area: 2 / 1 / 3 / 2; }
   .dlist.invreg .dlist-row .inv-due   { grid-area: 2 / 2 / 3 / 3; }
   .dlist.invreg .dlist-row .inv-gross { grid-area: 2 / 3 / 3 / 4; justify-self: end; }
   .dlist.invreg .dlist-row .inv-pdf   { grid-area: 2 / 4 / 3 / 5; justify-self: end; }

   /* A DOT, not a dash: a dash between two dates reads as a range -- "from
      the 30th to the 15th" -- and these are two different facts about one
      invoice, written and due. */
   .dlist.invreg .dlist-row > .inv-due {
      /* Two tracks, or the dot below becomes a grid ITEM of its own and
         sits on the line above the date it belongs to. Every cell here is
         a grid -- that is what draws an icon beside a value -- so a
         ::before is a track, not an inline mark. */
      grid-template-columns: max-content minmax(0, 1fr);
      column-gap: .3rem;
   }

   .dlist.invreg .dlist-row .inv-due::before {
      content: "·";
      /* The alternative text is empty on purpose: a screen reader
         announcing "middle dot" between two dates is noise. */
      content: "·" / "";
      color: var(--muted);
   }

   /* The number earns its box; nothing else on the line competes with it. */
   .dlist.invreg .dlist-row .inv-num code { font-size: var(--fs-sm); }
   .dlist.invreg .dlist-row .inv-party { font-weight: 500; }
   .dlist.invreg .dlist-row .inv-from,
   .dlist.invreg .dlist-row .inv-due { color: var(--muted); font-size: var(--fs-sm); }
   /* No box around it either. A bordered button at the end of every row is
      fifty rectangles down the screen competing with the one mark that is
      meant to be seen -- the stripe at the other edge. The glyph alone is
      still a 24px target, which is what the padding is for. */
   .dlist.invreg .dlist-row .inv-pdf .icobtn {
      padding: .2rem;
      border: 0;
      background: transparent;
      color: var(--muted);
   }
   .dlist.invreg .dlist-row .inv-pdf .icobtn:active { color: var(--ink); }

   /* TWO ICONS FOR ONE FACT IS ONE TOO MANY.
      A car type that has its own drawing shows it; the generic car beside it
      says nothing the specific one does not, and at this size the pair reads
      as a single smudge. The column header still carries the generic -- there
      is no custom icon to use there. */
   .dlist-row > *[data-icon]:has(.tico)::before { content: none; }

   /* A partner's legal name runs to six words and is the one value here that
      is recognised from its first two. Cut it at the track rather than let it
      set the width of the whole list. */
   /* On the SPAN, not on the cell. An ellipsis clips the box it is set on,
      and the cell's box holds the icon AND the value as separate children --
      the name was one of them and sailed straight past it. */
   .dlist-row .pname > span {
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }

   .dlist-row > * > * { min-width: 0; }
   /* break-word, not anywhere: only break a word that genuinely cannot fit,
      rather than breaking every one at the track edge. */
   .dlist-row > * { overflow-wrap: break-word; }

   /* ...and where the column has an icon, that replaces the word. This has
      to come after the rule above: both selectors have the same specificity,
      so source order is what decides which ::before wins. */
   .dlist-row > *[data-icon]::before {
      content: '';
      display: inline-block;
      width: 1rem; height: 1rem;
      /* The brand's green, not grey. A grey mark beside grey text is
         furniture the eye steps over; a coloured one is a landmark it can aim
         at, which is what an icon on a stacked row is FOR -- there is no
         column heading left to say what you are reading. --pri follows the
         theme: the dark palette's primary is a LIGHT green, and a hardcoded
         dark one would disappear into the card. */
      background: var(--pri);
      opacity: .8;
      align-self: center;
      -webkit-mask: var(--ico) center / contain no-repeat;
              mask: var(--ico) center / contain no-repeat;
   }

   /* A cell with nothing in it is noise once it has a label in front of it. */
   .dlist-row > *:empty { display: none; }

   /* An actions column has no header text, so there is no label to show and
      no reason to reserve the label track for it. */
   .dlist-row > *[data-label=""] { grid-template-columns: 1fr; }
   .dlist-row > *[data-label=""]::before { content: none; }
   .dlist .right { text-align: left; }

   /* --- THE MAIL LIST READS AS TWO LINES, ON A PHONE ONLY ---------------

      Three labelled cells stacked down a phone is three lines and two
      words of furniture per letter: thirty of them is a page nobody
      scrolls. A mail client's row is two lines, and this is that row --
      sender and when it came on the first, what it is about on the
      second. Wide, the list keeps its three columns exactly as before;
      everything here is inside the same --stack flag every other list
      uses, so the two shapes are one piece of markup.

      Areas rather than order, so the DOM stays in the order it reads in:
      sender, subject, date. */
   .dlist.maillist .dlist-row {
      grid-template-columns: minmax(0, 1fr) max-content;
      grid-template-areas: "who when" "what what";
      align-items: center;
      column-gap: .5rem;
      row-gap: .05rem;
   }
   /* The cell padding the stacked layout gives every value is a third of
      a line each here, and there are four of them in a two-line row. */
   .dlist.maillist .dlist-row > * { padding: 0; }
   .dlist.maillist .dlist-row { padding-block: .45rem; }

   .dlist.maillist .dlist-row > .mwho  { grid-area: who; }
   .dlist.maillist .dlist-row > .mwhen { grid-area: when; }
   .dlist.maillist .dlist-row > .mwhat { grid-area: what; }

   /* No labels. "Feladó" in front of a sender's name says nothing the
      shape of the row does not -- which is the whole saving. */
   .dlist.maillist .dlist-row > .mwhen::before,
   .dlist.maillist .dlist-row > .mwhat::before { content: none; }
   .dlist.maillist .dlist-row > .mwhen { grid-template-columns: minmax(0, 1fr); }

   /* The second line is TWO tracks -- what it is about, and the marks at
      the end of it. Named at this weight deliberately: the stacked rules
      above give every cell one track or an icon track, and at a lighter
      weight they put the clip on a third line of its own. */
   .dlist.maillist .dlist-row > .mwhat {
      display: grid;
      grid-template-columns: minmax(0, 1fr) max-content;
      align-items: center;
      column-gap: .4rem;
      padding: 0;
   }

   /* The sender's name is the line, so it takes what is left and cuts
      rather than wrapping under its own envelope. */
   .dlist.maillist .mwho {
      min-width: 0;
      font-weight: 500;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }
   .dlist.maillist .mwho > * { overflow: hidden; text-overflow: ellipsis; }

   /* When it came, small and quiet at the end of the first line. */
   .dlist.maillist .mwhen {
      justify-self: end;
      white-space: nowrap;
      font-size: var(--fs-xs);
      color: var(--muted);
      padding: 0;
   }

   /* The second line: what it is about on the left, the marks on the
      right. One line, cut -- a subject and the opening of a letter is
      more than a phone has room for and the point is the first few
      words of each. */
   .dlist.maillist .mtext {
      min-width: 0;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
   }
   .dlist.maillist .mprev {
      display: inline;
      color: var(--muted);
      font-size: var(--fs-xs);
   }
   /* An em dash between the subject and the letter, so the two do not
      read as one sentence. */
   .dlist.maillist .mprev::before { content: ' — '; }
   .dlist.maillist .mmarks { display: inline-flex; gap: .25rem; align-items: center; }

   /* An unread letter is bolder, not merely tinted: the tint is a card
      background on a phone held in daylight. */
   .dlist.maillist .dlist-row.is-new .mwho,
   .dlist.maillist .dlist-row.is-new .mtext a { font-weight: 600; }


   /* --- THE SHIFT LIST READS AS TWO LINES, ON A PHONE ------------------

      Seven labelled values stacked down a phone is six or seven lines a shift.
      Here it is two: who and how long on the first, then where, from when to
      when, and the marks on the second. A flex row that wraps rather than a
      grid, so a long washpoint name or an overtime note pushes the marks to a
      third line instead of being cut. Margins and no `gap`: the tablet's
      WebView has none on flexbox. */
   .dlist.hourlist .dlist-row {
      display: flex;
      flex-wrap: wrap;
      align-items: baseline;
      padding: .45rem var(--dl-x);
   }
   .dlist.hourlist .dlist-row > * {
      display: block;
      padding: 0;
      margin: 0 .6rem 0 0;
      min-width: 0;
      text-align: left;
      order: 2;
   }
   .dlist.hourlist .dlist-row > *::before { content: none; }
   .dlist.hourlist .dlist-row::after { content: ''; order: 1; flex: 0 0 100%; height: 0; }
   .dlist.hourlist .dlist-row > .hwho { order: 0; flex: 1 1 0; font-weight: 500; margin-right: .6rem; }
   .dlist.hourlist .dlist-row > .hlen { order: 0; flex: 0 0 auto; margin-right: 0; font-weight: 500; }
   .dlist.hourlist .dlist-row > .hsite,
   .dlist.hourlist .dlist-row > .hstart,
   .dlist.hourlist .dlist-row > .hend,
   .dlist.hourlist .dlist-row > .hot,
   .dlist.hourlist .dlist-row > .hsrc { font-size: var(--fs-xs); color: var(--muted); }
   .dlist.hourlist .dlist-row > .hend::before { content: '\2013\00a0'; margin-left: -.35rem; }
   .dlist.hourlist .dlist-row > .hend .hsame { display: none; }
   .dlist.hourlist .dlist-row > .hot > .sub { display: none; }
   .dlist.hourlist .dlist-row > .hnil,
   .dlist.hourlist .hy { display: none; }
   .dlist.hourlist .pill { padding: .05rem .4rem; font-size: var(--fs-xs); }
}

/* A phone and a small tablet do not need 1.5rem of padding either side of every
   card: that is a quarter of the width spent on margins. */
@media (max-width: 40rem) {
   .card, .card.pad { --card-pad: .875rem; }
   .card-body { padding: .875rem; }
   .card-head { padding-left: .875rem; padding-right: .875rem; }
   .card-foot { padding-left: .875rem; padding-right: .875rem; }
   .card.pad > .card-head { padding-left: 0; padding-right: 0; }
   .card.pad > .card-foot { padding-left: 0; padding-right: 0; }
}

/* A card built from head / body / foot parts pads them itself, and :has() is
   what zeroes the card's own padding so nothing is inset twice. An engine
   without :has() (the Android 8.1 tablet) keeps the card padding as well, and
   every such card came out with double side margins.

   Done without :has(): the parts pull themselves out by exactly what the card
   padded. Where :has() works --card-pad is already 0 for these cards, so the
   margins below come to nothing and the page is unchanged. */
.card:not(.pad) > .card-head,
.card:not(.pad) > .card-body,
.card:not(.pad) > .card-foot { margin-left: calc(var(--card-pad) * -1); margin-right: calc(var(--card-pad) * -1); }
.card:not(.pad) > .card-head:first-child,
.card:not(.pad) > .card-body:first-child { margin-top: calc(var(--card-pad) * -1); }
.card:not(.pad) > .card-body:last-child,
.card:not(.pad) > .card-foot:last-child { margin-bottom: calc(var(--card-pad) * -1); }



/* --- catalogue icons ---------------------------------------------------
   A washer picking a vehicle at six in the morning recognises a shape
   faster than they read "Tehergépjármű 12 t".

   The icons are SILHOUETTES stored as alpha: black ink where the shape is,
   nothing anywhere else. That is what makes them work at this size -- an
   outline loses its stroke to the scaling and turns into a smudge, while a
   filled shape keeps its mass -- and it is also what lets one file serve
   both themes.

   Ink, not picture. The file carries no colour worth keeping, so it is used
   as a MASK and painted in currentColor: the icon is dark on a light tile,
   light on a dark one, and white on the accent when the tile is selected,
   all from the same PNG and with nothing to keep in step.

   The layout follows the predecessor: the shape sits BEFORE the name on one
   line, not above it. A tile that is one line tall stays a tile; the moment
   the picture goes on top, a grid of eighty-seven of them becomes a wall.
   ------------------------------------------------------------------- */

.tico {
   display: inline-block; flex: 0 0 auto;
   width: 1.5rem; height: 1.5rem;
   background-color: currentColor;
   -webkit-mask: var(--ico) no-repeat center / contain;
           mask: var(--ico) no-repeat center / contain;
}
.pick > span:has(.tico) { gap: .45rem; }
.picn { min-width: 0; }

/* Wider where there is room for it: the catalogue list is a table and the
   icon is its own column, so it is not competing with a label. */
.dlist-row > .tico { width: 2rem; height: 2rem; }
.wtpick > .tico { margin-right: .5rem; }

/* --- icons the wash list wanted ---------------------------------------
   Same mechanism as the rest: a name in data-icon, the glyph here. Drawn
   at the same weight as the others so a row does not have two families of
   picture in it.
   ------------------------------------------------------------------- */

[data-icon="car"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M4 16.5h16M5 16.5l1.3-5.2A2 2 0 0 1 8.2 9.8h7.6a2 2 0 0 1 1.9 1.5l1.3 5.2'/><circle cx='7.5' cy='18' r='1.6'/><circle cx='16.5' cy='18' r='1.6'/></svg>"); }
[data-icon="carwash"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M4 19h16M5 19l1.2-4.6A2 2 0 0 1 8.1 13h7.8a2 2 0 0 1 1.9 1.4L19 19'/><path d='M7 9V7M12 8.5V6M17 9V7'/></svg>"); }
[data-icon="filters"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M3.5 7h6.2M15.5 7h5M3.5 12h3.2M12.5 12h8M3.5 17h8.2M18 17h2.5'/><circle cx='12.6' cy='7' r='2.1'/><circle cx='9.6' cy='12' r='2.1'/><circle cx='15.1' cy='17' r='2.1'/></svg>"); }
[data-icon="money"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='2.5' y='6.5' width='19' height='11' rx='2'/><circle cx='12' cy='12' r='2.6'/><path d='M6 10v4M18 10v4'/></svg>"); }
[data-icon="plate"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='7' width='18' height='10' rx='2'/><path d='M7.5 11v2M11 11v2M14.5 11v2M18 11v2'/></svg>"); }
/* lucide spray-can, for the cleaning report on the partner's door. A spray
   bottle and not a broom: what the form is about is an office that was not
   left right, and a broom reads as the yard. */
[data-icon="clean"]::before { --ico: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'><path d='M9 3h4v3H9z'/><path d='M8.5 6h5a2.5 2.5 0 0 1 2.5 2.5V19a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2V8.5A2.5 2.5 0 0 1 8.5 6Z'/><path d='M18.5 4.5h.01M21 7h.01M18.5 9.5h.01'/></svg>"); }

/* A cell that carries a picture beside its words. */
.withico { display: flex; align-items: center; gap: .4rem; min-width: 0; }
.withico > .tico { width: 1.35rem; height: 1.35rem; }

/* --- a plate ----------------------------------------------------------
   Monospaced and bold, because a plate is a code and not a word: it is
   compared character by character against the thing in front of the
   washer, and a proportional face puts "1" and "I" at different widths
   while making them look the same. Tabular figures for the same reason.
   ------------------------------------------------------------------- */

.plate {
   font-family: var(--font-mono);
   font-variant-numeric: tabular-nums;
   font-weight: 700;
   letter-spacing: .02em;
}

/* A list whose "all" switch is on: still readable, plainly not in play. */
.picks.is-off { opacity: .45; }

/* A plain-text mail IS preformatted: its line breaks and its indentation
   are the only layout it has, so it is shown in a <pre>. What it must NOT
   do is make the page scroll sideways -- a mail written at 120 columns, or
   one with a long URL in it, would otherwise widen every screen it is read
   on. So it wraps, and anything unbreakable breaks. */
/* The frame a message's own HTML is shown in. It is sandboxed with an
   opaque origin, so it cannot tell this page how tall it is and nothing
   here can measure it -- which is the price of the isolation and worth
   paying. So it gets a generous box and scrolls inside it. */
.mailframe {
   display: block;
   width: 100%;
   height: 70vh;
   height: min(70vh, 40rem);
   border: 0;
   background: #fff;
}


/* The zoom around a letter. With no script the frame is just the frame above;
   with it, app.js sizes the frame at 1/z of the box and scales it by z, so
   the letter lays out as if the screen were that much wider. */
.mailzoom .mzbar { display: flex; justify-content: flex-end; padding: .35rem .5rem .45rem; }
.mailzoom .mzbar > * { margin-left: .3rem; }
.mailzoom .mzwrap { position: relative; overflow: hidden; height: 70vh; height: min(70vh, 40rem); }
.mailzoom .mzwrap .mailframe { transform-origin: 0 0; max-width: none; }

/* AUTO-FIT WITHOUT A SCRIPT. The zoom buttons need app.js, and in the app
   app.js is the copy bundled at build time -- a phone running an older build
   has the new stylesheet (the app loads that live) and the old script. So the
   first fit is also done in CSS, from two things the server puts on the box:
   a class naming the width the letter wants (mzn-600), and --vw here, the
   screen's width in steps of 20px less the page margins, as a plain number.
   scale = --vw / width, and the frame is sized to the box divided by that.
   Only below the width where the letter would not fit anyway, so a wide
   screen never enlarges a narrow letter. With the script present it takes
   over with inline styles and the two agree. */
.mailzoom { --vw: 280; }
@media (min-width: 320px) { .mailzoom { --vw: 280; } }
@media (min-width: 340px) { .mailzoom { --vw: 300; } }
@media (min-width: 360px) { .mailzoom { --vw: 320; } }
@media (min-width: 380px) { .mailzoom { --vw: 340; } }
@media (min-width: 400px) { .mailzoom { --vw: 360; } }
@media (min-width: 420px) { .mailzoom { --vw: 380; } }
@media (min-width: 440px) { .mailzoom { --vw: 400; } }
@media (min-width: 460px) { .mailzoom { --vw: 420; } }
@media (min-width: 480px) { .mailzoom { --vw: 440; } }
@media (min-width: 500px) { .mailzoom { --vw: 460; } }
@media (min-width: 520px) { .mailzoom { --vw: 480; } }
@media (min-width: 540px) { .mailzoom { --vw: 500; } }
@media (min-width: 560px) { .mailzoom { --vw: 520; } }
@media (min-width: 580px) { .mailzoom { --vw: 540; } }
@media (min-width: 600px) { .mailzoom { --vw: 560; } }
@media (min-width: 620px) { .mailzoom { --vw: 580; } }
@media (min-width: 640px) { .mailzoom { --vw: 600; } }
@media (min-width: 660px) { .mailzoom { --vw: 620; } }
@media (min-width: 680px) { .mailzoom { --vw: 640; } }
@media (min-width: 700px) { .mailzoom { --vw: 660; } }
@media (min-width: 720px) { .mailzoom { --vw: 680; } }
@media (min-width: 740px) { .mailzoom { --vw: 700; } }
@media (min-width: 760px) { .mailzoom { --vw: 720; } }
@media (min-width: 780px) { .mailzoom { --vw: 740; } }
@media (max-width: 439px) {
   .mailzoom.mzn-400 .mzwrap .mailframe {
      width: 400px;
      height: calc(70vh * 400 / var(--vw));
      transform: scale(calc(var(--vw) / 400));
   }
}
@media (max-width: 539px) {
   .mailzoom.mzn-500 .mzwrap .mailframe {
      width: 500px;
      height: calc(70vh * 500 / var(--vw));
      transform: scale(calc(var(--vw) / 500));
   }
}
@media (max-width: 639px) {
   .mailzoom.mzn-600 .mzwrap .mailframe {
      width: 600px;
      height: calc(70vh * 600 / var(--vw));
      transform: scale(calc(var(--vw) / 600));
   }
}
@media (max-width: 679px) {
   .mailzoom.mzn-640 .mzwrap .mailframe {
      width: 640px;
      height: calc(70vh * 640 / var(--vw));
      transform: scale(calc(var(--vw) / 640));
   }
}
@media (max-width: 739px) {
   .mailzoom.mzn-700 .mzwrap .mailframe {
      width: 700px;
      height: calc(70vh * 700 / var(--vw));
      transform: scale(calc(var(--vw) / 700));
   }
}
@media (max-width: 839px) {
   .mailzoom.mzn-800 .mzwrap .mailframe {
      width: 800px;
      height: calc(70vh * 800 / var(--vw));
      transform: scale(calc(var(--vw) / 800));
   }
}
@media (max-width: 939px) {
   .mailzoom.mzn-900 .mzwrap .mailframe {
      width: 900px;
      height: calc(70vh * 900 / var(--vw));
      transform: scale(calc(var(--vw) / 900));
   }
}
@media (max-width: 1039px) {
   .mailzoom.mzn-1000 .mzwrap .mailframe {
      width: 1000px;
      height: calc(70vh * 1000 / var(--vw));
      transform: scale(calc(var(--vw) / 1000));
   }
}
@media (max-width: 1239px) {
   .mailzoom.mzn-1200 .mzwrap .mailframe {
      width: 1200px;
      height: calc(70vh * 1200 / var(--vw));
      transform: scale(calc(var(--vw) / 1200));
   }
}

.mailbody {
   margin: 0;
   font-family: var(--font-mono);
   font-size: var(--fs-sm);
   line-height: 1.55;
   white-space: pre-wrap;
   overflow-wrap: anywhere;
   color: var(--ink);
}

/* A tile that is a form's submit rather than a link or a radio: the shift
   board clocks somebody in with a POST, and a POST needs a button.

   It is listed in the chrome rule above rather than given its own, because
   a second copy of that rule is how a button ends up looking like a button
   next to two tiles that do not -- which is exactly what happened: the
   selector said `.pick > span, a.pick` and a <button class="pick"> matched
   neither, so it fell through to whatever the browser draws by default. */
.pickform { margin: 0; display: contents; }
button.pick {
   font: inherit;
   width: 100%;
   -webkit-appearance: none;
   appearance: none;
}

/* --- charts -----------------------------------------------------------
   Drawn on the server as SVG, because the policy is script-src 'self'
   with no inline script and every charting library wants a <script> block
   with the data in it. Everything here is currentColor at an opacity, so
   one palette serves both themes.
   ------------------------------------------------------------------- */

/* currentColor was always the plan, but nothing ever set a colour for it to
   be: a chart inherits from the card, so every line, area, bar and gauge on
   the site drew itself in --body, the grey running text is set in. A
   dashboard of grey charts under a green brand reads as a dashboard whose
   charts failed to load. One declaration gives all nine marks the accent,
   and it is still one declaration that serves both themes, because --pri
   changes with them. */
.chart { margin: 0; color: var(--pri); }
.chart svg { display: block; width: 100%; height: auto; overflow: visible; }
.chart figcaption {
   margin-top: .35rem;
   color: var(--muted);
   font-size: var(--fs-xs, .75rem);
   font-variant-numeric: tabular-nums;
}

.ch-line { fill: none; stroke: currentColor; stroke-width: 2; vector-effect: non-scaling-stroke;
   stroke-linejoin: round; stroke-linecap: round; }
.ch-area { fill: currentColor; opacity: .12; stroke: none; }
/* .72, not .55: a bar is a shape a reader has to be able to see the edge of,
   which is the 3:1 that non-text contrast asks for. At .55 the accent over a
   light card measured 2.4:1 and the old grey 2.5 -- below the floor in both
   themes. .72 measures 3.4 light and 3.1 dark. */
.ch-bar  { fill: currentColor; opacity: .72; rx: 1.5; }
/* The axis labels are TEXT, and text at .45 of anything is unreadable: these
   measured 2.3:1. They are a caption, so they take the colour the sheet
   already keeps for captions, at full strength. */
.ch-tick { fill: var(--muted); font-size: 11px;
   font-family: inherit; font-variant-numeric: tabular-nums; }

.gauge svg { max-width: 14rem; margin-inline: auto; }
.ga-track { fill: none; stroke: currentColor; opacity: .14; stroke-width: 14; stroke-linecap: round; }
.ga-fill  { fill: none; stroke: currentColor; stroke-width: 14; stroke-linecap: round; }
.ga-num   { fill: currentColor; font-size: 34px; font-weight: 700;
   font-family: inherit; font-variant-numeric: tabular-nums; }

/* A row of charts that becomes a column when there is no room. */
.charts { display: grid; gap: 1rem; grid-template-columns: 1fr; }
@media (min-width: 48rem) {
   .charts { grid-template-columns: 2fr 1fr; align-items: end; }
}

/* --- camera tiles -----------------------------------------------------
   A still picture per camera, at the size a glance needs. The frame is
   fetched by the ordinary camera endpoint, so nothing new serves images.
   ------------------------------------------------------------------- */

.cams { display: grid; gap: .75rem;
   grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr)); }
.cam { display: block; text-decoration: none; color: inherit; }
.cam img, .cam .camhole {
   display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
   border-radius: var(--r); background: var(--paper-2, rgba(128,128,128,.12));
   box-shadow: 0 0 0 1px var(--line);
}
.cam .camhole { display: grid; place-items: center; color: var(--muted);
   font-size: var(--fs-sm); text-align: center; padding: .5rem; }
.cam .camname { display: block; margin-top: .35rem; font-size: var(--fs-sm); font-weight: 500; }
.cam .camwhere { display: block; color: var(--muted); font-size: var(--fs-xs, .75rem); }

/* --- a camera as a widget ---------------------------------------------
   The dashboard's front cameras. NOT .cam -- that one is a whole tile that
   is itself a button, and here the tile holds two separate things to do:
   the picture opens the viewer, the washpoint name goes to its dashboard.
   ------------------------------------------------------------------- */

/* It IS a .card (flush, so the picture reaches the edge). Everything the
   box looks like -- ground, lit edge, radius, shadow -- comes from there
   and is not restated here; only the clipping, which a card has no reason
   to do on its own. */
.camwidget {
   display: flex; flex-direction: column;
   overflow: hidden;
}
.campic {
   display: block; position: relative;
   padding: 0; border: 0; background: none;
   font: inherit; color: inherit; text-align: left;
   cursor: zoom-in; width: 100%;
}
.campic img {
   display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
   background: #11171b;
}
/* A scrim, not just pale text: the name has to stay readable over a sunlit
   forecourt and over a dark bay, and nothing can be assumed about either. */
.campic-name {
   position: absolute; inset-inline: 0; bottom: 0;
   padding: 1.25rem .5rem .35rem;
   font-size: var(--fs-sm); font-weight: 500;
   color: #fff; text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
   background: linear-gradient(to top, rgba(0, 0, 0, .72), rgba(0, 0, 0, 0));
   white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
   pointer-events: none;
}
/* --pri, like every other focus ring on this sheet. Ten rules here used to
   ask for var(--accent, currentColor) and there has never been an --accent
   token anywhere in the project, so all ten fell through to currentColor and
   drew the brand's rings and the camera strip's "this one" marker in the
   grey of the running text beside them. */
.campic:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }

.camwidget-foot {
   display: flex; align-items: baseline; gap: .5rem;
   padding: .5rem .75rem .6rem;
}
.camwidget-site {
   font-size: var(--fs-sm); font-weight: 500;
   color: inherit; text-decoration: none;
   white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.camwidget-site:hover { text-decoration: underline; }
.camwidget-today {
   margin-inline-start: auto; flex: 0 0 auto;
   color: var(--muted); font-size: var(--fs-xs, .75rem);
   font-variant-numeric: tabular-nums;
}

/* --- last month, on a partner's panel ---------------------------------
   Small on purpose. It answers one question and offers the two things
   anybody does with the answer, and it must not out-shout the figures it
   sits under.
   ------------------------------------------------------------------- */

/* Counts on the left, the shape of the month on the right. They collapse
   to one column early: two cards and a chart side by side on a phone would
   make all three too narrow to read. */
.pmonths {
   display: grid; gap: .75rem;
   grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
   /* stretch, not start: the chart card is asked to be as tall as the two
      count cards beside it, which is what makes the row read as one block
      rather than three things that happen to be adjacent. */
   align-items: stretch;
}
@media (max-width: 47.99rem) {
   .pmonths { grid-template-columns: minmax(0, 1fr); }
}
.pmonths-counts { display: grid; gap: .6rem; }
/* A column, so the body can take whatever height is left over after the
   heading -- and the chart inside it can then fill that. */
.pmonths-chart { min-width: 0; display: flex; flex-direction: column; }
.pmonths-chart > .card-body { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.pmonths-chart [data-span-pane] { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.pmonths-chart .chart { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
/* preserveAspectRatio="none" on the line chart means it is happy to be any
   height at all, which is exactly what is wanted here. */
.pmonths-chart .chart svg { flex: 1 1 auto; height: auto; min-height: 7rem; }

.spanbar { display: flex; gap: .4rem; justify-content: flex-end; margin-bottom: .4rem; }
/* It IS a .card. Ground, radius, lit edge and shadow all come from there
   and are not restated here -- stating them again was a second opinion
   about what a box looks like, and it won, because it came later in the
   file. Only the padding is narrowed, and through the card's own token
   rather than around it. */
.pmonth { --card-pad: .75rem; }
.pmonth-cap {
   display: block;
   font-size: var(--fs-xs, .75rem); text-transform: uppercase;
   letter-spacing: .05em; color: var(--muted);
}
.pmonth-num {
   display: block; margin: .1rem 0 .15rem;
   font-size: 1.75rem; font-weight: 600; line-height: 1.1;
   font-variant-numeric: tabular-nums;
}
.pmonth-unit { font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
.pmonth .actions { margin-top: .5rem; }

/* --- a partner's own blocks ------------------------------------------- */

/* The partner switcher, when somebody reaches more than one. */
.ptabs { margin-bottom: .75rem; }
.ptabs .pick { text-decoration: none; }

.newsrow + .newsrow {
   margin-top: .6rem; padding-top: .6rem;
   box-shadow: inset 0 1px 0 0 var(--line);
}
.newsrow-head { font-weight: 500; }
.newsrow-head .sub { margin-inline-start: .4rem; font-weight: 400; }

/* Leaflet needs a height in pixels: it measures the container, and a map
   in a box sized by its content would be a box sized by a map of no
   height. */
.axhmap { position: relative; z-index: 0; isolation: isolate; height: 22rem; width: 100%; }
@media (max-width: 47.99rem) { .axhmap { height: 16rem; } }
/* The tiles are somebody else's pictures and carry their own white; the
   popup is ours and should follow the page. */
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
   background: var(--paper); color: var(--body);
}
.leaflet-container { background: var(--paper-2, #ddd); font: inherit; }

/* The pin, drawn rather than fetched -- Leaflet's default marker is a PNG
   it looks for in images/ beside its own stylesheet, and ours is vendored
   to assets/ where no such directory exists. */
.axhpin { background: none; border: 0; }
.axhpin-dot {
   display: block; width: 18px; height: 18px;
   border-radius: 50% 50% 50% 0;
   transform: rotate(-45deg);
   background: var(--pri, #2f6f4f);
   box-shadow: 0 0 0 2px #fff, 0 1px 3px rgba(0, 0, 0, .5);
}

/* --- what is expected, as opposed to what happened --------------------
   Set apart from the figures above it on purpose: those are measurements
   and this is not, and a prediction sitting unmarked among facts is how a
   guess gets quoted back as a number.
   ------------------------------------------------------------------- */

.ahead {
   display: flex; flex-wrap: wrap; align-items: baseline; gap: .35rem .75rem;
   margin: .25rem 0 .9rem;
   padding: .5rem .7rem;
   border-radius: var(--r-sm, .375rem);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.ahead-tag {
   text-transform: uppercase; letter-spacing: .06em;
   font-size: var(--fs-xs, .75rem); font-weight: 600;
   color: var(--muted);
}
.ahead-bit { font-size: var(--fs-sm); color: var(--muted); }
.ahead-bit strong { color: var(--body); font-variant-numeric: tabular-nums; }
.ahead-note { font-size: var(--fs-xs, .75rem); color: var(--muted); font-style: italic; }

/* --- reading a value off a chart --------------------------------------
   An invisible band per point, so the target is a column rather than a line
   two pixels wide -- which is unhittable with a finger.
   ------------------------------------------------------------------- */

.chart { touch-action: pan-y; }
.ch-hit { fill: transparent; cursor: pointer; }
.ch-mark-line {
   stroke: currentColor; stroke-width: 1;
   opacity: .35;
   /* The chart is drawn with preserveAspectRatio="none", so the x axis is
      stretched and a plain stroke would be stretched with it. */
   vector-effect: non-scaling-stroke;
}
.ch-mark-dot { fill: currentColor; stroke: var(--paper); stroke-width: 1.5;
   vector-effect: non-scaling-stroke; }
.ch-read { font-variant-numeric: tabular-nums; font-weight: 500; color: var(--body); }

/* --- the wash list's one search box ------------------------------------
   One box across every facet, and what is picked becomes a coloured label
   under it. The colour is the POINT: five chips in a row are read by their
   colour and icon long before the words are read.
   ------------------------------------------------------------------- */

/* Its own card, so the asking is visibly separate from the answering --
   the list below is the second card. No bottom margin of its own: .content
   is a flex column with a gap and spaces them. */
.findcard { --card-pad: .85rem; }
.findbox { display: block; }

/* ONE field, not three. The frame is on the row; the text box, the two
   dates and the dash inside it carry none of their own. */
.findbox-top {
   display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
   padding: .3rem .3rem .3rem .65rem;
   border-radius: var(--r-sm, .375rem);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.findbox-top:focus-within { box-shadow: inset 0 0 0 2px var(--pri); }
.findbox-field { position: relative; flex: 1 1 16rem; min-width: 0; }
.findbox-sep {
   flex: 0 0 auto; width: 1px; align-self: stretch; margin: .15rem .1rem;
   background: var(--pick-line);
}
.findbox-dates { display: flex; align-items: center; gap: .2rem; flex: 0 0 auto; }
.findbox-dash { color: var(--muted); }
.findbox-go { flex: 0 0 auto; }
/* relative, so a dropdown opened from any of these spans the whole row
   rather than hanging off the button that opened it. */
.findbox-buttons {
   position: relative;
   display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .55rem;
}

/* Every control inside the one field is borderless and transparent -- the
   row is the input, they are only its parts. */
.findbox-top input {
   border: 0; background: none; box-shadow: none; outline: none;
   font: inherit; color: inherit; padding: .35rem .1rem;
}
.findbox-field .tokinput { width: 100%; }
.findbox-dates input { min-width: 7.6rem; color: var(--muted); }
.findbox-dates input:focus { color: var(--body); }

/* The chosen labels sit under the box AND the buttons, because both put
   them there. Empty, the row takes no room at all. */
.tokchips { display: flex; flex-wrap: wrap; gap: .35rem; }
.tokchips:empty { display: none; }
.tokchips:not(:empty) { margin-top: .55rem; }

/* One colour per kind, carried from the suggestion to the chip. Set as a
   token so the chip, the dropdown button and the suggestion all read it
   from the same place. */
.tok-car     { --tok: #2f6f4f; }
.tok-wash    { --tok: #2b5d8a; }
.tok-site    { --tok: #8a5a2b; }
.tok-pay     { --tok: #6b3f8a; }
.tok-partner { --tok: #8a2b4f; }
.tok-flag    { --tok: #777; }

/* A chip is a small button that happens to be coloured by its kind: the
   same metrics, radius and type as .btn.sm, so a row of chips sits on the
   same line as the buttons above it without looking like a different
   system. Only the fill differs, and that is the whole point of it. */
.tokchip {
   display: inline-flex; align-items: center; justify-content: center;
   gap: .45rem;
   padding: .3rem .4rem .3rem .75rem;
   border: 0; border-radius: var(--r);
   font-size: var(--fs-xs); line-height: 1.4; white-space: nowrap;
   color: var(--body);
   background: color-mix(in srgb, var(--tok, #777) 16%, var(--paper));
   box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tok, #777) 42%, transparent);
}
.tokchip-off {
   border: 0; background: none; cursor: pointer;
   padding: 0 .15rem; font: inherit; font-size: .95rem; line-height: 1;
   color: var(--muted);
}
.tokchip-off:hover { color: var(--body); }

/* The drawn silhouette, painted in the kind's own colour -- the same mask
   technique the listing and the recording form use. */
.tokico {
   display: inline-block; width: 1.1em; height: 1.1em; flex: 0 0 auto;
   background-color: var(--tok, currentColor);
   -webkit-mask: var(--ico) center / contain no-repeat;
   mask: var(--ico) center / contain no-repeat;
}

.tokmenu, .tokdrop-list {
   position: absolute; z-index: 40; margin: .2rem 0 0; padding: .25rem;
   list-style: none; max-height: 18rem; overflow-y: auto;
   border-radius: var(--r-sm, .375rem);
   background: var(--paper);
   box-shadow: var(--sh-lg), 0 0 0 1px var(--line);
}
.tokmenu { inset-inline: 0; top: calc(100% + .45rem); }
/* The same drawings the list's column headers use, at button size and in
   the facet's own colour. */
.bico { display: inline-block; flex: 0 0 auto; width: 1em; height: 1em; }
.bico::before {
   content: ''; display: block; width: 100%; height: 100%;
   background: var(--tok, currentColor);
   -webkit-mask: var(--ico) center / contain no-repeat;
           mask: var(--ico) center / contain no-repeat;
}
.tokmenu li, .tokdrop-item {
   display: flex; align-items: center; gap: .4rem;
   width: 100%; padding: .32rem .45rem;
   border: 0; background: none; text-align: left;
   border-radius: var(--r-sm, .375rem);
   font: inherit; font-size: var(--fs-sm); color: inherit; cursor: pointer;
}
.tokmenu li:hover, .tokmenu li.is-on, .tokdrop-item:hover {
   background: var(--pick-bg-hov, var(--pick-bg));
}
.tokkind { margin-inline-start: auto; color: var(--muted); font-size: var(--fs-xs, .75rem); }

/* static on purpose: the list anchors to .findbox-buttons above, not to
   the button, which is what makes it full width. */
.tokdrop { position: static; }

/* Columns, not a column. Eighty wash types in one tall strip is a scroll
   nobody reads to the end of; the same eighty across the width of the card
   can be scanned. */
.tokdrop-list {
   inset-inline: 0; top: calc(100% + .3rem);
   display: grid; gap: .1rem .4rem;
   grid-template-columns: repeat(auto-fill, minmax(min(13rem, 100%), 1fr));
   padding: .5rem;
   max-height: 22rem;
}
.tokdrop-pane { grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr)); gap: .35rem; }
.tokdrop-list li { list-style: none; min-width: 0; }
.tokdrop-item { width: 100%; min-width: 0; }
.tokdrop-item span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn.is-on { box-shadow: inset 0 0 0 2px var(--pri); }

/* --- picking several types by typing ----------------------------------
   A text box that filters a list, and what you pick becomes a label with a
   cross on it. Used for the two filters that take more than one value.
   ------------------------------------------------------------------- */

.tagpick { position: relative; }
.tagpick-box {
   display: flex; flex-wrap: wrap; align-items: center; gap: .3rem;
   min-height: 2.25rem; padding: .25rem .4rem;
   border-radius: var(--r-sm, .375rem);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
}
.tagpick-box:focus-within { box-shadow: inset 0 0 0 2px var(--pri); }
.tagpick-chips { display: contents; }
.tagpick-input {
   flex: 1 1 6rem; min-width: 5rem;
   border: 0; background: none; padding: .15rem;
   font: inherit; color: inherit; outline: none;
}
.tag {
   display: inline-flex; align-items: center; gap: .25rem;
   padding: .1rem .2rem .1rem .45rem;
   border-radius: var(--r-sm, .375rem);
   background: var(--paper-2, rgba(128, 128, 128, .18));
   box-shadow: inset 0 0 0 1px var(--line);
   font-size: var(--fs-sm); white-space: nowrap;
}
.tag-off {
   border: 0; background: none; cursor: pointer;
   padding: 0 .2rem; font: inherit; font-size: 1rem; line-height: 1;
   color: var(--muted);
}
.tag-off:hover { color: var(--body); }
.tagpick-menu {
   position: absolute; z-index: 30; inset-inline: 0; top: calc(100% + .2rem);
   margin: 0; padding: .25rem; list-style: none;
   max-height: 14rem; overflow-y: auto;
   border-radius: var(--r-sm, .375rem);
   background: var(--paper);
   box-shadow: var(--sh-lg), 0 0 0 1px var(--line);
}
.tagpick-menu li {
   padding: .3rem .45rem; border-radius: var(--r-sm, .375rem);
   font-size: var(--fs-sm); cursor: pointer;
}
.tagpick-menu li:hover, .tagpick-menu li.is-on { background: var(--pick-bg-hov, var(--pick-bg)); }

/* --- a wash opened from the listing ----------------------------------
   The whole row opens it, so the row has to look like something to press.
   ------------------------------------------------------------------- */

.dlist-row.is-peek { cursor: pointer; }
.dlist-row.is-peek:hover { background: var(--pick-bg); }

.peekmodal {
   width: min(96vw, 44rem);
   max-width: none; max-height: 92vh;
   padding: 0; border: 0;
   border-radius: var(--r-lg, .75rem);
   background: var(--paper);
   color: var(--body);
   box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, .45);
}
.peekmodal::backdrop { background: rgba(0, 0, 0, .72); }
.peekmodal-head {
   display: flex; align-items: center; gap: .5rem;
   padding: .6rem .75rem;
   box-shadow: inset 0 -1px 0 0 var(--line);
}
.peekmodal-body { padding: .75rem; overflow-y: auto; max-height: 76vh; }
/* While the fragment is on its way the box is empty, and an empty box that
   is about to have content should not collapse to nothing first. */
.peekmodal.is-busy .peekmodal-body { min-height: 8rem; }

.peek-head {
   display: flex; align-items: center; gap: .5rem;
   margin-bottom: .6rem;
}
.peek-head .sub { margin-inline-start: .4rem; }
.peek-grid {
   display: grid; gap: .5rem .9rem;
   grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
   margin-bottom: .6rem;
}
.peek-total { text-align: right; margin: .5rem 0 0; }
.peek-vers { display: flex; flex-wrap: wrap; gap: .4rem; align-items: baseline; margin: .6rem 0 0; }

/*
   Printing is a block, not a row.

   It used to be a .peek-vers like the others: a label and some buttons on
   one line, reading as one more detail of the record. It is not one. It is
   the only control on this viewer that puts paper into somebody's hand, and
   the owner asked for it set apart -- its own line with the buttons under
   the heading rather than beside it, a look of its own, and a printer on
   the heading.

   The panel is .pick-bg rather than a border, so it reads as a separate
   surface without drawing a line across a viewer that already has several.
*/
.peek-print {
   margin: .9rem 0 0;
   padding: .7rem .8rem;
   border-radius: var(--r-sm);
   background: var(--pick-bg);
}
.peek-print > .lbl {
   display: flex;
   align-items: center;
   margin: 0 0 .55rem;
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-transform: uppercase;
   letter-spacing: .04em;
}
.peek-print > .actions { gap: .4rem; }

/* The same printer on the full page's card, where the heading is an h2 and
   already carries its own size and colour -- the glyph only has to line up
   with it, which is what the em-based sizing is for. */
.card.is-print > .card-head h2 { display: flex; align-items: center; }

.ico-print {
   width: 1em;
   height: 1em;
   flex: none;
   vertical-align: -.125em;
   margin-inline-end: .45em;
}
.peek-ver {
   padding: .1rem .4rem; border-radius: var(--r-sm, .375rem);
   background: var(--pick-bg); font-size: var(--fs-xs, .75rem);
}
.peek-foot {
   display: flex; gap: .5rem; flex-wrap: wrap;
   margin-top: .9rem; padding-top: .7rem;
   box-shadow: inset 0 1px 0 0 var(--line);
}

/* --- the typed search on the wash list --------------------------------
   One box above the filter bar, and under it a line saying how the phrase
   was read. The read-back is the important half: a filter nobody asked for
   is worse than no result, because the short list gets believed.
   ------------------------------------------------------------------- */

/* The invoice register used to have a .findbar of its own -- a search box
   and its buttons on one line. It is one .filterbar now, because the dates,
   the state, the company and the amount all have to compose with the term
   and two forms each throw the other's answer away; the search box is the
   .field.grow in it. What is left here is the READ-BACK, which that screen
   does use: a line of chips under the bar saying what is narrowing the
   list, because a filter nobody remembers switching on is worse than no
   result -- a short list gets believed. */
.findread {
   display: flex; flex-wrap: wrap; gap: .35rem;
   margin: 0 0 .75rem;
}
.findbit {
   display: inline-flex; align-items: baseline; gap: .35rem;
   /* A supplier name is up to 191 characters -- "CONCORDIA-TRANS
      KERESKEDELMI SZÁLLITÁSI SZOLGÁLTATÓ KFT." is a real one -- and a chip
      that cannot shrink pushes the card open on a phone. */
   max-width: 100%;
   padding: .2rem .5rem;
   border-radius: var(--r-sm, .375rem);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   font-size: var(--fs-sm);
}
.findbit-k { color: var(--muted); font-size: var(--fs-xs, .75rem); }
.findbit-v { font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
/* Marked, not hidden: when the answer came from a model rather than from
   the rules it is worth knowing, because that is the one that can be
   confidently wrong. */
.findbit-ai { color: var(--muted); font-style: italic; }

/* The shift board at a washpoint with a long rota. */
.shiftfilter {
   width: 100%; margin-bottom: .5rem;
}
.picks.tight { grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); }
.picks.tight button.pick {
   min-height: 2.1rem; padding: .3rem .5rem;
   font-size: var(--fs-sm); white-space: nowrap;
   overflow: hidden; text-overflow: ellipsis; display: block; text-align: center;
}
.shiftgroup { margin-top: .75rem; }

/* --- the signature pad ------------------------------------------------
   A washpoint monitor is a touchscreen, so this is sized for a finger and
   not for a cursor: a tall drawing area, large buttons, and touch-action
   none on the canvas so a stroke does not scroll the page under it.
   ------------------------------------------------------------------- */

.signpad { margin-top: .75rem; }
.signpad canvas {
   display: block; width: 100%; height: 11rem;
   touch-action: none; cursor: crosshair;
   background: #fff;
   border-radius: var(--r);
   box-shadow: inset 0 0 0 1px var(--line);
}
.signpad .signline {
   /* Where a signature sits on paper: a rule to write above.

      SHIFTED, not pulled. A negative margin moves the element AND everything
      after it, so the rule landed in the right place and dragged the buttons
      up behind the canvas with it -- "Rendben" came out with its top half
      sliced off. An offset on a relatively positioned, zero-height box moves
      the drawing alone and leaves the layout where it was. */
   position: relative; top: -2.2rem;
   height: 0; margin: 0;
   border-top: 1px dashed var(--line); pointer-events: none;
}
.signwho { font-weight: 600; }
.signpad .actions { margin-top: .6rem; }
.signpad .btn { min-height: 2.75rem; padding-inline: 1.1rem; }

/* --- the recording player ---------------------------------------------
   The arrangement is the predecessor's: the camera and the moment sit ON
   the picture, the clock in its corner, a seeker under it and one row of
   buttons. The colours, the buttons and the type are this system's.

   A recording is a sequence of stills, so the "video" is an <img> whose
   src changes. The stage holds its aspect so the page does not jump as
   frames of slightly different sizes arrive.
   ------------------------------------------------------------------- */

.vpstage {
   position: relative;
   background: #000;
   aspect-ratio: 16 / 9;
   display: grid;
   place-items: center;
   overflow: hidden;
}
.vpimg { display: block; width: 100%; height: 100%; object-fit: contain; }

.vpover {
   position: absolute; top: .5rem;
   display: flex; align-items: center; gap: .4rem;
   flex-wrap: wrap;
}
.vpover-tl { left: .5rem; right: .5rem; }
.vpover-tr { right: .5rem; top: .5rem; left: auto; justify-content: flex-end; }

/* Controls over a picture: dark enough to read against daylight and
   against a night shot, which a translucent surface token is not. */
.vpwhen, .vpclock {
   background: rgba(0, 0, 0, .62);
   color: #fff;
   border: 0;
   border-radius: var(--r-sm, .375rem);
   font-family: inherit;
   font-size: var(--fs-sm);
   line-height: 1.6;
}
.vpwhen { display: inline-flex; align-items: center; gap: .15rem; padding: .2rem .4rem; }
.vpdate, .vpnum {
   background: transparent; border: 0; color: #fff;
   font: inherit; font-variant-numeric: tabular-nums; padding: 0;
}
.vpnum { width: 2.1rem; text-align: center; -moz-appearance: textfield; }
.vpnum::-webkit-outer-spin-button, .vpnum::-webkit-inner-spin-button {
   -webkit-appearance: none; margin: 0;
}
.vpcolon { color: #fff; opacity: .7; }
.vpclock {
   padding: .2rem .5rem;
   font-variant-numeric: tabular-nums;
}
.vpspin {
   width: 1rem; height: 1rem; margin-left: .35rem;
   border: 2px solid rgba(255, 255, 255, .35);
   border-top-color: #fff;
   border-radius: 50%;
   animation: vpspin 700ms linear infinite;
}
@keyframes vpspin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .vpspin { animation: none; } }

.vpnote {
   position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
   margin: 0; padding: .4rem .7rem;
   background: rgba(0, 0, 0, .7); color: #fff;
   border-radius: var(--r-sm, .375rem);
   font-size: var(--fs-sm);
}

/* The seeker runs over what is LOADED. Nothing knows how long the
   recording is, so this is a position in the buffer -- which is what the
   predecessor's past/future split showed as well. */
.vpseek {
   position: relative; height: 2rem; cursor: pointer; touch-action: none;
}
.vpseek-track, .vpseek-done {
   position: absolute; left: 0; top: 50%; height: .35rem;
   transform: translateY(-50%); border-radius: 99px;
}
.vpseek-track { right: 0; background: var(--line); }
.vpseek-done { width: 0; background: var(--pri); }
.vpseek-knob {
   position: absolute; top: 50%; left: 0;
   width: 1rem; height: 1rem; margin: -.5rem 0 0 -.5rem;
   border-radius: 50%;
   background: var(--pri);
   box-shadow: 0 0 0 3px var(--paper, #fff);
}
.vpseek.is-seeking .vpseek-knob { transform: scale(1.2); }
.vpseek:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

.vpbar {
   display: flex; flex-wrap: wrap; gap: .4rem;
   justify-content: center; align-items: stretch;
   padding: .5rem .75rem .9rem;
}
.vpbtn {
   display: flex; flex-direction: column; align-items: center; gap: .1rem;
   min-width: 4.2rem; padding: .45rem .5rem;
   border: 0; border-radius: var(--r);
   background: var(--pick-bg);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   color: var(--pick-ink);
   font-family: inherit; cursor: pointer;
   transition: background var(--speed), box-shadow var(--speed);
}
.vpbtn:hover { background: var(--pick-bg-hov); }
.vpbtn-main { background: var(--btn-grad); color: var(--on-pri); box-shadow: none; }
.vpicon { font-size: 1.05rem; line-height: 1.2; }
.vpicon-text { font-weight: 700; font-variant-numeric: tabular-nums; }
.vplabel { font-size: var(--fs-xs, .75rem); opacity: .85; white-space: nowrap; }
.tilecam-play { display: block; font-size: var(--fs-xs, .75rem); margin-top: .15rem; }

/* --- the camera viewer ------------------------------------------------
   A <dialog>, so the browser owns the backdrop, the focus trap and
   Escape. It fills most of the screen because the whole point is to look
   closer, and the stage is black so a night shot does not sit on a white
   page pretending to be brighter than it is.
   ------------------------------------------------------------------- */

.cammodal {
   width: min(96vw, 1200px);
   max-width: none;
   max-height: 92vh;
   padding: 0;
   border: 0;
   border-radius: var(--r-lg, .75rem);
   /* --paper, not --surface. The latter is not a token this stylesheet
      defines, so it resolved to nothing and the dialog had no background
      at all -- the page showed straight through the text. --paper is the
      opaque one the cards and menus use, which is what a modal wants. */
   background: var(--paper);
   color: var(--body);
   box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, .45);
   overflow: hidden;
}
.cammodal::backdrop { background: rgba(0, 0, 0, .72); }

.cammodal-head {
   display: flex; flex-direction: column; gap: .4rem;
   padding: .6rem .75rem;
   border-bottom: 1px solid var(--line);
}
.cammodal-row { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
/* The name takes what is left and gives way rather than pushing the close
   button off the row -- which is what a long camera name did. */
.cammodal-name { min-width: 0; overflow: hidden; text-overflow: ellipsis;
   white-space: nowrap; }
.cammodal-age, .cammodal-zoom {
   color: var(--muted); font-size: var(--fs-xs, .75rem);
   font-variant-numeric: tabular-nums; min-width: 2.5rem; text-align: center;
}

.cammodal-stage {
   position: relative;
   background: #000;
   aspect-ratio: 16 / 9;
   max-height: 72vh;
   /* Capped by the height, the box takes its width from the ratio -- so it
      is narrower than the dialog and sat against the left edge. */
   margin-inline: auto;
   overflow: hidden;
   display: grid; place-items: center;
   touch-action: none;
   cursor: grab;
}
.cammodal-stage:active { cursor: grabbing; }
.cammodal-stage img {
   display: block; width: 100%; height: 100%; object-fit: contain;
   transform-origin: center center;
   will-change: transform;
}
.cammodal-foot { margin: 0; padding: .5rem .75rem .7rem; }

/* The tile is a button now, so it needs a button's defaults removed. */
button.cam {
   border: 0; padding: 0; background: none; font: inherit;
   text-align: left; cursor: zoom-in; width: 100%;
}

/* --- the player on a narrow screen ------------------------------------
   Controls ON the picture is right where there is room for them: they sit
   beside what they act on and cost no height. On a phone there is no room
   -- the camera name, a date and two numbers wrap into three lines and
   cover a third of the picture, which is the one thing the screen is for.

   So below 48rem they come off it and sit underneath. The clock stays on
   the picture, because it is two words wide and it belongs to the frame
   rather than to the controls.
   ------------------------------------------------------------------- */

@media (max-width: 47.99rem) {
   .vpover-tl {
      position: static;
      padding: .5rem .75rem 0;
      gap: .4rem;
   }
   .vpover-tl .vpwhen {
      background: var(--pick-bg);
      color: var(--ink);
      box-shadow: inset 0 0 0 1px var(--pick-line);
   }
   .vpover-tl .vpdate,
   .vpover-tl .vpnum,
   .vpover-tl .vpcolon { color: var(--ink); }
   .vpwhen { flex: 1 1 auto; justify-content: center; }

   /* The stage comes first in the box even though the controls come first
      in the markup: the picture is what somebody is looking at. */
   .vplay { display: flex; flex-direction: column; }
   .vpstage { order: -1; }
}

/* The date field is as wide as whatever the browser decides to write in
   it -- "2026. Oct 1." on one, "2026-10-01" on another -- so it is given a
   width and a tabular face instead of being left to guess. */
.vpdate { width: 8.5rem; font-variant-numeric: tabular-nums; }

/* One row of buttons that wraps into equal cells rather than into a ragged
   edge: nine controls of three different widths look like a mistake. */
.vpbar {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(4.6rem, 1fr));
   gap: .4rem;
}
.vpbtn { min-width: 0; }

/* The play button is the one anybody reaches for first, so it spans and
   stays where the thumb is rather than taking its turn in the grid. */
@media (max-width: 47.99rem) {
   .vpbar { grid-template-columns: repeat(4, 1fr); }
   .vpbtn-main { grid-column: span 4; order: -1; }
}

/* Room for the knob at both ends, or it hangs off the track at 0% and
   100% -- which is exactly where it sits when a buffer has just loaded. */
.vpseek { padding-inline: .6rem; }
.vpseek-track { left: .6rem; right: .6rem; }
.vpseek-done { left: .6rem; }
.vpseek-knob { margin-left: 0; }

/* The dialog's close: a glyph in the corner, at a size a thumb can find. */
.camclose {
   display: grid; place-items: center;
   width: 2.25rem; height: 2.25rem;
   margin: -.25rem -.25rem -.25rem 0;
   border: 0; border-radius: var(--r);
   background: transparent;
   color: var(--muted);
   font: inherit; font-size: 1.5rem; line-height: 1;
   cursor: pointer;
   transition: background var(--speed), color var(--speed);
}
.camclose:hover { background: var(--pick-bg-hov); color: var(--body); }
.camclose:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

/* --- the viewer's filmstrip -------------------------------------------
   The washpoint's other cameras, under the picture. A row that scrolls
   rather than wraps: nine of them wrapped would push the picture off a
   phone, which is the thing the dialog exists to show.
   ------------------------------------------------------------------- */

.camstrip {
   display: flex; gap: .4rem;
   padding: .5rem .75rem 0;
   overflow-x: auto;
   scrollbar-width: thin;
}
/* An author `display` beats the UA's [hidden] rule, so hiding it has to be
   said here or the strip shows through as an empty row. */
.camstrip[hidden] { display: none; }
/* NOT .camthumb -- that name is the camera list's, and its rule holds a
   picture at opacity 0 until its own loader marks it live. Borrowing it
   here made every thumbnail in the strip invisible whether it loaded or
   not. The strip owns its own names. */
.camstrip-item {
   flex: 0 0 auto; width: 7.5rem;
   padding: 0; border: 0; background: none; cursor: pointer;
   font: inherit; text-align: left; color: inherit;
   opacity: .6; transition: opacity var(--speed);
}
.camstrip-item:hover { opacity: 1; }
/* The slot is the SPAN, not the image: a camera that sends nothing must
   still leave a box to press, and an <img alt=""> with no picture paints
   nothing at all -- its own background included. */
.camstrip-pic {
   display: block; width: 100%; height: 4.25rem;
   border-radius: var(--r-sm, .375rem);
   background: #000;
   box-shadow: 0 0 0 1px var(--line);
   overflow: hidden;
}
.camstrip-pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.camstrip-name {
   display: block; margin-top: .15rem;
   font-size: var(--fs-xs, .75rem);
   white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.camstrip-item.is-on { opacity: 1; }
.camstrip-item.is-on .camstrip-pic { box-shadow: 0 0 0 2px var(--pri); }
.camstrip-item:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

/* --- the bank ---------------------------------------------------------
   Three small additions and no new decoration: an account tile is a .card
   and takes the card's own border, radius and shadow. What is added is the
   arrangement INSIDE it, and one balance figure big enough to read from
   across a desk.
   -------------------------------------------------------------------- */
.bankacc { --card-pad: .9rem; }
.bankacc-top { display: flex; align-items: baseline; gap: .5rem; }
.bankacc-top .pill { margin-inline-start: auto; }
.bankacc-sum {
   margin: .45rem 0 0;
   font-size: 1.35rem; font-weight: 600;
   font-variant-numeric: tabular-nums;
}
.bankacc-sum .sub { font-size: var(--fs-sm); font-weight: 400; }

/* The filter row's selects, sized to sit among the buttons rather than
   stretching to the width of their longest option. */
.findbox-buttons select { max-width: 14rem; }

/* A közlemény is a sentence somebody's accounting software wrote and it can
   be two hundred characters of it. One line, cut where it runs out -- the
   whole text is in the viewer, which is what the row opens. */
.clip { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 24rem; }

/* One suggested invoice, as a row that is also the button accepting it. */
.peek-suggest {
   display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap;
   padding: .35rem 0;
}
.peek-suggest + .peek-suggest { box-shadow: inset 0 1px 0 0 var(--line); }

/* --- the recording form, one step at a time ---------------------------
   A revealed step should read as having just arrived rather than as having
   always been there, so it fades in. The motion is small and it is the
   only thing announcing that the form grew.
   -------------------------------------------------------------------- */
[data-step] { animation: stepin .18s ease-out both; }

@keyframes stepin {
   from { opacity: 0; transform: translateY(-.25rem); }
   to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
   [data-step] { animation: none; }
}

/* Washpoints found by the search carry their address, which is what tells
   two "Bilk"s apart. */
[data-place-found] .picktag { display: block; margin: .1rem 0 0; }

/* --- kiosk: a 21 or 24 inch touchscreen somebody stands in front of ----
   The SAME components, scaled. Nothing here is a kiosk-only widget: the
   recording form on this screen is the recording form everybody else uses,
   and a second set of components would be a second set to fix.

   Sized in rem against a raised root, not in pixels against one screen.
   The predecessor's console is pinned -- user-scalable=no, maximum-scale=1
   -- which is why it fits one tablet and nothing else; this reflows from a
   10 inch tablet up to a 24 inch till screen, which is what the owner asked
   for.
   -------------------------------------------------------------------- */
body.kiosk {
   /* Everything downstream is in rem, so one number moves the whole screen.
      clamp() rather than a media query: the step between a 21 and a 24 inch
      panel is not a breakpoint, it is a slope.

      vmax, NOT vw. A 900x1600 screen mounted portrait is a big panel that
      happens to be narrow, and keyed on vw it landed on the minimum -- 16px
      type on a screen somebody reads from a metre away. vmax takes the
      larger side, so a tall screen scales like the wide one it is.

      AND ONLY ON A PANEL. vmax has no idea how big a screen physically is:
      a phone held upright is 414x844 and its vmax is the 844, so the kiosk
      token opened on a phone drew 21px type into a 414px column and
      everything overflowed -- which reads, correctly, as the page having
      zoomed itself in. Nothing can tell a panel from a phone except the
      narrow side, so that is what decides: below 45rem this is a phone and
      takes the ordinary type, above it this is a panel in portrait or
      landscape and takes the slope.

      AND IT HAS TO BE THE ROOT, NOT body. That was the bug in this block for
      as long as it has existed: `rem` is resolved against the ROOT element
      whatever body says, so raising body's font-size scaled every size that
      INHERITS and not one of the sizes that is MEASURED. On a 24 inch screen
      the buttons came out at 23px inside a card whose padding, gaps, grid
      floors, radii and max-width were still the sizes a 16px laptop root
      gives -- big words in small boxes. That is why the vehicle tiles cut
      their labels off mid-word, and why the step counter beside a 23px
      button stayed at 13px: var(--fs-sm) is rem, so the kiosk never touched
      it.

      html:has(> body.kiosk) is the only way to reach the root from a class
      the markup puts on body, and :has() is already load-bearing on .card.
      body keeps 1rem so it simply follows the root; writing the clamp on
      both would compound it, because the second one would resolve its own
      rem against the first. */
   font-size: 1rem;
}

html:has(> body.kiosk) { font-size: 1rem; }

@media (min-width: 45rem) {
   /* vw, not vmax. vmax takes the LARGER axis, which on a phone held
      upright is its height -- so a 430x820 screen asked for the same root
      size as a 24 inch panel and the form ran off the side of it. The
      question this scale answers is "how wide is the thing I am standing in
      front of", and that is the width. A phone lands on the 1rem floor, a
      1280 panel near the middle, a 1920 one at the ceiling. */
   html:has(> body.kiosk) { font-size: clamp(1.05rem, 0.6rem + 1.45vw, 2rem); }
}

/* ONE column. The layout is a two-column grid -- menu, then everything else
   -- and taking the sidebar out of the markup does not take its column out
   of the grid: .main simply moved into the first one and rendered the whole
   page at the width of a menu, on a 24 inch screen. */
body.kiosk .layout { grid-template-columns: minmax(0, 1fr); }
/* width:100% is doing real work. .content is a flex ITEM of .main, and an
   auto margin on the cross axis switches OFF the stretch a flex item
   normally gets -- so `margin-inline: auto` alone made it shrink to fit its
   widest child: a 416px form card sitting in the middle of a 900px screen.
   The explicit width restores the fill, and the auto margins still centre
   it once max-width bites. */
body.kiosk .content {
   width: 100%;
   max-width: 90rem;
   margin-inline: auto;
   padding: 2rem clamp(1rem, 4vw, 5rem);
}

/* The choice tiles need a wider track here than anywhere else, and the
   reason is that only one of the two numbers moved. The grid floor is 9rem
   of a root this screen has already raised, but the face on top of it also
   takes 1.05em -- so the label grew by a quarter against the tile it sits
   in, and the longest vehicle names stopped fitting. 11.5rem restores the
   proportion the same tiles have on a desk. Portrait overrides this again
   below, which is why it is written before that block and not after. */
body.kiosk .picks { grid-template-columns: repeat(auto-fill, minmax(min(11.5rem, 100%), 1fr)); }

/* Portrait. Everything on this screen is a column anyway; what changes is
   that a narrow one cannot afford side padding or multi-column grids. */
@media (orientation: portrait) {
   body.kiosk .content { padding-inline: clamp(.75rem, 3vw, 2rem); }
   body.kiosk .washtypes { grid-template-columns: minmax(0, 1fr); }
   body.kiosk .row2 { grid-template-columns: minmax(0, 1fr); }
   body.kiosk .picks { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); }
}

/* Touch targets. A finger is about 1cm; these are the two things somebody
   presses hundreds of times a day. */
body.kiosk .btn { padding: .9em 1.5em; font-size: 1.05em; }
body.kiosk .btn.lg { padding: 1.1em 2em; font-size: 1.2em; }
body.kiosk .pick > span,
body.kiosk a.pick { padding: .9em 1em; font-size: 1.05em; }
body.kiosk .wtrow { padding: .9em 1em; }
body.kiosk input,
body.kiosk select { font-size: 1.05em; padding: .7em .8em; }

/* The bar that replaces the menu: where this screen is, and the way back. */
.kioskbar {
   display: flex; align-items: center; gap: 1rem;
   padding: 1rem clamp(1rem, 4vw, 5rem);
   box-shadow: inset 0 -1px 0 0 var(--line);
   font-size: 1.05em;
}
.kiosk-home { font-weight: 600; text-decoration: none; }

/* Both of these are pressed with a finger. The way out is a 20px gear glyph
   and was therefore a 20px target on a screen nobody is holding a mouse at;
   the box round it is the 2.75rem a touch target asks for, and the glyph
   inside it is unchanged. */
.kiosk-home, .kiosk-exit {
   display: inline-flex;
   align-items: center;
   justify-content: center;
   min-height: 2.75rem;
}
.kiosk-exit { min-width: 2.75rem; }

/* The welcome screen is a heading, a name and one button. Left at the top of
   a 1080-high panel they sat in the first third with two thirds of empty
   page under them, which reads as a screen that has not finished loading.
   They centre in the height instead. A min-height rather than a full one, so
   that a short portrait panel does not push the button off the bottom. */
.kiosk-hero {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   min-height: min(62vh, 34rem);
   text-align: center;
   padding: clamp(1.5rem, 5vw, 4rem) 0 2rem;
}
.kiosk-hero h1 { font-size: 2.2em; margin: 0 0 .2em; }
.kiosk-hero .sub { font-size: 1.2em; margin: 0 0 2rem; }
.kiosk-go { font-size: 1.4em; padding: 1em 2.5em; }

.kiosk-print { --card-pad: clamp(1rem, 3vw, 2rem); }
.kiosk-plate {
   margin: .5rem 0 1.25rem;
   font-size: 2em; font-weight: 700; letter-spacing: .06em;
   text-transform: uppercase;
}

/* --- the on-screen keyboard -------------------------------------------
   Fixed to the bottom, because the field being typed into is above it and
   a keyboard that moves with the page is one somebody chases.
   -------------------------------------------------------------------- */
.vkb {
   position: fixed; inset-inline: 0; bottom: 0; z-index: 40;
   padding: .6rem clamp(.5rem, 2vw, 2rem) calc(.6rem + env(safe-area-inset-bottom, 0px));
   background: var(--paper);
   box-shadow: 0 -.5rem 1.5rem -.5rem rgba(0, 0, 0, .25), inset 0 1px 0 0 var(--line);
}
.vkb-rows { display: flex; flex-direction: column; gap: .4rem; max-width: 60rem; margin-inline: auto; }
.vkb-row  { display: flex; gap: .4rem; justify-content: center; }
.vkb-key {
   flex: 1 1 0; min-width: 0;
   padding: .7em .2em;
   font: inherit; font-weight: 600;
   color: var(--ink);
   background: var(--pick-bg);
   border: 0; border-radius: var(--r-sm, .375rem);
   box-shadow: inset 0 0 0 1px var(--pick-line);
   cursor: pointer;
}
.vkb-key:active { background: var(--pick-bg-hov); }
.vkb-space { flex: 4 1 0; }
.vkb-wide  { flex: 1.6 1 0; }
.vkb-done  { flex: 2 1 0; background: var(--btn-grad); color: var(--on-pri); box-shadow: none; }

/* Room to scroll the field clear of the keyboard. */
/* What the keyboard actually takes, measured by app.js and written here as
   --vkb-h. It used to be a flat 22rem, which was right for one font size on
   one screen -- and the keyboard grows with the kiosk's type scale, so the
   last row of the form ended up underneath it. The fallback is the old
   number, for the moment before the first measurement lands. */
body.has-vkb .content { padding-bottom: calc(var(--vkb-h, 22rem) + 1.5rem); }

/* The kiosk's time steppers. Their own line below the value, because four
   buttons beside a date and an edit button is a row nobody can aim at. */
.whensteps { display: flex; flex-wrap: wrap; gap: .4rem; flex-basis: 100%; }

/* A money summary wraps rather than running off the card: on a narrow
   screen "Nettó: 15 433 Ft  Bruttó: 19 600 Ft" is two lines, not one line
   and an overflow. */
.peek-total { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .25rem 1rem; }

/* Net and gross on one line, and gross is the quieter of the two.
   -------------------------------------------------------------------------
   The owner asked for both figures on the wash viewer. They are 27% apart,
   so the danger is not that one is missing -- it is that a reader takes the
   wrong one for the total. Hence a label on each, and a weight difference:
   net is the figure every other screen agrees with, gross is the one the
   invoice and the DKV card carry. Same treatment in the card footer of the
   full page, so the two screens read alike. */
.peek-gross,
.foot-gross { color: var(--muted); white-space: nowrap; }

/* The same pair in the full page's card footer. It wraps as one block --
   see the note in the view. */
.foot-money {
   display: flex; flex-wrap: wrap; justify-content: flex-end;
   gap: .25rem 1rem;
}

/* An issued invoice, shown on the screen that issued it.
   -------------------------------------------------------------------------
   The number is read out loud over a counter, so it is set large enough to
   read at arm's length on a kiosk; the document is embedded rather than
   linked because the browser's own PDF viewer carries the print button, and
   on a touchscreen with no desktop a downloaded file goes nowhere anybody
   can find it. White ground in both themes: it is a sheet of paper. */
.docref { font-size: 1.5rem; letter-spacing: .04em; }

.docview {
   display: block;
   width: 100%;
   height: min(70vh, 48rem);
   margin-top: 1.25rem;
   border: 1px solid var(--line);
   border-radius: var(--r);
   background: #fff;
}

body.kiosk .docview { height: 58vh; }

/* The document viewer window.
   -------------------------------------------------------------------------
   The same iframe, in a modal that also has a head and a foot, so the
   frame takes what is left rather than the 70vh it gets on a page of its
   own -- otherwise the print button is below the fold on a laptop, which
   is the one button somebody opened this to press. */
.docmodal .modal-card { max-width: 52rem; }
.docmodal .docview { margin-top: 0; height: min(54vh, 34rem); }
/* The number sits BESIDE the word, not across the head: the h3 would
   otherwise take the whole row and push it into the middle, where it
   reads as a second heading rather than as this document's number. */
.docmodal .modal-head h3 { flex: 0 0 auto; }
.docmodal .docref { font-size: var(--fs-sm); color: var(--muted); }
.docmodal .buyerbox { margin-top: 1rem; }

/* The other half of a reversed pair: the number a storno cancelled, or the
   storno that cancelled this one. Its own line under the word, because it
   is the answer to a different question from the one the cell is headed
   with, and because two numbers side by side in a narrow column read as
   one long number. */
.docpair { display: block; font-size: var(--fs-xs); color: var(--muted); }

@media (max-width: 40rem) {
   /* A phone has no room for a PDF and a form at once. The document is
      still here to check the number against, and printing opens the
      browser's own viewer anyway. */
   .docmodal .docview { height: 38vh; }
}

/* Deleting from the wash viewer.
   -------------------------------------------------------------------------
   One line: the word, the reason, the button. The reason has to fit a
   sentence, so it takes whatever is left -- and wraps to its own line on a
   phone rather than squeezing the button off the edge. */
.peek-del {
   display: flex;
   flex-wrap: wrap;
   gap: .4rem;
   align-items: center;
   margin: .6rem 0 0;
}

.peek-del .lbl { color: var(--muted); font-size: var(--fs-xs); font-weight: 500; }

.peek-del input[type="text"] {
   flex: 1 1 10rem;
   min-width: 0;
   padding: .35rem .6rem;
   border: 1px solid var(--line);
   border-radius: var(--r-sm);
   background: var(--paper);
   color: var(--ink);
   /* NO font-size of its own. See the touch rule at the end of this sheet:
      anything under 16px makes iOS zoom the page in on focus. */
   font: inherit;
}

.peek-del input[type="text"]:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

/* The washpoint's own panel, as separate cards.
   -------------------------------------------------------------------------
   It used to be one card carrying the day's figures, a month of days, today's
   hours, the rate and the last washes -- a wall somebody had to read through
   to reach the one number they wanted. Five questions are five cards.

   The arrangement on a wide screen:

        +---------------------------+  +----------+
        | name, today, this month   |  | RECORD   |
        | thirty days               |  |          |
        +-------------+-------------+  | camera   |
        | today's hrs | rate        |  |          |
        +-------------+-------------+  +----------+
        | the last washes                          |
        +------------------------------------------+

   The action card spans both chart rows because the picture under the button
   wants the height, and because what somebody came here to DO should not be
   a thing they scroll past.

   On a narrow screen it is one column in DOM order, which puts the button
   first -- on a phone at a bay that is the right answer. */
.sitegrid {
   display: grid;
   gap: var(--gap);
   grid-template-columns: minmax(0, 1fr);
}

/*
   36rem and not 60rem, and the reason is worth keeping because it is
   backwards from what the number suggests.

   The sidebar appears at 60rem and takes 284px of it. So a 900px window has
   853px of CONTENT and got ONE column, while a 1000px window has 716px of
   content and got TWO -- the layout went from wide-and-stacked to
   narrow-and-split as the window GREW, and the widest single-column camera
   on the whole site was the one at 900px, 853 by 640, which is what the
   owner was looking at.

   Measured, at a 900px window: 320 for the yard, 509 for the graphs. That is
   more room for a thirty-day line than the same page gives it at 1000px,
   where the sidebar has taken its share first.

   It was 48rem, and the screen that proved it too high is the one bolted to
   the tiles at Nagykőrösi út: a portrait monitor on a Pi, about 600 CSS
   pixels wide. One column there meant a 549px camera 412px tall, the only
   button on the screen 640px down the page, and 2888px of document on a
   screen that shows 1000 -- a wall-mounted display somebody has to scroll.
   Split, the same screen puts the picture and the button side by side and
   the day's figures beside them, all above the fold.

   Below 36rem the stack is still right: that is a phone at a bay, and the
   button belongs first.
*/
@media (min-width: 36rem) {
   /* Two columns, not three: a narrow one for the things you LOOK AT or
      PRESS -- the yard, the button, the rate -- and a wide one for the two
      graphs, which are the only things here that need the width to say
      anything. A thirty-day line squeezed into a third of a row is a
      squiggle.

      TWO ROWS SHARED BETWEEN THEM, which is the whole reason this is one
      grid and not two stacks side by side. Stacked independently, each
      column's card edges fell wherever its own contents happened to end and
      nothing lined up across the gutter. Named rows make the camera-plus-
      button block end exactly where the month's graph ends, and the rate end
      exactly where the day's does. */
   .sitegrid {
      /* The narrow column is a PROPORTION with a ceiling, not a fixed
         20rem. Fixed, it ate 320 of the 549 a 600px screen has and left
         the thirty-day line 205px to draw in -- the squiggle this comment
         warns about, arrived at from the other direction. As a share it
         gives up width with the screen and still stops at 20rem, so every
         window wider than about 790px is laid out exactly as it was
         before -- that is where 45% reaches the old 320 and the cap takes
         over. */
      grid-template-columns: minmax(0, min(45%, 20rem)) minmax(0, 1fr);
      grid-template-rows: auto auto auto;
      align-items: stretch;
   }
   /* The notes run the full width above everything. They are the one thing
      on this screen that somebody else wrote TO the reader, and a column
      would make them look like another statistic. */
   .sitegrid .sg-notes  { grid-column: 1 / -1; grid-row: 1; }
   .sitegrid .sg-top    { grid-column: 1; grid-row: 2; }
   .sitegrid .sg-now    { grid-column: 2; grid-row: 2; }
   .sitegrid .sg-gauge  { grid-column: 1; grid-row: 3; }
   .sitegrid .sg-hours  { grid-column: 2; grid-row: 3; }
   /* Today's claims run the full width, above the recent washes and below
      everything that is about this morning's figures. Half a row would put
      the one thing on this screen that is about the NEXT few hours in a
      narrow column -- and the plate, the contact and the done mark are
      three columns of a list, which is a width, not a panel. */
   .sitegrid .sg-slots  { grid-column: 1 / -1; grid-row: 4; }
   .sitegrid .sg-recent { grid-column: 1 / -1; grid-row: 5; }

   /* A yard that has never washed has no figures, no rate and no hourly
      chart -- the grid is then the notes and this block, and a narrow
      column with half the screen empty beside it is worse than the stack
      it replaced. So it takes the width and puts the picture and the
      button side by side instead.

      :has() asks the question the grid cannot: whether there is anything
      in the other column at all. */
   .sitegrid:not(:has(.sg-now)) .sg-top {
      grid-column: 1 / -1;
      flex-direction: row;
      align-items: stretch;
   }
   .sitegrid:not(:has(.sg-now)) .sg-top > * { flex: 1 1 0; min-width: 0; }

   /* The picture takes whatever the row leaves after the button, so the
      column ends level with the graph beside it rather than wherever a
      4:3 frame happened to stop. */
   .sg-top .sg-camcard { flex: 1 1 auto; min-height: 8rem; }
   .sg-top .sg-camcard .campic,
   .sg-top .sg-camcard .campic img { height: 100%; }
   .sg-top .sg-camcard .campic img { aspect-ratio: auto; }
}

/* The top of the narrow column: the yard, and the thing to press. */
.sg-top { display: flex; flex-direction: column; gap: var(--gap); }

/* THE CARD IS THE BUTTON.
   -------------------------------------------------------------------------
   Not a button inside a card. At a washpoint there is exactly one thing to
   press, and a small control floating in a big white box reads as one option
   among several rather than as the point of the screen. It takes the primary
   button's own fill, so it cannot drift from it when the brand moves.

   --on-pri and not #fff: the dark theme's primary is a LIGHT green and takes
   dark ink. Hardcoding white there measures 1.95, which is the trap --on-pri
   exists to close. */
.actcard {
   display: flex;
   align-items: center;
   justify-content: center;
   gap: .6rem;
   /* Less tall, at the owner's word. It was 7.5rem, which at a glance read
      as a panel rather than a control; the icon does the work the height was
      doing. */
   min-height: 4.5rem;
   padding: 1rem 1.25rem;
   background: var(--btn-grad);
   box-shadow: 0 .25rem .75rem -.125rem var(--btn-glow);
   color: var(--on-pri);
   text-align: center;
   text-decoration: none;
   cursor: pointer;
   transition: transform .12s ease, box-shadow .12s ease;
}

.actcard:hover { color: var(--on-pri); box-shadow: 0 .4rem 1rem -.125rem var(--btn-glow); }
.actcard:active { transform: translateY(1px); }
.actcard:focus-visible { outline: 2px solid var(--on-pri); outline-offset: -4px; }

.actcard-label { font-size: 1.125rem; font-weight: 600; letter-spacing: .01em; }
.actcard .bico { width: 1.5rem; height: 1.5rem; }

@media (prefers-reduced-motion: reduce) {
   .actcard { transition: none; }
   .actcard:active { transform: none; }
}

/* The picture is a card in its own right, square-ish rather than 16/9: a
   column one third of a row wide is tall and narrow, and a letterbox in it
   is a stripe. */
.sg-camcard { overflow: hidden; }
.sg-camcard .campic img { aspect-ratio: 4 / 3; }

/* A chart in a card of its own has no heading to sit under, so it carries
   its own caption and needs no extra room above it. In the wide column both
   graphs take the whole width -- that width is the reason they are there. */
.sg-hours svg, .sg-gauge svg, .sg-now svg { display: block; width: 100%; height: auto; }

/* Stretched to its neighbour's height, a chart card has room to spare. The
   drawing sits in the middle of it rather than clinging to the top edge with
   a band of nothing underneath. */
.sg-gauge, .sg-hours { display: flex; flex-direction: column; justify-content: center; }

/* Deleting a wash from inside the viewer.
   -------------------------------------------------------------------------
   The button sits in the foot beside Módosítás -- the two things somebody
   does TO a wash, where a reader looking for one is looking for the other --
   and opens this rather than submitting. The reason IS the decision, so it
   belongs in front of the person making it rather than behind a browser
   confirm that asks nothing anybody has to think about.

   While it is open the rest of the viewer fades back. Not hidden: the reason
   somebody writes is about what they can see, and taking the wash away while
   they describe it would be perverse. Faded is enough to stop it competing
   with the one field that now matters. */
.peekmodal-body.is-deleting > *:not(.peek-delpane) {
   opacity: .3;
   filter: saturate(.4);
   pointer-events: none;
   transition: opacity .18s ease, filter .18s ease;
}

.peek-delpane {
   display: flex;
   flex-direction: column;
   gap: .5rem;
   margin-top: 1rem;
   padding: 1rem;
   border-radius: var(--r);
   /* Its own ground, so it reads as a thing that opened ON the viewer rather
      than as more of the viewer. --paper-2 and a rim rather than a shadow:
      inside a dialog a shadow has nothing to cast onto. */
   background: var(--paper-2);
   box-shadow: inset 0 0 0 1px var(--line);
}

.peek-delhead { margin: 0; font-weight: 600; }

.peek-dellabel { color: var(--muted); font-size: var(--fs-xs); font-weight: 500; }

.peek-delpane input[type="text"] {
   width: 100%;
   padding: .5rem .65rem;
   border: 1px solid var(--line);
   border-radius: var(--r-sm);
   background: var(--paper);
   color: var(--ink);
   /* NO font-size of its own. See the touch rule at the end of this sheet:
      anything under 16px makes iOS zoom the page in on focus. */
   font: inherit;
}

.peek-delpane input[type="text"]:focus-visible {
   outline: 2px solid var(--pri);
   outline-offset: 1px;
}

.peek-delact { display: flex; flex-wrap: wrap; gap: .5rem; }

@media (prefers-reduced-motion: reduce) {
   .peekmodal-body.is-deleting > *:not(.peek-delpane) { transition: none; }
}

/* The price table in the viewer.
   -------------------------------------------------------------------------
   Four columns -- the wash type and three figures -- and the general rule
   stacks any four-column list below 28rem, which is most of a phone. That
   threshold is right for a listing whose values are plates and company names
   and wrong for this one: a VAT rate is three characters and a price is six,
   so the row fits long before 28rem and stacking it spends four lines saying
   what fits on one.

   19rem, measured against the narrowest of them: "Mosástípus" needs the
   remaining track and the three figures need about 4rem each. */
@container dlw (min-width: 19rem) {
   .dlist.pricelist {
      --dl-tpl: minmax(0, 1fr) auto auto auto;
      --dl-x: .625rem;
      --stack: 0;
   }
}

/* iOS zooms the page in when a form control under 16px takes focus.
   -------------------------------------------------------------------------
   And it does not zoom back out. Tapping the reason field in the wash viewer
   magnified the whole modal and left it that way, which is what the owner saw
   -- but the cause is not that panel: --fs is 15px, so EVERY text field on
   the site did it, and the modal only made it obvious because there is
   nowhere to scroll back to.

   16px exactly, and only where the pointer is coarse: on a mouse, 15px is the
   size the rest of the type was drawn against and there is nothing to fix. */
@media (pointer: coarse) {
   input[type=text], input[type=email], input[type=password], input[type=date],
   input[type=number], input[type=search], input[type=tel], input[type=url],
   input[type=datetime-local], input[type=time], input[type=month],
   select, textarea {
      font-size: 16px;
   }
}

/* The period picker.
   -------------------------------------------------------------------------
   Replaces the browser's own date inputs, which drew mm/dd/yyyy on an English
   machine and opened a calendar nothing here could style. A button showing
   the chosen period, and a panel holding the ranges people actually ask for
   beside a calendar for the days they do not.

   The ranges sit on the LEFT because they answer the question nine times in
   ten; the calendar is there for the tenth. */
.daterange { position: relative; display: inline-flex; }

.daterange-open {
   display: inline-flex;
   align-items: center;
   gap: .45rem;
   min-width: 0;
   padding: .5rem .8rem;
   border: 0;
   border-radius: var(--r);
   background: none;
   color: inherit;
   font: inherit;
   font-size: var(--fs);
   white-space: nowrap;
   cursor: pointer;
}

.daterange-open:hover { background: var(--paper-2); }
.daterange-open:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }
.daterange-open .bico { width: 1.05em; height: 1.05em; color: var(--pri); }

.drmenu {
   position: absolute;
   z-index: 40;
   top: calc(100% + .35rem);
   inset-inline-end: 0;
   display: grid;
   grid-template-columns: auto auto;
   gap: .75rem;
   padding: .6rem;
   border-radius: var(--r);
   background: var(--paper);
   box-shadow: var(--sh-lg), 0 0 0 1px var(--line);
}

.drquick {
   display: grid;
   grid-template-rows: repeat(8, auto);
   align-content: start;
   gap: .1rem;
   min-width: 9.5rem;
}

.drmenu-item {
   padding: .4rem .6rem;
   border: 0;
   border-radius: var(--r-sm);
   background: none;
   color: inherit;
   font: inherit;
   font-size: var(--fs-sm);
   text-align: left;
   white-space: nowrap;
   cursor: pointer;
}

.drmenu-item:hover { background: var(--paper-2); }
.drmenu-item:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }

/* "No filter" is not a period, so it sits apart from the ones that are. */
.drmenu-clear {
   margin-top: .2rem;
   padding-top: .5rem;
   border-top: 1px solid var(--line);
   border-radius: 0;
   color: var(--muted);
}

.drcal { min-width: 15rem; }

.drcal-head {
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: .4rem;
   padding: .1rem .15rem .45rem;
}

.drcal-nav {
   width: 1.75rem; height: 1.75rem;
   border: 0;
   border-radius: var(--r-sm);
   background: none;
   color: var(--muted);
   font-size: 1.25rem;
   line-height: 1;
   cursor: pointer;
}

.drcal-nav:hover { background: var(--paper-2); color: var(--ink); }
.drcal-nav:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }

.drcal-grid {
   display: grid;
   grid-template-columns: repeat(7, 1fr);
   gap: 1px;
}

.drcal-dow {
   padding: .2rem 0;
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-align: center;
}

.drcal-day {
   padding: .4rem 0;
   border: 0;
   border-radius: var(--r-sm);
   background: none;
   color: inherit;
   font: inherit;
   font-size: var(--fs-sm);
   font-variant-numeric: tabular-nums;
   text-align: center;
   cursor: pointer;
}

.drcal-day:hover { background: var(--paper-2); }
.drcal-day:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }

/* Today is marked, not selected -- a ring rather than a fill, so it still
   reads as today once it is inside a chosen period. */
.drcal-day.is-today { box-shadow: inset 0 0 0 1px var(--pri); }

/* The days between the two ends carry a wash of the brand colour; the two
   ends themselves carry it properly. --on-pri for the ink, because the dark
   theme's primary is a LIGHT green and takes dark text. */
.drcal-day.is-in { background: color-mix(in srgb, var(--pri) 16%, transparent); border-radius: 0; }
.drcal-day.is-end { background: var(--pri); color: var(--on-pri); }
.drcal-day.is-first { border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm); }
.drcal-day.is-last  { border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm); }

.drcal-hint { margin: .5rem 0 0; color: var(--muted); font-size: var(--fs-xs); }

/* On a phone it is a sheet at the bottom of the screen: the panel is wider
   than the 102px button it hangs off, and the bar it sits in is already full.
   The calendar goes under the ranges rather than beside them.

   position: FIXED, and that is the whole fix.

   It was absolute, with .daterange set to position:static so the panel could
   span the card. That left the nearest positioned ancestor as <body> -- so
   `top: calc(100% + .35rem)` meant 100% of the DOCUMENT, and the panel opened
   at y=906 in a 900px viewport. It opened on every tap; there was simply
   nowhere on screen to see it, which reads as a button that does nothing.

   Fixed needs no positioned ancestor at all, so there is nothing left to get
   wrong. */
@media (max-width: 40rem) {
   .drmenu {
      position: fixed;
      inset-inline: .75rem;
      top: auto;
      bottom: .75rem;
      max-height: 80vh;
      overflow-y: auto;
      grid-template-columns: minmax(0, 1fr);
      gap: .5rem;
   }
   .drquick {
      grid-template-rows: none;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      min-width: 0;
   }
   .drmenu-clear { grid-column: 1 / -1; }
   .drcal { min-width: 0; }
   /* A finger, not a cursor. */
   .drmenu-item { padding: .6rem .7rem; font-size: var(--fs); }
   .drcal-day { padding: .55rem 0; font-size: var(--fs); }
}

/* The kiosk's step bar.
   -------------------------------------------------------------------------
   Only on a kiosk, because only there is a step hidden. It says which
   question this is of how many -- a washer who cannot see the rest of the
   form has no other way to know whether they are nearly done -- and carries
   the way back, without which a mistyped plate would mean abandoning the
   wash and starting again. */
.stepbar {
   margin-bottom: 1.25rem;
   padding-bottom: .75rem;
   border-bottom: 1px solid var(--line);
}

.steprow { display: flex; align-items: center; gap: .75rem; }

.stepwhere { color: var(--muted); font-size: var(--fs-sm); font-weight: 500; }

/* What has already been answered. Wraps rather than scrolls: five short
   answers fit a panel's width, and a strip that scrolls sideways on a
   touchscreen is a strip that gets swiped by accident. */
.stepdone { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .6rem; }

.stepdone:empty { display: none; }

.stepchip {
   display: inline-flex;
   align-items: baseline;
   gap: .4rem;
   max-width: 100%;
   padding: .3rem .6rem;
   border: 0;
   border-radius: var(--r-sm);
   background: var(--paper-2);
   color: inherit;
   font: inherit;
   font-size: var(--fs-sm);
   text-align: left;
   cursor: pointer;
}

.stepchip:hover { box-shadow: inset 0 0 0 1px var(--line); }
.stepchip:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

.stepchip-k { color: var(--muted); font-size: var(--fs-xs); white-space: nowrap; }

/* The answer is the part worth reading, so it keeps the room and the label
   does not. A long wash-type list is cut rather than allowed to wrap the
   chip into a paragraph. */
.stepchip-v {
   min-width: 0;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   font-weight: 500;
}

/* A disabled way out should read as unavailable, not as missing: a button
   that vanishes moves everything beside it. It used to be faded to .35 here
   as well as the .55 every other disabled button got, which on a kiosk left
   "Vissza" as a ghost of a word on the first step. The shared disabled rule
   now recolours rather than fades, so this one only has to keep the button
   in the row. */
.stepbar .btn[disabled] { pointer-events: none; }

/* One question on a screen this size has room to breathe, and the thing
   being answered should be the thing the eye lands on. */
body.kiosk .formstep { margin-block: .5rem 1.5rem; }

/* The step bar stays put while a long step scrolls under it, and the way on
   is repeated at the foot of the step. Between them there is never a moment
   where somebody has answered a question and can see nothing to press. */
body.kiosk .stepbar {
   position: sticky;
   top: 0;
   z-index: 5;
   /* It has to be opaque, or the step scrolls through it -- but --paper is
      not what a card looks like. The card is --glass-card over the page with
      a brand wash down its top edge, and a solid white bar inside it
      measured 255,255,255 against the 237,242,242 around it: a white block
      sitting in a card, with a green stripe of untouched card showing above.

      So the bar paints what the card paints: the same glass fill composited
      over --bg to make it opaque, with the same sheen on top. And it starts
      at the card's own top edge rather than inside its padding, so the two
      gradients begin at the same line and there is nothing left to show
      between them. The top padding gives back the inset the negative margin
      took away. */
   background-color: var(--bg);
   background-image: var(--card-sheen),
                     linear-gradient(var(--glass-card), var(--glass-card));
   border-radius: var(--r) var(--r) 0 0;
   margin-inline: calc(var(--card-pad, 1.5rem) * -1);
   margin-block-start: calc(var(--card-pad, 1.5rem) * -1);
   padding: calc(var(--card-pad, 1.5rem) + .25rem) var(--card-pad, 1.5rem) .75rem;
}

.stepgo { display: flex; justify-content: flex-end; margin-top: 1.25rem; }
.stepgo .btn { min-width: 10rem; }

/* What the VAT lookup found, before anything is written into the form.
   -------------------------------------------------------------------------
   Its own ground, because it is an ANSWER rather than another field: the
   operator reads it, decides, and presses. Nothing above it moves until they
   do. */
.vatfound {
   margin: .25rem 0 1.25rem;
   padding: 1rem;
   border-radius: var(--r);
   background: var(--paper-2);
   box-shadow: inset 0 0 0 1px var(--line);
}

.vatfound-rows {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
   gap: .5rem 1.25rem;
}

.vatfound-rows > div { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.vatfound-rows strong { overflow-wrap: anywhere; }

/* The line VIES actually returned, under the split of it. The split is a
   guess across twenty-seven countries that write addresses differently, and
   somebody correcting it should be able to see what it was guessing from. */
.vatfound-raw {
   margin: .75rem 0 0;
   padding-top: .6rem;
   border-top: 1px solid var(--line);
   color: var(--muted);
   font-size: var(--fs-sm);
   overflow-wrap: anywhere;
}

.vatfound-raw .lbl { display: block; }
.vatfound .actions { margin-top: .9rem; }

/* The lanes, on the recording form.
   -------------------------------------------------------------------------
   One button per lane, and pressing one shows what is standing in it. The
   pictures sit ABOVE the plate boxes they belong to -- the registration above
   the first, the trailer above the second -- which is the predecessor's
   arrangement and works because the eye goes picture, box, picture, box. */
.lanebox { margin-top: 1rem; }

.lanepicks { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .35rem; }
.lanepick.is-on { box-shadow: inset 0 0 0 2px var(--pri); }

.laneshots {
   display: grid;
   grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
   gap: .75rem;
   margin-top: .75rem;
}

.laneshot { margin: 0; }

.laneframe {
   display: block;
   width: 100%;
   padding: 0;
   border: 0;
   border-radius: var(--r-sm);
   overflow: hidden;
   background: none;
   cursor: zoom-in;
}

.laneframe:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

.laneshot img {
   display: block;
   width: 100%;
   aspect-ratio: 5 / 4;
   object-fit: contain;
   /* Black, like the predecessor's: a camera picture letterboxed against the
      card's own ground reads as a broken image. */
   background: #11171b;
}

/* THE SHAPE FOLLOWS WHAT THE SERVER SENT.
   -------------------------------------------------------------------------
   Two different pictures arrive here and they want different boxes, so the
   answer says which one it is -- `found` on the lane reply, `is-plate` here
   -- and the box follows.

   A PLATE CROP is the plate and a tenth of the frame around it, which is
   wide and shallow: in a 5:4 box it was three quarters black and took a
   screenful to show a number plate, which is what the owner asked to stop.
   5:2 is about the shape of the crop, so it nearly fills it, and the block
   is HALF AS TALL as it was.

   A WHOLE FRAME keeps 5:4, and that is not an oversight. It arrives when no
   plate was found, or when no finder is configured, and that is exactly
   when the picture is the only evidence there is: "the camera is pointed at
   a wall" has to be visible in it. Cropping it into a letterbox to save the
   same room would take away the one thing it is for.

   contain either way, because a picture squeezed to fit a box is a plate
   somebody reads wrong. */
.laneshot.is-plate img { aspect-ratio: 5 / 2; }

.laneshot figcaption {
   margin-top: .35rem;
   color: var(--muted);
   font-size: var(--fs-sm);
}


/* What the washpoint has been told.
   -------------------------------------------------------------------------
   The one thing on a dashboard that a person wrote to the reader, so it does
   not look like another statistic: no heading, no chrome, just the words and
   who said them. A pinned note carries a brand-coloured edge -- "the lane is
   closed" should not look like every other line. */
.notelist { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }

.note {
   padding-left: .9rem;
   border-left: 3px solid var(--line);
}

.note.is-here { border-left-color: var(--pri); }

.note-body { margin: 0; overflow-wrap: anywhere; }
.note-body:first-letter { text-transform: none; }

.note-by { margin: .2rem 0 0; color: var(--muted); font-size: var(--fs-sm); }
/*
   The noticeboard, edited where it stands.

   One note, at the top of the dashboard, with a pencil in its corner that
   turns the text into a box and back. No second page and no dialog: the
   owner's words were "simply in the page, very easily editable".

   The head is a flex row with the pencil pushed to the end, so the button
   sits in the top-right corner of the card whatever the note says and
   however many lines it runs to.
*/
.noteboard { position: relative; }
.nb-head {
   display: flex;
   align-items: center;
   gap: .5rem;
   margin-bottom: .45rem;
}
.nb-ttl {
   color: var(--muted);
   font-size: var(--fs-xs);
   font-weight: 500;
   text-transform: uppercase;
   letter-spacing: .04em;
}
.nb-pen {
   margin-inline-start: auto;
   display: inline-flex;
   align-items: center;
   justify-content: center;
   width: 1.9rem;
   height: 1.9rem;
   padding: 0;
   border: 0;
   border-radius: var(--r-sm);
   background: transparent;
   color: var(--muted);
   cursor: pointer;
   font-size: 1rem;
}
.nb-pen:hover,
.nb-pen:focus-visible { background: var(--pick-bg); color: var(--ink); }
.nb-pen .ico-print { margin: 0; }

.nb-none { margin: 0; }

.nb-area {
   width: 100%;
   min-height: 5.5rem;
   resize: vertical;
}
.nb-box .actions { margin-top: .5rem; }
.nb-state { color: var(--dng); font-size: var(--fs-sm); }

/* The network's note: somebody else's board, shown under this one and
   fenced off from it so the two are not read as one message. */
.nb-net {
   margin-top: .75rem;
   padding-top: .7rem;
   border-top: 1px solid var(--line);
}


body.kiosk .note-body { font-size: 1.05em; }

/* Taking a DKV card payment.
   -------------------------------------------------------------------------
   The portal takes the better part of a minute and nobody is holding a
   request open for it, so the screen says where it has got to. A minute of
   nothing is a minute somebody assumes it failed and presents the card
   again. */
.dkvsum { font-size: 1.5rem; font-variant-numeric: tabular-nums; }

.dkvstep { margin: .5rem 0 .6rem; font-weight: 500; }

.dkvbar {
   height: .5rem;
   border-radius: 999px;
   background: var(--paper-2);
   box-shadow: inset 0 0 0 1px var(--line);
   overflow: hidden;
}

.dkvbar > span {
   display: block;
   width: 10%;
   height: 100%;
   background: var(--pri);
   transition: width .4s ease;
}

.dkvrun.is-paid    .dkvbar > span { background: var(--pri); }
/* --dng, not --danger: there has never been a token of that name, so a
   refused card painted the hardcoded fallback -- a brick red that belongs to
   neither theme and does not move with the dark one. */
.dkvrun.is-refused .dkvbar > span { background: var(--dng); }

.dkvcode {
   margin: .75rem 0 0;
   font-family: var(--font-mono);
   font-size: 1.1rem;
   overflow-wrap: anywhere;
}

.dkvrun .actions { margin-top: 1.1rem; }

/* A bay, a card and wet hands. */
body.kiosk #d-card { font-size: 1.4rem; padding: .9rem 1rem; letter-spacing: .06em; }

@media (prefers-reduced-motion: reduce) {
   .dkvbar > span { transition: none; }
}

/* --- a network's mark on the touchscreen -------------------------------
   Height-bounded, never width-bounded: logos arrive in every aspect ratio
   anybody's designer felt like, and a width cap turns a wide wordmark into
   a tall one that pushes the bar open. object-fit keeps whatever shape it
   came in within that height.
   ------------------------------------------------------------------- */
.kiosk-logo {
   height: 2rem; width: auto; max-width: 12rem;
   object-fit: contain; display: block;
   margin-inline-end: .75rem;
}

/* The same picture on the form that uploads it, where the question is "is
   this the right image" rather than "does it fit a bar". */
.logopreview {
   max-height: 5rem; max-width: 100%; width: auto;
   object-fit: contain; display: block;
   border-radius: var(--r-sm, .375rem);
}

/* --- one header on a touchscreen, not three ---------------------------
   The kiosk bar already says where this machine is. The page then printed
   its own title and a subtitle under it -- "Mosas rogzitese" over "Bilk .
   AXH" -- so the washpoint's name appeared twice and the network's once,
   above a step bar that says which question you are on. Three bands of
   chrome before the first thing you can touch, on a screen that does one
   job.

   The bar and the step bar stay; the page head goes. Nothing is lost: the
   bar carries the place, the step bar carries the step, and what the screen
   is for is the only thing it ever does.
   ------------------------------------------------------------------- */
body.kiosk .page-head { display: none; }

/* The bar itself, kept to one line and small. It is a signpost, not a
   title: the content below it is what somebody came to touch. */
body.kiosk .kioskbar {
   padding-block: .35rem;
   min-height: 0;
   /* Pinned. On a panel the form is taller than the screen, and the one
      line that says where this machine is and how to get back to the start
      was the first thing to scroll away. */
   position: sticky; top: 0; z-index: 30;
   background: var(--paper);
   box-shadow: 0 .4rem 1rem -.6rem rgba(0, 0, 0, .25), inset 0 -1px 0 0 var(--line);
}
body.kiosk .kioskbar strong { font-size: var(--fs-sm); font-weight: 600; }
body.kiosk .kiosk-logo { height: 1.6rem; }

/* --- the keyboard is sized by the SCREEN, not by the page -------------
   Its keys take `font: inherit`, so when the kiosk root grew to 28px the
   keyboard grew with it: 491px of a 1024px panel, half the screen, with the
   form squeezed into what was left.

   A key has to be big enough for a thumb and no bigger. Sizing it from the
   viewport height keeps it around a quarter of the screen whatever the page
   type scale is doing, which is the only number that matters here -- the
   question is "how much of this screen is keyboard", and that is a share of
   its height.
   ------------------------------------------------------------------- */
body.kiosk .vkb { font-size: clamp(14px, 2.1vh, 22px); }
body.kiosk .vkb-key { padding: .6em .2em; }

/* The lane pictures on a panel, where height is the scarce thing.
   -------------------------------------------------------------------------
   The shape is a ratio, so on a 24 inch panel a two-column grid gives each
   picture an 800 pixel column and the ratio asks for 320 or 650 pixels of
   height to go under it. Everything else here scales with the root and
   should; this cannot, because with the keyboard out the content is its own
   scrolling box of a FIXED height -- the screen, less the bar, less the
   keyboard -- and a picture that grew with the type scale took the room the
   questions need. The washer then scrolled to reach the plate box directly
   beneath the picture of the plate.

   A share of the viewport, then, since the question being answered is "how
   much of this screen is camera". rem as well as vh, so a small panel does
   not get a picture sized for a big one. */
body.kiosk .laneshot img { max-height: min(22rem, 34vh); }
body.kiosk.has-vkb .laneshot img { max-height: min(15rem, 24vh); }

/* --- the keyboard is part of the layout, not something on top of it ---
   On a bay panel the keyboard is always out, so treating it as an overlay
   meant the form ran underneath it and the field being typed into could sit
   behind the keys. Reserving padding at the end let everything be SCROLLED
   clear, which is not the same as being clear -- it put the work on the
   washer.

   So the content takes the room that is actually left: the screen, minus
   the bar at the top, minus the keyboard at the bottom, and scrolls inside
   that. Nothing can be behind the keys because nothing is drawn there. The
   bar needs no stickiness in this mode either -- it is outside the
   scrolling box.

   --vkb-h is measured by app.js from the keyboard itself; --kbar-h here is
   the bar, which is one line.
   ------------------------------------------------------------------- */
body.kiosk.has-vkb .content {
   height: calc(100dvh - var(--kbar-h, 3.25rem) - var(--vkb-h, 22rem));
   overflow-y: auto;
   overscroll-behavior: contain;
   padding-bottom: 1rem;
   -webkit-overflow-scrolling: touch;
}

/*
   The one card on a page that ends something irreversibly.

   A tint rather than a border, for the reason .card's own comment gives:
   a real border grows the box and .dlistwrap bleeds to the card edge on a
   negative margin read from --card-pad, which knows nothing about it. The
   inset ring is layered over the glass one at the same width, so the edge
   turns red without moving.
*/
.card.is-danger {
   background-image: linear-gradient(var(--dng-sub), var(--dng-sub)), var(--card-sheen);
   box-shadow: var(--sh-glass), inset 0 1px 0 0 var(--glass-edge),
      inset 0 0 0 1px color-mix(in srgb, var(--dng) 35%, transparent);
}
.card.is-danger > h2 { color: var(--dng); }

/* The inventory of what is still attached. Not a .dlist: it is one number
   and one noun a row, which is a definition list and not a table -- and it
   has to stay readable at the width the card is on a phone. */
.holds {
   margin: 0 0 1rem;
   display: grid;
   grid-template-columns: auto 1fr;
   gap: .3rem .6rem;
   font-size: var(--fs-sm);
}
.holds dt { font-variant-numeric: tabular-nums; text-align: right; font-weight: 600; }
.holds dd { margin: 0; }

/* A row for something that has been hidden. Dimmed, not struck through: it
   is still a real network with real washes behind it, and it is in this list
   only because somebody asked to see the hidden ones. The name keeps full
   contrast, because it is the link they came to click. */
.dlist-row.is-off { opacity: .62; }
.dlist-row.is-off a { opacity: 1; }

/* --- the roster grid, and the verdict box on the absence list ---------
   Two screens of 'Jelenléti ív és beosztás'. Both put controls inside a
   .dlist cell, which the stylesheet otherwise only ever fills with text, so
   they need the controls narrowed: a time input at the default width: 100%
   makes a seven-day grid twice as wide as the page.

   input[type=time] is deliberately given the base field styling here rather
   than added to the long selector above it. That list is the fields a form
   screen uses; this one appears in a grid cell and nowhere else, and adding
   it up there would have set width: 100% on it as well.
   -------------------------------------------------------------------- */

input[type=time] {
   padding: .35rem .45rem;
   background: var(--paper);
   border: 1px solid var(--line);
   border-radius: var(--r);
   color: var(--ink);
   font-family: inherit;
   font-size: var(--fs-sm);
   line-height: 1.4;
}

/* A cell of the roster: the two times side by side, with whatever has to be
   said about the day underneath them. flex-wrap, because on a narrow screen
   the .dlist stacks and the cell becomes a full-width row. */
.dlist.rota .cell {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   gap: .25rem;
}
/* The two time boxes. Typed, not picked -- see the view -- so they are as
   wide as "08:00" and no wider, which is what lets seven days fit across a
   screen somebody is holding. Centred, because a column of right-aligned
   times with a ragged left edge reads as two columns. */
.dlist.rota .cell input[data-rota-from],
.dlist.rota .cell input[data-rota-to] {
   flex: 0 1 4.4rem;
   min-width: 3.9rem;
   padding-inline: .35rem;
   text-align: center;
   font-variant-numeric: tabular-nums;
}
.dlist.rota .cell .sub { flex: 1 0 100%; }

/* THE ROSTER IS A TABLE, and stays one.
   -------------------------------------------------------------------------
   Every other list in this system stacks into cards on a narrow screen,
   which is right for a listing whose rows are read one at a time. A rota is
   not read that way: the whole point of it is reading ACROSS -- who is on
   Thursday -- and down -- what am I working this week. Stacked, both of
   those questions need scrolling and counting.

   So it keeps its columns at every width and the wrapper scrolls sideways
   instead, which is what the predecessor does and what the owner asked for
   in two words. The day columns are equal, so every day is the same width
   whatever is written in it; the two time boxes wrap inside a column rather
   than widening it. Specificity beats the stacking rules deliberately --
   see the --stack flag above. */
.dlist.rota[data-cols="8"] {
   /* 5.4rem is what "CSÜTÖRTÖK" needs at the head size: the widest thing
      in a day column is the day's NAME, not the two time boxes under it,
      now that they are stacked. */
   --dl-tpl: minmax(7.5rem, 1.1fr) repeat(7, minmax(5.4rem, 1fr));
   --dl-x: .35rem;
   --stack: 0;
}

/* ...AND IT STAYS INSIDE ITS OWN BOX.
   -------------------------------------------------------------------------
   The grid is wider than a phone, which is the point -- it scrolls sideways
   under the pinned name column. But the wrapper's scroll was not CONTAINING
   it: the document itself came out 647px wide inside a 500px viewport, so
   the whole PAGE panned sideways and could be pinched out to half size.
   That is what the owner saw, and the zoom was the symptom rather than the
   fault.

   contain: paint is the fix, and it is put on this wrapper alone rather than
   on every .dlistwrap: containment makes the box a containing block for
   anything positioned inside it, and a list elsewhere with a dropdown in a
   row would have its menu clipped to the row. Nothing inside the roster
   escapes its own box -- the window lives outside the grid entirely. */
.rotawrap { contain: paint; }

/* The day and its date on TWO LINES. A head cell does not wrap by default,
   which is right for a one-word column heading and wrong for "Csütörtök
   10-15": on one line it is wider than the column it heads, and what the
   reader got was "HÉTFŐ 10-1". */
.dlist.rota .dlist-head > * {
   white-space: normal;
   /* A shade smaller and without the tracking the other lists use: this
      head has to fit "CSÜTÖRTÖK" in a column sized for "14:00", and every
      pixel of letter-spacing is a pixel of day column. */
   font-size: .6875rem;
   letter-spacing: 0;
   /* A backstop for a translation with a longer word in it than Hungarian
      has: break it rather than clip it. */
   overflow-wrap: anywhere;
   line-height: 1.3;
   padding-block: .5rem;
}
.dlist.rota .dlist-head .sub { display: block; font-size: var(--fs-xs); }

/* The lines that make it read as a table. The .dlist's own rules draw the
   horizontal ones; these are the verticals, which a list does not want and
   a grid of times does. */
.dlist.rota .dlist-head > * + *,
.dlist.rota .dlist-row > * + * { border-left: 1px solid var(--line-soft); }
.dlist.rota .dlist-head > * { text-align: center; }
.dlist.rota .dlist-head > *:first-child { text-align: left; }
.dlist.rota .cell { justify-content: center; text-align: center; }

/* The name stays while the days scroll past it. A rota wider than the
   screen is useless if the column that says whose row it is scrolls away
   with it -- so it sticks, and carries its own background because the rows
   behind it are transparent. */
.dlist.rota .dlist-head > *:first-child,
.dlist.rota .dlist-row > *:first-child {
   position: sticky;
   left: 0;
   z-index: 1;
   background: var(--paper);
   border-right: 1px solid var(--line);
}
.dlist.rota .dlist-head > *:first-child { background: var(--paper-2); }
.dlist.rota .rota-notes > *:first-child { background: var(--paper-2); }

/* The handle. Four-way arrow, the predecessor's glyph in the predecessor's
   place. touch-action: none, or the browser scrolls the page instead of
   letting the row be dragged -- it decides that before any script runs. */
.rota-grip {
   display: inline-grid;
   place-items: center;
   width: 1.6rem;
   height: 1.6rem;
   flex: 0 0 auto;
   padding: 0;
   border: 0;
   border-radius: var(--r-sm, .4rem);
   background: none;
   color: var(--muted);
   cursor: grab;
   touch-action: none;
}
.rota-grip:hover { background: var(--paper-2); color: var(--ink); }
.rota-grip:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }
.rota-grip .ico-print { width: 1rem; height: 1rem; }

.dlist.rota .dlist-row.is-dragging { opacity: .6; }
.dlist.rota .dlist-row.is-dragging > * { background: var(--pri-sub); }
.dlist.rota.is-sorting { cursor: grabbing; user-select: none; }

/* The name column of the roster: the name, what the line is, and the two
   buttons that act on the row. A flex row so the buttons sit beside a short
   name and wrap under a long one rather than squeezing it. */
.dlist.rota .rota-who {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   gap: .3rem;
}
.dlist.rota .rota-acts { display: flex; flex-wrap: wrap; gap: .3rem; }

/* The day's note, in a row of its own at the top of the grid. Set apart
   from the people below it: it is about the day and not about anybody. */
.dlist.rota .rota-notes { background: var(--paper-2); }
.dlist.rota .rota-notes input[type=text] { width: 100%; min-width: 0; }
.dlist.rota .rota-notes .lbl { color: var(--muted); font-size: var(--fs-xs); }

/* A day the window cannot offer, because an approved absence covers it and
   the row behind has no boxes to write. Dimmed, never removed: a day
   missing from the list would be a list somebody counts wrong. */
.shiftmodal [data-shift-dayrow].is-off { opacity: .55; }
.shiftmodal .modal-body > .field { margin-bottom: .75rem; }

/* A day an approved absence covers. Dimmed rather than struck through: the
   person is real and the day is real, and the only thing that is not there
   is a shift. There are no inputs in this cell at all -- a box that cannot
   be saved is worse than no box. */
.dlist.rota .cell.is-away { opacity: .72; }

/* The verdict box on the absence list: a select, a reason and a button, in
   one row of a cell. Overrides form.inline's display:inline, which would
   leave the three controls on three lines because each one is display:block. */
.dlist form.decide {
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   gap: .3rem;
   margin-bottom: .3rem;
}
.dlist form.decide select { flex: 0 1 9rem; padding: .35rem .45rem; font-size: var(--fs-sm); }
/* The same construction again, for the role picker on a person's page: a
   select and a button in one line of a cell that also carries the revoke
   button beside them. form.inline is display:inline, which puts the two on
   two lines because each control is display:block -- and then the button
   lands under the select next to an unrelated one, which is how it read
   before this rule existed.

   The select is CAPPED rather than sized to its longest option. Role names
   run to "Kioszk (erintokepernyo)", and a column wide enough for that one
   is a column that wide on every row, including the rows with no picker in
   them at all. */
.dlist form.rolepick {
   display: inline-flex;
   /* NOWRAP, and the select shrinks instead. Wrapping put the button on a
      line of its own underneath, directly beside the revoke button of the
      same row -- two buttons side by side that do opposite things, one of
      them not even on the line its own control is on. */
   flex-wrap: nowrap;
   align-items: center;
   gap: .3rem;
   margin-inline-end: .35rem;
}
/* The right padding is NOT .45rem: `select` reserves 2.2rem there for the
   chevron it draws as a background image, and a padding shorthand wipes
   that -- the longest role name then ran under its own arrow. */
.dlist form.rolepick select {
   flex: 0 1 11rem;
   padding: .35rem 1.9rem .35rem .45rem;
   font-size: var(--fs-sm);
}
.dlist form.decide input[type=text] { flex: 1 1 8rem; padding: .35rem .45rem; font-size: var(--fs-sm); }

/*
   The jelenléti ív.
   -------------------------------------------------------------------------
   A month of days across and people down is a MATRIX, and it is the one
   shape .dlist cannot carry: thirty-one cells in a row is not a list of
   anything. So a grid of its own, with the name column sticky and the days
   scrolling under it -- the sheet is read by running a finger along one
   person's row, and a name that scrolls off makes that impossible.

   --iv-days is set per month from the view, because February is not October.
*/
.ivwrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.ivgrid {
   display: grid;
   grid-template-columns:
      minmax(8rem, max-content)
      repeat(var(--iv-days, 31), minmax(1.6rem, 1fr))
      minmax(5rem, max-content);
   min-width: max-content;
   font-size: var(--fs-sm);
   font-variant-numeric: tabular-nums;
}

.iv-name, .iv-cell, .iv-sum {
   padding: .4rem .3rem;
   border-bottom: 1px solid var(--line-soft);
   text-align: center;
   white-space: nowrap;
}

.iv-name {
   position: sticky;
   left: 0;
   z-index: 1;
   /* Opaque, or the days scroll visibly underneath the names. */
   background: var(--paper);
   padding-inline: .9rem;
   text-align: left;
}
.iv-sum { padding-inline: .9rem; text-align: right; }

.iv-head {
   font-size: var(--fs-xs);
   font-weight: 600;
   text-transform: uppercase;
   letter-spacing: .03em;
   color: var(--muted);
}

/* Saturday and Sunday, so the eye finds the weeks without counting. */
.iv-cell.is-weekend { background: var(--paper-2); }

/* A day off. Tinted rather than lettered-in-colour: the letter has to stay
   legible for somebody printing this in black and white. */
.iv-cell.is-away { background: var(--inf-sub); font-weight: 600; }

/* Not decided yet. Drawn, because whoever reads the sheet needs to know it
   is coming -- and faded, because it is not yet a day off. */
.iv-cell.is-pending { opacity: .55; font-weight: 400; }

.iv-nil { color: var(--muted); opacity: .5; }

/* A rostered day off. Marked, but not tinted like leave: it is a day nobody
   put this person on the sheet, not a day they took. Muted, so the eye
   passes over it the way it passes over a weekend. */
.iv-cell.is-rest { color: var(--muted); font-weight: 600; }

/* ---------------------------------------------------------------------------
   The counter: product tiles, the basket, and the totals under both.
   --------------------------------------------------------------------------- */

/* The offer, as targets rather than a list.

   auto-fill with a 7rem floor: on a 21in touchscreen that lays out six or
   seven across, and on a phone it falls to two without a media query. The
   minmax() guard is the usual one -- `min(7rem, 100%)` so a narrow viewport
   cannot force a track wider than the screen and start it scrolling. */
.tilegrid {
   display: grid;
   grid-template-columns: repeat(auto-fill, minmax(min(7rem, 100%), 1fr));
   gap: .75rem;
}

.tile-form { display: contents; }

/* A whole tile is the button. Nothing inside it is separately clickable, so
   a thumb landing anywhere on it adds the thing -- which is the entire
   interaction this screen exists for. */
.tile {
   display: flex; flex-direction: column; align-items: center; gap: .4rem;
   width: 100%; min-height: 8.5rem;
   padding: .75rem .5rem;
   border: 1px solid var(--line);
   border-radius: var(--r-lg);
   background: var(--btn-soft, transparent);
   color: inherit;
   font: inherit;
   text-align: center;
   cursor: pointer;
}
.tile:hover { filter: brightness(.97); }
.tile:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

.tile-img {
   width: 3rem; height: 3rem;
   object-fit: contain;
   flex: none;
}
/* A product with no picture keeps the same shape, so a grid of some-with
   and some-without does not look broken. */
.tile-img.is-blank {
   display: block;
   border: 1px dashed var(--line);
   border-radius: .375rem;
   opacity: .5;
}

.tile-name  { font-size: var(--fs-sm); line-height: 1.25; }
.tile-price { font-weight: 600; font-variant-numeric: tabular-nums; }

/* The quantity stepper. Minus, the number, plus -- three separate forms,
   because without script each one has to post for itself. */
.qtycell {
   display: inline-flex; align-items: center; justify-content: flex-end;
   gap: .4rem;
}
.qtyform { display: inline; }
.qtynum  { min-width: 1.5rem; text-align: center; font-variant-numeric: tabular-nums; }

.btn.xs { padding: .15rem .5rem; font-size: var(--fs-xs); line-height: 1.4; }

/* The total, and the two figures behind it.

   Gross leads because gross is what the customer hands over -- the one
   place in this system where that is the headline. The net and the VAT
   follow in the muted weight, each labelled in words, so the figure that
   reaches the receipt is never a surprise. */
.till-total {
   width: 100%; margin: 0;
   display: flex; flex-wrap: wrap; align-items: baseline;
   justify-content: space-between; gap: .25rem 1rem;
}
.till-total strong { font-size: 1.125rem; font-variant-numeric: tabular-nums; }
.till-net { color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }

/* Billing details, folded away. <details> because the CSP forbids the
   script that would otherwise toggle it, and because a counter sale
   usually does not need them at all. */
.buyerbox { margin: 0 0 1rem; }
.buyerbox > summary {
   cursor: pointer; padding: .35rem 0;
   font-size: var(--fs-sm); font-weight: 600;
}
.buyerbox[open] > summary { margin-bottom: .5rem; }

/* Small pictures beside a name in a list, and beside the field that sets
   one. Fixed box either way, so a tall image cannot stretch its row. */
.rowthumb {
   width: 2rem; height: 2rem; object-fit: contain;
   vertical-align: middle; margin-right: .4rem;
}
.formthumb {
   width: 6rem; height: 6rem; object-fit: contain;
   border: 1px solid var(--line); border-radius: .375rem;
}

/* Why a document was refused, under the row it was refused on. */
.dlist-note {
   padding: .25rem 1rem .75rem;
   color: var(--muted); font-size: var(--fs-xs);
}

/*
   The booking week.
   -------------------------------------------------------------------------
   /booking/week: days across, time down, and every booking a block the
   size of its own length. The predecessor's shape, because that is the
   screen the owner reads without thinking about it.

   THIS IS NOT A LIST AND IT IS NOT A .dlist. A list answers "what is
   booked"; the day view already does that and does it better. This screen
   answers "WHERE ARE THE GAPS", and a gap is empty space of a particular
   height at a particular time -- there is no row that can carry it. So a
   positioned grid, on purpose. The view says the same thing at the top of
   itself so that neither file reads as an oversight.

   THE GEOMETRY ARRIVES FROM PHP, in `style` attributes, and nothing here
   is built by a script:

     --bkw-rows   on .bkweek, how many ten-minute rows tall the week is
     --bkw-t      minutes from the top of the grid        (hour marks, blocks)
     --bkw-l      how many minutes long a block is
     --bkw-x      a block's left edge, as a percentage of its day
     --bkw-w      its width, likewise

   The horizontal pair is a percentage because the number of columns
   belongs to the DAY -- a day whose data overlaps inside one lane grows a
   third column while its neighbour keeps halves. The vertical scale is a
   token here, so ten minutes is one number to change.
*/

.bkwrap {
   overflow-x: auto;
   -webkit-overflow-scrolling: touch;
}

.bkweek {
   /* One row of heads over one row of columns, with the clock in a gutter
      on the left of both. */
   display: grid;
   grid-template-columns: 3.4rem repeat(7, minmax(0, 1fr));
   /* Below this the day columns are too narrow to read a plate in, so the
      wrapper scrolls instead of squeezing. The phone does not meet this:
      it shows one day, see the media query at the end of this block. */
   min-width: 46rem;
   /* SMALLER THAN --fs-xs, which is the only place on this site that is.
      It is arithmetic, not taste: seven days in a 970px content column is
      a 130px day, two lanes makes that a 63px block, and 63px holds about
      52px of text. A Hungarian plate at 12px is 52px wide BEFORE the
      padding, so it wrapped to two lines and the registration -- the one
      thing somebody is scanning for -- read as "ABC-" over "123". At 11px
      it fits on one line with room to spare. The heads and the clock keep
      the normal size below; it is only inside the blocks that the space
      is this tight, and the phone layout puts it back up to --fs-sm
      because there is one day on screen there instead of seven. */
   font-size: .6875rem;
   /* Room for the closing label. The last hour mark sits ON the bottom
      edge of the columns and is lifted half a line to centre it there, so
      without this the scroller clips it and the week ends at a line with
      no time on it. */
   padding-bottom: .7rem;

   /* Ten minutes. The one number that sets how tall the week is. */
   --bkw-row: 1.05rem;
   --bkw-min: calc(var(--bkw-row) / 10);
}

/* --- the heads --------------------------------------------------------- */

.bkw-corner {
   border-bottom: 1px solid var(--line);
}

.bkw-dayhead {
   font-size: var(--fs-xs);
   display: flex;
   flex-direction: column;
   align-items: center;
   gap: .1rem;
   padding: .4rem .25rem;
   border-left: 1px solid var(--line-soft);
   border-bottom: 1px solid var(--line);
   color: inherit;
   text-decoration: none;
   text-align: center;
}
.bkw-dayname         { font-weight: 600; }
.bkw-date            { color: var(--muted); font-variant-numeric: tabular-nums; }

/* A free stretch of a column can be clicked to take a booking there, so it
   looks it: the cell cursor, and a faint wash under the pointer. Only where the
   reader may add one (data-bk-add), and only on the empty part -- the blocks sit
   on top of the column and keep their own pointer. */
.bkweek[data-bk-add] .bkw-col { cursor: cell; }
.bkweek[data-bk-add] .bkw-col:hover {
   background-image: linear-gradient(color-mix(in srgb, var(--pri) 5%, transparent),
                                     color-mix(in srgb, var(--pri) 5%, transparent));
}
.bkw-ev { cursor: pointer; }

/* The dialog's form. The peek dialog's body already pads; what this adds is the
   gap above the actions on a booking that exists. */
.bkmodal .bkmodal-status { margin-top: 1rem; padding-top: .75rem; border-top: 1px solid var(--line-soft); }
.bkmodal .row2 { gap: .75rem; }

/* Today, so the eye finds it without reading seven dates. */
.bkw-dayhead.is-today { box-shadow: inset 0 -2px 0 var(--pri); }
.bkw-dayhead.is-today .bkw-dayname { color: var(--pri); }

/* --- the clock, and the days under it ---------------------------------- */

.bkw-gutter,
.bkw-col {
   position: relative;
   height: calc(var(--bkw-rows) * var(--bkw-row));
}

.bkw-gutter {
   border-right: 1px solid var(--line);
}

/* An hour label sits ON its line rather than under it, so the eye lands on
   the same pixel row as the block that starts at that time. */
.bkw-hour {
   position: absolute;
   right: .35rem;
   top: calc(var(--bkw-t) * var(--bkw-min));
   transform: translateY(-.55em);
   font-size: var(--fs-xs);
   color: var(--muted);
   font-variant-numeric: tabular-nums;
}

/* The hour rules, painted rather than marked up: 144 empty divs a day is a
   DOM nobody needs, and a gradient keeps the lines in step with the row
   token above automatically. The lighter rule is the half hour. */
.bkw-col {
   border-left: 1px solid var(--line-soft);
   background-image:
      repeating-linear-gradient(to bottom,
         var(--line) 0, var(--line) 1px,
         transparent 1px, transparent calc(var(--bkw-row) * 6)),
      repeating-linear-gradient(to bottom,
         var(--line-soft) 0, var(--line-soft) 1px,
         transparent 1px, transparent calc(var(--bkw-row) * 3));
}
.bkw-col.is-weekend  { background-color: var(--paper-2); }
.bkw-dayhead.is-weekend .bkw-dayname { color: var(--muted); }

/* --- a booking --------------------------------------------------------- */

.bkw-ev {
   position: absolute;
   top:    calc(var(--bkw-t) * var(--bkw-min));
   height: calc(var(--bkw-l) * var(--bkw-min));
   left:   var(--bkw-x);
   width:  var(--bkw-w);

   display: flex;
   flex-direction: column;
   /* The time and the plate first, because those are what somebody is
      looking for; everything after them is clipped away on a short block
      rather than shrinking the two that matter. */
   overflow: hidden;
   gap: 0;
   padding: .15rem .25rem;
   border: 1px solid var(--pri);
   border-left: 3px solid var(--pri);
   border-radius: .25rem;
   background: var(--pri-sub);
   color: var(--body);
   text-decoration: none;
   line-height: 1.25;
}
.bkw-ev:hover { filter: brightness(.97); }
.bkw-ev:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

.bkw-when  { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* THE CLOSING TIME GOES WHEN THERE IS NO ROOM FOR IT.

   Seven days across a 970px content column is a 130px day, and a day with
   two lanes in it is a 63px block -- measured, not guessed. "07:20-07:50"
   needs 68px, so on a laptop every block on this screen was clipping its
   own time in the middle of the second number.

   A container query rather than a viewport one, because what decides this
   is the width of THE BLOCK -- a Tuesday with one lane in use has twice
   the room a Wednesday with three has, on the same screen. The full range
   stays in the title attribute, which costs nothing and needs no script.
   Same reasoning as the .dlist breakpoint further up this file. */
.bkw-ev    { container-type: inline-size; }

@container (max-width: 5.5rem) {
   .bkw-t2 { display: none; }
}
/* NOWRAP, after the measurement in .bkweek above: a plate broken over two
   lines is the one thing on this block that must not happen. */
.bkw-plate { font-weight: 600; white-space: nowrap; }
.bkw-lane,
.bkw-tel,
.bkw-what  { color: var(--muted); }
/* A name and a number may break anywhere rather than reach past the edge:
   a block is as wide as its lane's share of the day and no wider. */
.bkw-who,
.bkw-tel   { overflow-wrap: anywhere; }

/* Waiting for its code: drawn in the amber the rest of the site uses for
   "not settled yet", and dashed, so it is not mistaken for a firm booking
   at a glance from across the bay. */
.bkw-ev.is-wait {
   border-color: var(--wrn);
   border-style: dashed;
   border-left-style: solid;
   background: var(--wrn-sub);
}

/* A service rather than a wash. */
.bkw-ev.is-service {
   border-color: var(--inf);
   background: var(--inf-sub);
}

/* Cancelled, or nobody came. Still drawn -- the week view shows the same
   rows the day view does, and a slot somebody cancelled is a slot that is
   now FREE, which is information on a screen about gaps. Faded and struck
   through so it is never read as a booking. */
.bkw-ev.is-dim {
   border-color: var(--line);
   background: var(--paper-2);
   opacity: .6;
}
.bkw-ev.is-dim .bkw-plate { text-decoration: line-through; }

/* --- the day chips, and the phone ---------------------------------------
   A PHONE SHOWS ONE DAY, NOT SEVEN.

   390px over seven columns is 45px a column, which is not a week view --
   it is seven columns too narrow to hold "ABC-123". Stacking the days one
   under another was the other candidate and it was rejected: a stack has
   no shared time axis, so the one thing this screen exists to show -- a
   gap, seen against the same clock every day is measured on -- is exactly
   what a stack destroys.

   So the grid keeps its shape and shows ONE day. The chips above it are
   plain links carrying ?d=1..7 and the SERVER marks the chosen one, so
   changing day works with no script at all, which is the same rule the
   geometry follows.
   ----------------------------------------------------------------------- */

.bkw-chips { display: none; }

@media (max-width: 47.99rem) {
   .bkw-chips {
      display: flex;
      gap: .25rem;
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      padding: .6rem .75rem;
      border-bottom: 1px solid var(--line);
   }

   .bkw-chip {
      flex: 1 0 auto;
      min-width: 2.75rem;
      /* A thumb at a bay, not a mouse: this is the whole reason the chips
         are their own control rather than the day heads doing double duty. */
      min-height: 2.5rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: .25rem .4rem;
      border: 1px solid var(--line);
      border-radius: var(--r-lg, .5rem);
      color: inherit;
      text-decoration: none;
      font-size: var(--fs-xs);
   }
   .bkw-chipday { font-weight: 600; font-variant-numeric: tabular-nums; }
   .bkw-chip.is-today  { border-color: var(--pri); }
   .bkw-chip.is-on     { background: var(--pri); border-color: var(--pri); color: #fff; }

   /* The other six days leave the GRID rather than being hidden inside it:
      display:none takes an item out of grid flow, so the two that remain
      land in the two tracks declared below and nothing has to be placed by
      hand. */
   .bkweek {
      min-width: 0;
      grid-template-columns: 3.4rem minmax(0, 1fr);
      font-size: var(--fs-sm);
      /* Rows get taller, because there is room for them now and a block is
         read at arm's length. */
      --bkw-row: 1.35rem;
   }
   .bkw-dayhead:not(.is-picked),
   .bkw-col:not(.is-picked) { display: none; }
}

/* --- price grid: /price/network/{id} ---------------------------------
   A matrix whose columns are data, so a real <table> with the header row
   and header column held in place while the rest scrolls both ways. */
.pgrid-wrap { margin-bottom: 1rem; }
.pgrid-bar { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem; margin-bottom: .6rem; }
.pgrid-csrf { display: none; }
.pgrid-scroll { overflow: auto; max-height: 75vh; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); }
.pgrid { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; font-size: .9rem; }
.pgrid th, .pgrid td { padding: .45rem .7rem; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); white-space: nowrap; text-align: center; }
.pgrid thead th { position: sticky; top: 0; z-index: 2; background: var(--paper-2); white-space: normal; min-width: 11rem; max-width: 14rem; font-weight: 600; }
.pgrid tbody th { position: sticky; left: 0; z-index: 1; background: var(--paper-2); text-align: left; font-weight: 600; min-width: 11rem; }
.pgrid thead th.corner { left: 0; z-index: 3; min-width: 11rem; }
.pgrid .g { font-style: italic; opacity: .8; margin-left: .5rem; }
.pgrid td.off { opacity: .55; }
.pgrid.is-edit tbody td { cursor: text; }
.pgrid.is-edit tbody td:hover { background: var(--pri-sub); }
.pgrid td.busy { opacity: .5; }
.pgrid td.saved { background: var(--suc-sub); transition: background .4s; }
.pgrid td.err { background: var(--dng-sub); }
.pgrid-input { width: 7rem; margin: 0; padding: .2rem .4rem; text-align: right; }
.pgrid-wrap.is-full { position: fixed; inset: 0; z-index: 900; margin: 0; padding: .75rem; background: var(--bg); overflow: hidden; display: flex; flex-direction: column; }
.pgrid-wrap.is-full .pgrid-scroll { max-height: none; flex: 1; }
html.pgrid-lock { overflow: hidden; }
.pgrid-unit { margin-left: .4rem; font-size: .8rem; opacity: .7; }
.pgrid[data-mode="gross"] .n { font-style: italic; opacity: .8; }
.pgrid[data-mode="gross"] .g { font-style: normal; opacity: 1; font-weight: 600; }
.pgrid[data-mode="net"] .n { font-weight: 600; }
.pgrid-bar .seg { display: inline-flex; align-items: center; gap: .35rem; }

/* --- the absence calendar: /hr/absences/calendar ----------------------
   A month of one washpoint, days across and absences drawn as bars over
   the days they cover. The owner asked for the predecessor's screen and
   this is its shape.

   ONE GRID DEFINITION, used by four rows -- the day heads, the shading
   behind the lanes, the lanes themselves and the count strip. They are
   separate elements rather than one tall grid because the lanes are a
   variable number of rows and a bar has to be able to say which COLUMN it
   starts in without also having to know which row it landed in. They stay
   aligned because every one of them inherits the same template from
   .abcal, and nothing but .abcal sets it. */
.abcalwrap {
   overflow-x: auto;
   -webkit-overflow-scrolling: touch;
}

.abcal {
   --abcal-col: 2.2rem;      /* one day, at its narrowest              */
   --abcal-lane: 1.9rem;     /* one lane of bars                        */

   display: grid;
   grid-template-columns: repeat(var(--abcal-days), minmax(var(--abcal-col), 1fr));
   /* So a month does not squeeze to illegibility on a phone: below this
      the wrapper scrolls sideways instead. Thirty-one days at 2.2rem is
      about 68rem, and a fortnight's window is narrower by construction. */
   min-width: calc(var(--abcal-days) * var(--abcal-col));
   font-size: var(--fs-xs);
}

/* Each of the four rows spans the whole grid and lays its own cells out on
   the same columns. The track list is REPEATED rather than inherited: an
   inherited one is the parent's USED value, a list of fixed pixels, and a
   row that then adds a column gap to it is wider than the grid it is
   supposed to line up with -- which showed up as eighteen stray pixels of
   sideways scroll on a month that otherwise fitted. */
.abcal-days,
.abcal-body,
.abcal-back,
.abcal-lane,
.abcal-foot {
   grid-column: 1 / -1;
   display: grid;
   grid-template-columns: repeat(var(--abcal-days), minmax(var(--abcal-col), 1fr));
}

.abcal-days {
   border-bottom: 1px solid var(--line);
}

.abcal-dayhead {
   display: flex;
   flex-direction: column;
   align-items: center;
   gap: .05rem;
   padding: .3rem .1rem;
   border-left: 1px solid var(--line-soft);
   text-align: center;
}
.abcal-dayhead:first-child { border-left: 0; }
.abcal-dow  { color: var(--muted); }
/* The first of a month says which month, in the weekday's slot. It is the
   only label on the strip that is not a day, so it is the only one in ink. */
.abcal-dow.is-mon { color: var(--ink); font-weight: 600; }
.abcal-dnum { font-weight: 600; font-variant-numeric: tabular-nums; }

.abcal-dayhead.is-off .abcal-dnum { color: var(--muted); }
/* Today, so the eye finds it without reading thirty-one dates. */
.abcal-dayhead.is-today { box-shadow: inset 0 -2px 0 var(--pri); }
.abcal-dayhead.is-today .abcal-dnum { color: var(--pri); }

/* --- the lanes, and the shading behind them --------------------------- */

.abcal-body {
   position: relative;
   padding: .3rem 0;
   /* ROW gap only. A column gap here would push the lanes wider than the
      day heads above them, which is the one thing this grid may not do. */
   row-gap: .2rem;
}

/* Painted once, behind every lane. A cell per lane would be a hundred and
   fifty empty spans on a five-lane month to say the same thing. */
.abcal-back {
   position: absolute;
   inset: 0;
   pointer-events: none;
}
.abcal-col { border-left: 1px solid var(--line-soft); }
.abcal-col:first-child { border-left: 0; }
/* The weekend. --paper-2 is nearly the card it sits on and vanished at a
   glance, which is the only glance this band gets; mixed from --line it
   reads as a band in both themes without becoming a second colour. */
.abcal-col.is-off   { background: color-mix(in srgb, var(--line) 55%, transparent); }
.abcal-col.is-today { background: var(--pri-sub); }

.abcal-lane {
   position: relative;
   z-index: 1;
   min-height: var(--abcal-lane);
   align-items: stretch;
}

/* --- a bar ------------------------------------------------------------
   Its geometry is two custom properties written into the style attribute
   by the view: which column it starts in and how many it covers. */
.abcal-bar {
   grid-column: var(--c) / span var(--n);
   display: flex;
   align-items: baseline;
   gap: .3rem;
   min-width: 0;
   overflow: hidden;
   white-space: nowrap;
   padding: .25rem .4rem;
   border-radius: var(--r-sm, 6px);
   border-left: 3px solid var(--abcal-tone, var(--sec));
   background: var(--abcal-sub, var(--paper-2));
   color: var(--ink);
   text-decoration: none;
   line-height: 1.25;
}
.abcal-bar:hover { filter: brightness(.97); }
.abcal-bar:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }

/* A bar is an <a> where the reader may open the absence and a <button>
   where they may not -- the view's own comment says why. The button has to
   be talked out of looking like one. */
button.abcal-bar {
   appearance: none;
   font: inherit;
   text-align: left;
   cursor: pointer;
}

.abcal-who  { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.abcal-what { color: var(--muted); overflow: hidden; text-overflow: ellipsis; }

/* The six jogcím. The tone drives the border and the fill together, so a
   kind is one pair of values and not six rules. */
/* Three selectors each, not two: the key's swatch, the bar, and the card a
   tap on the bar opens -- whose left edge is the colour of what was
   pressed, which is most of how the reader knows the right card opened. */
.abcal-bar.is-paid,   .abcal-swatch.is-paid,   .abcal-pop.is-paid   { --abcal-tone: var(--pri); --abcal-sub: var(--pri-sub); }
.abcal-bar.is-extra,  .abcal-swatch.is-extra,  .abcal-pop.is-extra  { --abcal-tone: var(--inf); --abcal-sub: var(--inf-sub); }
.abcal-bar.is-sick,   .abcal-swatch.is-sick,   .abcal-pop.is-sick   { --abcal-tone: var(--wrn); --abcal-sub: var(--wrn-sub); }
.abcal-bar.is-bad,    .abcal-swatch.is-bad,    .abcal-pop.is-bad    { --abcal-tone: var(--dng); --abcal-sub: var(--dng-sub); }
.abcal-bar.is-unpaid, .abcal-swatch.is-unpaid, .abcal-pop.is-unpaid { --abcal-tone: var(--sec); --abcal-sub: var(--paper-2); }
.abcal-bar.is-rest,   .abcal-swatch.is-rest,   .abcal-pop.is-rest   { --abcal-tone: var(--line); --abcal-sub: var(--paper-2); }

/* Waiting for a decision: hatched, which is how the predecessor draws it
   and the one marking that reads at a glance as "not settled yet". */
.abcal-bar.is-wait,
.abcal-swatch.is-wait {
   background-image: repeating-linear-gradient(135deg,
      rgba(255, 255, 255, .55) 0 4px, transparent 4px 9px);
}

/* Refused. On the grid because somebody has to see that they asked. */
.abcal-bar.is-no { opacity: .6; }
.abcal-bar.is-no .abcal-who { text-decoration: line-through; }

/* It started before this month, or it runs on past it: the edge is cut
   square and the other end keeps its corner, so the bar says which. */
.abcal-bar.is-from { border-start-start-radius: 0; border-end-start-radius: 0; border-left-style: dashed; }
.abcal-bar.is-to   { border-start-end-radius: 0; border-end-end-radius: 0;
                     border-right: 3px dashed var(--abcal-tone, var(--sec)); }

/* --- how many people are away that day -------------------------------- */

.abcal-foot {
   border-top: 1px solid var(--line);
}
.abcal-count {
   padding: .25rem .1rem;
   text-align: center;
   font-variant-numeric: tabular-nums;
   color: var(--muted);
   border-left: 1px solid var(--line-soft);
}
.abcal-count:first-child { border-left: 0; }
.abcal-count.is-some { font-weight: 600; color: var(--ink); }

/* --- the key ----------------------------------------------------------- */

.abcal-key {
   display: flex;
   flex-wrap: wrap;
   gap: .35rem .9rem;
   margin-top: .6rem;
   font-size: var(--fs-xs);
   color: var(--muted);
}
.abcal-keyitem { display: inline-flex; align-items: center; gap: .35rem; }
.abcal-swatch {
   width: 1.6rem;
   height: .8rem;
   border-radius: 3px;
   border-left: 3px solid var(--abcal-tone, var(--sec));
   background: var(--abcal-sub, var(--paper-2));
}

/* --- what a tap on a bar opens ----------------------------------------
   A bar can be two days wide and a name does not fit in two days. The
   owner: "it's hard to guess the names. So when you tap the name, that
   should first show the box a bit larger with the full name written, and
   there should be a button to edit this absence."

   So: ONE card for the page, placed by app.js at the bar that was pressed.
   Fixed rather than absolute, because the thing it has to sit beside is
   inside a box that scrolls sideways -- an absolutely positioned panel
   would need the scroll offset of its own container and would then travel
   with it, off the screen. Fixed costs one listener that shuts it when
   anything scrolls, which is also the honest behaviour: the bar it belongs
   to has moved.
   ------------------------------------------------------------------- */

.abcal-pop {
   position: fixed;
   /* Over the cards and the sticky heads, under .pgrid's full-screen
      takeover (900) and the modal layer (60) is not enough here: a bar can
      be under the sticky filter bar at 30. */
   z-index: 70;
   top: 0; left: 0;             /* app.js writes both before it shows */
   width: min(20rem, calc(100vw - 1.5rem));
   padding: .8rem .9rem;
   border-radius: var(--r);
   border-left: 4px solid var(--abcal-tone, var(--sec));
   background: var(--paper);
   box-shadow: var(--sh-lg), 0 0 0 1px var(--line);
   font-size: var(--fs-sm);
}
.abcal-pop[hidden] { display: none; }
.abcal-pop:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }

/* The full name, which is the whole point of the card. Allowed to wrap --
   "Szabó-Kovács Annamária" does not fit on one line of a phone-wide box
   and truncating it here would be the bug this card exists to fix. */
.abcal-pop-who {
   margin: 0 0 .5rem;
   font-size: var(--fs); font-weight: 600; line-height: 1.3;
   overflow-wrap: anywhere;
}
.abcal-pop-line {
   display: flex; gap: .5rem; align-items: baseline;
   margin: 0 0 .2rem;
}
.abcal-pop-k { flex: 0 0 5.5rem; color: var(--muted); }
.abcal-pop .actions { margin: .7rem 0 0; }

/* The phone. The grid already scrolls sideways; what it needs here is for
   a day to stay wide enough to hold a name when one bar covers a week. */
@media (max-width: 720px) {
   .abcal { --abcal-col: 2rem; }
   .abcal-pop-k { flex-basis: 4.5rem; }
}

/* --- a date box, drawn by us -----------------------------------------
   app.js replaces every <input type="date"> and <input type="month"> on the
   site with the wash list's picker. The panel is the same one; what this
   block adds is the FIELD shape, because those boxes stand in forms where
   everything else is an input and a bare button beside a select reads as a
   different kind of control. */
.daterange.is-field { display: block; width: 100%; }

.daterange.is-field > .daterange-open {
   display: flex;
   width: 100%;
   padding: .5rem .8rem;
   border: 1px solid var(--line);
   border-radius: var(--r);
   background: var(--paper);
   color: var(--ink);
   font-size: var(--fs);
   line-height: 1.5;
   text-align: left;
}
.daterange.is-field > .daterange-open:hover { border-color: var(--sec); background: var(--paper); }
.daterange.is-field > .daterange-open:focus-visible {
   outline: 2px solid var(--pri);
   outline-offset: 1px;
}
.daterange.is-field [data-dr-label] { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* Nothing chosen in a box the form insists on, said the way the browser
   would have said it if it could still see the control. */
.daterange.is-field.is-missing > .daterange-open { border-color: var(--dng); }

/* The panel hangs from the START of a field, because a field is as wide as
   its column and the end of it can be half a screen away from the hand that
   opened it. In the wash list's search box it stays where it was. */
.daterange.is-field .drmenu { inset-inline-start: 0; inset-inline-end: auto; }

/* The wash list's quick list is eight ranges and reserves eight rows. Here
   it is three days or two months, and the reserved rows left the panel a
   third empty under them. */
.daterange.is-field .drquick { grid-template-rows: none; }

/* A PERIOD box (cwm_range_field) is the same field shape. Its own class and
   not .is-field, because app.js's date-box block owns that one and looks for
   parts a period box does not have. */
.daterange.is-range { display: block; width: 100%; }
.daterange.is-range > .daterange-open {
   display: flex;
   width: 100%;
   padding: .5rem .8rem;
   border: 1px solid var(--line);
   border-radius: var(--r);
   background: var(--paper);
   color: var(--ink);
   font-size: var(--fs);
   line-height: 1.5;
   text-align: left;
}
.daterange.is-range > .daterange-open:hover { border-color: var(--sec); background: var(--paper); }
.daterange.is-range > .daterange-open:focus-visible { outline: 2px solid var(--pri); outline-offset: 1px; }
.daterange.is-range [data-rp-label] { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.daterange.is-range .drmenu { inset-inline-start: 0; inset-inline-end: auto; }
.daterange.is-range .drquick { grid-template-rows: none; }
@media (max-width: 540px) {
   .daterange.is-range .drmenu { grid-template-columns: 1fr; inset-inline-start: 0; inset-inline-end: 0; }
}

/* A MONTH grid: twelve buttons, three to a row, in place of the days. The
   arrows then step by a year -- there is nowhere else for them to go. */
.drcal-grid.is-months {
   grid-template-columns: repeat(3, 1fr);
   gap: .25rem;
}

.drcal-month {
   padding: .55rem .35rem;
   border: 0;
   border-radius: var(--r-sm);
   background: none;
   color: inherit;
   font: inherit;
   font-size: var(--fs-sm);
   cursor: pointer;
}
.drcal-month:hover { background: var(--paper-2); }
.drcal-month:focus-visible { outline: 2px solid var(--pri); outline-offset: -2px; }
.drcal-month.is-today { box-shadow: inset 0 0 0 1px var(--pri); }
.drcal-month.is-end   { background: var(--pri); color: var(--on-pri); }

/* A day outside min/max is still drawn -- the month has to keep its shape --
   and plainly cannot be chosen. */
.drcal-day[disabled] { opacity: .35; cursor: default; }
.drcal-day[disabled]:hover { background: none; }

/* The phone. The panel is two columns on a desktop; at this width that is
   wider than the screen, so the quick list goes above the calendar. */
@media (max-width: 540px) {
   .daterange.is-field .drmenu {
      grid-template-columns: 1fr;
      inset-inline-start: 0;
      inset-inline-end: 0;
   }
   .daterange.is-field .drquick {
      grid-template-rows: none;
      grid-template-columns: repeat(2, 1fr);
      min-width: 0;
   }
   .daterange.is-field .drcal { min-width: 0; }
}

/* The drawing under a salary advance. Stored as dark ink on nothing, so it
   gets a white ground of its own -- on a dark theme the ink would otherwise
   sit on nothing it can be read against. */
.sigimg {
   display: block; max-width: 100%; height: auto;
   background: #fff; border-radius: var(--r);
   box-shadow: inset 0 0 0 1px var(--line);
}

/* Booking washpoints: the lane count and the place sit in one row beside
   the buttons. The labels stay for screen readers and the titles for the
   mouse; the column heads already say what each value is. */
.bksite-form { flex-wrap: nowrap; }
.bksite-form .lbl {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}
.bksite-form > input[type="number"] { flex: 0 0 4.5rem; width: 4.5rem; }
.bksite-form > select { flex: 0 1 auto; width: auto; min-width: 9rem; }
.dlist-row > .right.bksite-actions { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; flex-wrap: wrap; }

/* --- the partner's claim form, laid out as the predecessor's ----------
   One question under another, each prompt centred above its answer: the
   structure of wi.vedox.hu's form, in this site's own materials. The two
   answers a dispatcher is really after -- which day, and is there room --
   are drawn: a month calendar whose days carry how many places are left, and
   below it the windows of the chosen day, each saying how many remain.

   The calendar is hidden until app.js adds .is-ready. Without script every
   day's windows are listed under their own date, which is the whole form.
   -------------------------------------------------------------------- */
.pbook .qstep { text-align: center; margin-bottom: 1.6rem; }
.pbook .qstep > label, .pbook .qstep > .lbl {
   display: block; margin-bottom: .35rem;
   font-size: 1.2rem; font-weight: 700;
}
.pbook .qstep input[type=text], .pbook .qstep input[type=email],
.pbook .qstep input[type=tel] { text-align: center; }
.pbook .qstep .hint { text-align: center; margin-bottom: .5rem; }

.pcal { display: none; max-width: 21rem; margin: .4rem auto 0; padding: .6rem;
   border-radius: var(--r-lg); background: var(--paper);
   box-shadow: inset 0 0 0 1px var(--line); }
.pcal.is-ready { display: block; }
.pcal-head { display: flex; align-items: center; justify-content: space-between;
   gap: .5rem; margin-bottom: .4rem; }
.pcal-title { font-weight: 700; color: var(--pri); font-size: 1.1rem; }
.pcal-nav { width: 2.4rem; height: 2.4rem; border: 0; border-radius: 50%;
   background: var(--pri-sub); color: var(--pri); font-size: 1.5rem; line-height: 1;
   cursor: pointer; }
.pcal-nav:disabled { opacity: .35; cursor: default; }
.pcal-week, .pcal-month { display: grid; grid-template-columns: repeat(7, 1fr); gap: .25rem; }
.pcal-month[hidden] { display: none; }
.pcal-week span { padding: .3rem 0; font-weight: 700; font-size: .95rem; }
.pcal-pad { display: block; }
.pcal-day { display: flex; flex-direction: column; align-items: center; justify-content: center;
   min-height: 2.9rem; padding: .15rem 0; border: 0; border-radius: var(--r);
   background: var(--pri-sub); color: var(--pri); font: inherit; cursor: pointer;
   box-shadow: inset 0 0 0 1px transparent; }
.pcal-day:disabled { background: transparent; color: var(--muted); opacity: .45; cursor: default; }
.pcal-day.is-full:disabled { text-decoration: line-through; }
.pcal-day.is-today { box-shadow: inset 0 0 0 1px var(--pri); }
.pcal-day.is-sel { background: var(--pri); color: #fff; }
.pcal-day:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; }
.pcal-d { font-size: 1.05rem; font-weight: 600; line-height: 1.1; }
.pcal-n { font-size: .72rem; line-height: 1.1; opacity: .85; }
.pcal-key { margin: .5rem 0 0; }

.slotgroup { margin-bottom: .75rem; }
.slotgroup[hidden] { display: none; }
.slotday { margin: 0 0 .35rem; font-weight: 600; color: var(--muted); }
.has-cal .slotday { display: none; }
.pbook .picks { display: grid; grid-template-columns: 1fr; gap: .5rem; text-align: center; }
.pbook .pick > span { flex-wrap: wrap; column-gap: .35rem; }
.pbook .pick > span { justify-content: center; text-align: center; min-height: 3.1rem; }
.pbook .pick strong { color: var(--pri); }
.pbook .pick.is-off strong { color: var(--muted); }
.pbook .pick input:checked + span strong { color: inherit; }

.notice { margin: .6rem 0 0; color: var(--dng); font-weight: 700; text-align: center; }
.notice[hidden] { display: none; }

/* --- the welcome page: a map of the washes ---------------------------
   The map is Leaflet's, drawn into this box. Its pins are two coloured dots
   (our own washes in the brand colour, a partner's in slate) rather than
   Leaflet's marker image: that image is a file the app would have to bundle
   and a third thing the stylesheet could not recolour for the dark theme.
   --------------------------------------------------------------------- */
.placemap { position: relative; z-index: 0; isolation: isolate; height: 24rem; border-radius: var(--r-lg); overflow: hidden;
   background: var(--pri-sub); box-shadow: inset 0 0 0 1px var(--line); }
.placemap.is-failed { display: none; }
.pin { display: inline-block; width: 1.1rem; height: 1.1rem; border-radius: 50%;
   border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, .45); vertical-align: middle; }
.pin-wrap { display: grid; place-items: center; background: none; border: 0; }
.pin-wrap .pin { width: 1.35rem; height: 1.35rem; }
.pin-own { background: var(--pri); }
.pin-partner { background: #3d5a80; }
.placekey { margin: .6rem 0 0; }
.placepop { min-width: 12rem; line-height: 1.4; color: #1c2a33; }
.placepop strong { font-size: 1.05rem; }
.placepop .sub { color: #51606b; font-size: .85rem; }
.placepop .placehours summary { cursor: pointer; font-size: .85rem; color: #51606b; }
.placeacts { display: flex; gap: .4rem; margin-top: .5rem; flex-wrap: wrap; }
.welcome-in { margin-bottom: 1rem; }

/* --- havi összesítés: a day a line -----------------------------------
   The day list is its own grid and not a .dlist: it carries inputs and a
   row of buttons, and one line a day is the whole point -- a month is thirty
   of them and is read by running the eye down. The row is the grid; the
   form inside it is display: contents, so the cells are still the row's.
   Below 62rem the comment goes (it is in the title and in the CSV), and on a
   phone each day is two lines with the labels the cells carry. */
.mmonth-card { container: mm / inline-size; }
.mmonth { font-size: var(--fs-sm); --mm-cols: 6.6rem minmax(4.2rem, 6.4rem) 4.9rem 4.9rem 2.2rem minmax(6.6rem, 8.2rem) minmax(7.4rem, 9.6rem) minmax(0, 1fr) 2.6rem auto 1.4rem; }
.mhead, .mrow { display: grid; grid-template-columns: var(--mm-cols); align-items: center; column-gap: .5rem; padding: .2rem 1rem; }
.mhead { color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; background: var(--bg-sub, transparent); border-bottom: 1px solid var(--line); padding-block: .45rem; }
.mrow { border-bottom: 1px solid var(--line); min-height: 2.1rem; }
.mrow .right, .mhead .right { text-align: right; }
.mday-form { display: contents; }
.mrow > .mday-panel { grid-column: 1 / -1; display: none; padding: .5rem 0 .6rem; }
.mday-toggle:checked ~ .mday-panel { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: flex-end; }
.mday-panel .mday-note { display: flex; gap: .6rem; align-items: flex-end; flex: 1 1 24rem; }
.mday-panel .mday-note .field { flex: 1; margin: 0; }
.mday-panel .field { margin: 0; }
.mday-note-ro { grid-column: 1 / -1; margin: 0 0 .3rem; }
.mrow .c-comment, .mrow .c-plan, .mrow .c-clock, .mrow .c-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mrow .l-short { display: none; }
.mrow .c-act { display: inline-flex; gap: .25rem; justify-content: flex-end; white-space: nowrap; }
.mrow .c-act .btn.xs { padding: .1rem .38rem; font-size: .66rem; text-transform: uppercase; letter-spacing: .02em; cursor: pointer; line-height: 1.5; }
.mrow input.mday-time { width: 4.6rem; padding: .28rem .3rem; font-size: 1rem; text-align: center; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: .02em; }
.mrow input.mday-time::placeholder { color: var(--muted); font-weight: 400; opacity: .6; }
.mrow input.mday-time.is-bad { outline: 2px solid var(--dng); background: color-mix(in srgb, var(--dng) 12%, transparent); }
.mrow .c-state { font-size: var(--fs-xs); text-align: center; }
.mrow.is-saving .c-state::before { content: "\2026"; color: var(--muted); }
.mrow.is-ok .c-state::before { content: "\2713"; color: var(--suc); }
.mrow.is-err .c-state::before { content: "!"; color: var(--dng); font-weight: 700; }
.mrow.is-err input.mday-time { outline: 2px solid var(--dng); }

/* The predecessor's colours: solid, and the red wins over the yellow. */
.mrow.warn  { background: color-mix(in srgb, #ffe566 58%, transparent); }
.mrow.blue  { background: color-mix(in srgb, #7cc8ff 45%, transparent); }
.mrow.error { background: color-mix(in srgb, #ff7a7a 62%, transparent); }
.mrow:hover { filter: brightness(.97); }
.mkey { display: inline-block; width: .9rem; height: .9rem; border-radius: 3px; vertical-align: -.1rem; border: 1px solid var(--line); }
.mkey.warn  { background: color-mix(in srgb, #ffe566 58%, transparent); }
.mkey.blue  { background: color-mix(in srgb, #7cc8ff 45%, transparent); }
.mkey.error { background: color-mix(in srgb, #ff7a7a 62%, transparent); }

@container mm (max-width: 62rem) {
   .mmonth { --mm-cols: 5.5rem 4.2rem 4.4rem 4.4rem 1.6rem 5.6rem 6rem 1.7rem auto 1rem; }
   .mhead, .mrow { column-gap: .3rem; padding-inline: .6rem; }
   .mrow input.mday-time { width: 4.2rem; font-size: .95rem; }
   .mrow .l-full { display: none; }
   .mrow .l-short { display: inline; }
   .mhead > :nth-child(8), .mrow .c-comment { display: none; }
   .mrow .c-act { gap: .15rem; }
   .mrow .c-act .btn.xs { padding: .1rem .28rem; font-size: .62rem; }
}
@container mm (max-width: 44rem) {
   .mhead { display: none; }
   .mmonth .mrow { display: flex; flex-wrap: wrap; gap: .15rem .6rem; align-items: center; padding-block: .4rem; }
   .mmonth .mday-form { display: contents; }
   .mrow .c-comment { display: block; flex-basis: 100%; }
   .mrow .c-act { flex-basis: 100%; flex-wrap: wrap; justify-content: flex-start; }
   .mrow .c-hours, .mrow .c-over { text-align: left; }
   .mrow .c-plan::before, .mrow .c-clock::before { content: attr(data-label) ": "; color: var(--muted); }
}

@media print {
   .mrow .c-act, .mrow .c-state, .mday-panel, .page-head .btn, .page-head form, .hint, .sidebar, .topbar, nav { display: none !important; }
   .mrow input.mday-time { border: 0; background: transparent; padding: 0; width: auto; }
   .mmonth { --mm-cols: 6.4rem 5rem 3.4rem 3.4rem 2.2rem 7rem 8rem minmax(0, 1fr) 2.6rem; }
   .mhead > :nth-child(n+10), .mrow .c-act, .mrow .c-state { display: none !important; }
   .mrow, .mhead { padding-inline: .2rem; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
   .card { box-shadow: none; border: 0; }
}
.mrow > .mday-err { grid-column: 1 / -1; margin: 0 0 .25rem; color: var(--dng); font-size: var(--fs-xs); }
.mrow .c-act .btn.xs { margin: 0; }

/* The network picker (network_picker() in functions.php): one dropdown in the
   page head, in place of a row of pills. */
.netpick { display: inline-flex; align-items: center; gap: .4rem; margin: 0; }
.netpick select { min-width: 11rem; max-width: 18rem; font-weight: 600; }


/* "Download the app", for an Android browser: one slim line, dismissible. */
.apppromo { position: relative; display: flex; align-items: center; flex-wrap: wrap; padding: .6rem 2.4rem .6rem .9rem; margin-bottom: 1rem; }
.apppromo .apppromo-text { flex: 1 1 14rem; min-width: 0; margin: 0 .5rem .2rem 0; }
.apppromo .apppromo-text strong { display: block; color: var(--ink); }
.apppromo .apppromo-text .sub { display: block; font-size: var(--fs-xs); }
.apppromo .btn { flex: 0 0 auto; margin-right: .3rem; }
.apppromo .icon-btn { position: absolute; top: .15rem; right: .15rem; }
.apppromo[hidden] { display: none; }


/* The currencies page: chips for what is handled, one line per rate, one line
   per partner or washpoint. Margins and no `gap` (old WebViews). */
.curbar { display: flex; flex-wrap: wrap; margin: 0 -.25rem; }
.curchip { display: inline-flex; align-items: center; flex-wrap: wrap; margin: 0 .25rem .5rem; padding: .35rem .6rem;
   border: 1px solid var(--line); border-radius: var(--r); background: var(--paper); }
.curchip > * { margin-right: .5rem; }
.curchip.is-main { border-color: var(--pri); }
.curchip form.inline { display: inline-flex; }
.curadd { display: flex; align-items: center; flex-wrap: wrap; }
.curadd select { width: auto; flex: 0 1 18rem; margin-right: .4rem; }
.currate { display: flex; align-items: center; flex-wrap: wrap; padding: .25rem 0; }
.currate > * { margin-right: .5rem; }
.currate input[type=text] { width: 7rem; }
.currate input[type=date] { width: auto; }
.curcode { min-width: 4.5rem; }
.curlist { display: block; }
.currow { display: flex; align-items: center; justify-content: space-between; padding: .3rem 0; border-bottom: 1px solid var(--line-soft); }
.currow:last-child { border-bottom: 0; }
.currow .curname { flex: 1 1 auto; min-width: 0; margin-right: .6rem; overflow-wrap: anywhere; }
.currow select { width: auto; flex: 0 0 auto; max-width: 11rem; padding-top: .3rem; padding-bottom: .3rem; font-size: var(--fs-sm); }

/* A partner's rebate rules on the partner page: one line each, and the add form
   as a wrapping row of labelled fields. */
.rebrow { display: flex; align-items: center; justify-content: space-between; padding: .35rem 0; border-bottom: 1px solid var(--line-soft); }
.rebrow > span { flex: 1 1 auto; min-width: 0; margin-right: .6rem; }
.rebrow .sub { display: block; }
.rebrow.is-off { opacity: .6; }
.rebadd { display: flex; flex-wrap: wrap; align-items: flex-end; margin: .6rem -.25rem 0; }
.rebadd > * { margin: 0 .25rem .5rem; }
.rebadd label { flex: 1 1 9rem; min-width: 0; }
.rebadd label .lbl { display: block; font-size: var(--fs-xs); }
.rebadd input, .rebadd select { width: 100%; }

/* The partner's price grid: which cell is their own price, which has the
   percentage taken off, which is the plain list, and which they cannot be
   sold. */
.pdisc { display: inline-flex; align-items: center; }
.pdisc input[type=text] { width: 6rem; margin-right: .3rem; }
.pdisc .pdisc-unit { margin-right: .6rem; }
.pgrid td.own  { box-shadow: inset 3px 0 0 var(--pri); }
.pgrid td.disc { box-shadow: inset 3px 0 0 var(--wrn, #d9a21b); }
.pgrid td.base { box-shadow: inset 3px 0 0 var(--line); }
.pgrid td.locked, .pgrid th.locked { background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(128,128,128,.12) 6px 12px); cursor: not-allowed; }
.pgrid th.locked { text-decoration: line-through; opacity: .6; }
.pgrid td .g { margin-left: .4rem; }
.pgrid-key { display: inline-flex; align-items: center; flex-wrap: wrap; font-size: var(--fs-xs); }
.pgrid-key .kdot { display: inline-block; width: .55rem; height: .55rem; margin: 0 .25rem 0 .7rem; border-radius: 50%; background: var(--line); }
.pgrid-key .kdot.own { background: var(--pri); }
.pgrid-key .kdot.disc { background: var(--wrn, #d9a21b); }
.pgrid-key .kdot.off { background: transparent; border: 1px solid var(--muted); }
