/* =========================================================================
   md.domenet.info -- the site stylesheet
   Palette re-tinted 2026-10-28: the brand is #4eaae3 (sky), the other accents
   sit around it (teal success, violet info, amber warning, coral danger).
   -------------------------------------------------------------------------
   A port of the cwm.axh.hu design system (dev_files/cwm-stiluskonyv-
   2026-10-06.html, the owner's style book, 2026-10-28). Vanilla CSS, no
   build step, no third-party asset: the one external thing the style book
   loads, the Public Sans face, is served from web/assets/fonts instead.

   Sections 1 to 5 are the style book's text, kept verbatim where the
   component exists here and left out where it is cwm's own business (wash
   points, cameras, the shift board, invoices). Section 3, the shell, is the
   book's sidebar and topbar too (the owner, 2026-10-28), with the open and
   collapse switches done without a script. Section 9 holds the few rules
   this site needs beyond the book.

   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, message, data list ...
     5. utilities   a deliberately small set, last so they win
     7. error pages, column icons
     8. data list
     9. site additions

   Add new components in section 4 (or 9), 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. The class names are the contract: before
   writing a new one, look for the component in the style book.
   ========================================================================= */

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

:root {
   /* --- brand ------------------------------------------------------- */
   --pri:        #4eaae3;
   --pri-hover:  #2a86c5;
   --pri-sub:    #e0f0fb;   /* tinted background for the primary accent   */
   --sec:        #8592a3;
   --suc:        #2ec48f;
   --suc-sub:    #dff6ec;
   --inf:        #7b74f0;
   --inf-sub:    #e6e4fc;
   --wrn:        #f5a623;
   --wrn-sub:    #fdf0d5;
   --dng:        #ef5a4b;
   --dng-sub:    #fde1de;

   /* --- 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:         #e3ebf5;   /* 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(200, 228, 250, .90);  /* mint, top left             */
   --amb-2:      rgba(214, 221, 252, .90);  /* sky, top right             */
   --amb-3:      rgba(205, 239, 246, .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, #3f9edb, #2a82c0);
   --btn-grad-hov:  linear-gradient(135deg, #3592d2, #2574ae);
   --btn-glow:      rgba(42, 130, 192, .30);

   --btn-soft:      linear-gradient(135deg, #e8f3fc, #d5e8f7);
   --btn-soft-ink:  #2374ad;

   --btn-dang:      linear-gradient(135deg, #d6463a, #ab332a);
   --btn-dang-hov:  linear-gradient(135deg, #c53e33, #962c24);
   --btn-dsoft:     linear-gradient(135deg, #fde8e6, #fbd9d6);
   /* #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: #b32f27;

   --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(78, 170, 227, .07), rgba(78, 170, 227, 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(223, 246, 236, .78);
   --msg-info:      rgba(230, 228, 252, .78);
   --msg-warn:      rgba(253, 240, 213, .80);
   --msg-err:       rgba(253, 225, 222, .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:       #6dbdf0;
      --pri-hover: #8fccf5;
      --on-pri:    #0a2334;
      --pri-sub:   #15374f;
      --suc-sub:   #11372a;
      --inf-sub:   #26224f;
      --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(78, 170, 227, .12);
      --amb-2:      rgba(123, 116, 240, .09);
      --amb-3:      rgba(46, 196, 143, .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, #5fb4ec, #3d98d6);
      --btn-grad-hov:  linear-gradient(135deg, #75c1f1, #4ca5e0);
      --btn-glow:      rgba(0, 0, 0, .40);
      --btn-soft:      linear-gradient(135deg, #163b55, #10304a);
      --btn-soft-ink:  #6dbdf0;
      --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(17, 55, 42, .76);
      --msg-info:    rgba(38, 34, 79, .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:       #6dbdf0;
   --pri-hover: #8fccf5;
   --on-pri:    #0a2334;
   --pri-sub:   #15374f;
   --suc-sub:   #11372a;
   --inf-sub:   #26224f;
   --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(78, 170, 227, .12);
   --amb-2:      rgba(123, 116, 240, .09);
   --amb-3:      rgba(46, 196, 143, .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, #5fb4ec, #3d98d6);
   --btn-grad-hov:  linear-gradient(135deg, #75c1f1, #4ca5e0);
   --btn-glow:      rgba(0, 0, 0, .40);
   --btn-soft:      linear-gradient(135deg, #163b55, #10304a);
   --btn-soft-ink:  #6dbdf0;
   --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(17, 55, 42, .76);
   --msg-info:    rgba(38, 34, 79, .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%; }

/*
   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;
   }
}



/*
   The style book takes Public Sans from Google Fonts. This site loads no
   third-party asset, so the three weights the book asks for (400, 500, 600)
   and the italic come from web/assets/fonts (uswds/public-sans v2.001, SIL
   Open Font License, LICENSE.txt beside them). font-display: swap -- the
   system stack in --font is close enough that a late swap is not a jolt.
*/
@font-face {
   font-family: "Public Sans"; font-style: normal; font-weight: 400; font-display: swap;
   src: url("fonts/PublicSans-Regular.woff2") format("woff2");
}
@font-face {
   font-family: "Public Sans"; font-style: normal; font-weight: 500; font-display: swap;
   src: url("fonts/PublicSans-Medium.woff2") format("woff2");
}
@font-face {
   font-family: "Public Sans"; font-style: normal; font-weight: 600; font-display: swap;
   src: url("fonts/PublicSans-SemiBold.woff2") format("woff2");
}
@font-face {
   font-family: "Public Sans"; font-style: italic; font-weight: 400; font-display: swap;
   src: url("fonts/PublicSans-Italic.woff2") format("woff2");
}


/* =========================================================================
   3. SHELL -- sidebar, topbar, content
   -------------------------------------------------------------------------
   The style book's own shell (the owner, 2026-10-28: "use the one in the
   style book"); the two-row header and phone bar of menu proposal A are
   deferred (dev_files/PLAN-REDESIGN.md §3). The book's text follows,
   verbatim; the no-script switches are added at the end of the section.
   ========================================================================= */

.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; }

/* 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 {
   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; }
   .topbar { margin-inline: 1rem; padding-inline: 1rem; }
   .content { padding: 1rem; }
}



/* --- without a script ------------------------------------------------
   The style book's app.js toggles .is-open (the off-canvas panel) and
   .is-collapsed (icons only) on .layout. This site works without a script,
   so the two states are two hidden checkboxes at the top of .layout and
   the buttons are their <label>s; :has() reads them. The class forms are
   kept above so that markup from the book still behaves. The backdrop is
   a label for the same box, so tapping beside the panel closes it. */
.layout > .nav-switch { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.layout:has(> #nav-collapse:checked) { grid-template-columns: var(--menu-w-sm) minmax(0, 1fr); }
.layout:has(> #nav-collapse:checked) .sidebar-brand .name,
.layout:has(> #nav-collapse:checked) .nav-head,
.layout:has(> #nav-collapse:checked) .nav-item .label,
.layout:has(> #nav-collapse:checked) .nav-item .badge {
   position: absolute;
   width: 1px; height: 1px;
   overflow: hidden;
   clip: rect(0 0 0 0);
   white-space: nowrap;
}
.layout:has(> #nav-collapse:checked) .nav-item { justify-content: center; padding-inline: 0; }
.layout:has(> #nav-collapse:checked) .sidebar-brand { justify-content: center; padding-inline: 0; }
.layout:has(> #nav-open:checked) > .backdrop { opacity: 1; visibility: visible; }
/* The two buttons: the collapse switch on a desk, the open switch on a
   phone; the other one is not drawn. */
label.icon-btn { cursor: pointer; }
.topbar .nav-open-btn { display: none; }
/* The menu's icons: the same strokes the phone bar drew, at the book's
   .ico size. */
.nav-item .ico svg { width: 1.15rem; height: 1.15rem; display: block; stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
/* The super admins' group, told apart as the strip was: a warning caption. */
.nav-head.sys { color: #a67200; }
/* Who is signed in, on the topbar: the book's .who lockup as a link. */
.topbar a.who { text-decoration: none; }
.topbar a.who:hover .name { color: var(--pri); }
.topbar .ctx { display: inline-flex; align-items: center; gap: .5rem; font-size: var(--fs-sm); white-space: nowrap; }
.topbar form.inline { display: inline; }
/* A connected button group, ONE capsule: rounded on the outer sides only,
   the buttons joined. The language picker is one; any other form or div
   can opt in with .btn-capsule. Written with :first-of-type/:last-of-type
   because the picker's form also holds hidden inputs. */
.topbar form.lang, .authbox form.lang, .btn-capsule { display: inline-flex; gap: 0; }
.authbox form.lang { display: flex; width: max-content; margin: 0 auto 1rem; }
.topbar form.lang .btn { min-width: 2.5rem; padding-inline: .6rem; }
form.lang .btn, .btn-capsule > .btn { border-radius: 0; }
form.lang .btn + .btn, .btn-capsule > .btn + .btn { margin-left: 1px; }
form.lang .btn:first-of-type, .btn-capsule > .btn:first-child { border-top-left-radius: var(--r-pill); border-bottom-left-radius: var(--r-pill); }
form.lang .btn:last-of-type, .btn-capsule > .btn:last-child { border-top-right-radius: var(--r-pill); border-bottom-right-radius: var(--r-pill); }
/* The light / dark switch (a script-set data-theme, see layout app.php). */
.theme-btn { font-size: 1rem; }
.theme-btn .t-ico { display: none; }
html:not([data-theme]) .theme-btn .t-auto, html[data-theme="light"] .theme-btn .t-light, html[data-theme="dark"] .theme-btn .t-dark { display: inline; }
/* Back link row of a page: outside any card. */
.backrow { display: flex; }
.backrow.bottom { margin-top: var(--gap); }
/* A heading that stands between cards rather than inside one. */
.content > h2 { margin-bottom: calc(var(--gap) * -.55); }
.content > .crumbs + .page-head { margin-top: calc(var(--gap) * -.6); }

@media (max-width: 991px) {
   .layout:has(> #nav-open:checked) .sidebar { transform: none; }
   .layout:has(> #nav-collapse:checked) .sidebar-brand .name,
   .layout:has(> #nav-collapse:checked) .nav-head,
   .layout:has(> #nav-collapse:checked) .nav-item .label,
   .layout:has(> #nav-collapse:checked) .nav-item .badge {
      position: static; width: auto; height: auto; clip: auto;
   }
   .layout:has(> #nav-collapse:checked) .nav-item,
   .layout:has(> #nav-collapse:checked) .sidebar-brand { justify-content: flex-start; padding-inline: .75rem; }
   .layout:has(> #nav-collapse:checked) { grid-template-columns: minmax(0, 1fr); }
   .topbar .nav-collapse-btn { display: none; }
   .topbar .nav-open-btn { display: inline-grid; }
   .topbar .who .name { display: none; }
}
/* The impersonation banner comes before the grid and is sticky over it. */
.imp { position: sticky; top: 0; z-index: 60; }

/* --- dropdown (the book's section 4 component) ----------------------
   Markup contract, as in the style book:

     <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. The book's app.js toggles .is-open; this site has no script,
   so the same component is written as <details class="dropdown"> with a
   <summary> for the button: the browser toggles [open], the summary is
   focusable and answers Enter and Space, and the page works without JS.
   Both forms share every other class.
   ------------------------------------------------------------------- */

.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,
details.dropdown[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 <details> form: the summary is the button and shows no disclosure
   marker; a .who lockup inside it reads as the book's account button. */
details.dropdown > summary { list-style: none; cursor: pointer; }
details.dropdown > summary::-webkit-details-marker { display: none; }
details.dropdown > summary:focus-visible { outline: 2px solid var(--pri); outline-offset: 2px; border-radius: var(--r); }
details.dropdown > summary.who { display: inline-flex; align-items: center; gap: .6rem; }
details.dropdown[open] > summary.who .name { color: var(--pri); }
.dropdown-menu form.inline { display: block; }
.dropdown-menu .dropdown-head .sub { display: block; text-transform: none; letter-spacing: 0; color: var(--muted); }
.dropdown-menu .dropdown-head strong { display: block; color: var(--ink); text-transform: none; letter-spacing: 0; font-size: var(--fs-sm); }



/* =========================================================================
   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; }

/*
   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; }
.btn[disabled], .btn.is-disabled { opacity: .55; 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); }

.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;
}
.field .hint { display: block; margin-top: .3rem; color: var(--muted); font-size: var(--fs-xs); }
.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; }

/* --- 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;
}

/* --- 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; }

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); }

/* --- 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; }

/* --- 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);
}
.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; }

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

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); }


/* =========================================================================
   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; }

.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; }

.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); }


/* =========================================================================
   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); }

[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="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>"); }
[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;
}
.dlist-row:last-child > * { border-bottom: 0; }
.dlist-row:hover > * { background: var(--paper-2); }
.dlist-row > strong, .dlist-row a { color: var(--ink); }
.dlist-row a:hover { color: var(--pri); }
/*
   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. */

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

   .dlist-row {
      /* Two label/value pairs abreast when there is room for them, one when
         there is not. auto-fit rather than a fixed 2, so this needs no second
         breakpoint: the track count follows the width it actually gets. */
      /* 9rem, not a rounder number: a 390px phone leaves the card about
         19rem of inner width, so two tracks plus the .75rem gutter below
         have to come in under that or auto-fit quietly gives you one. */
      grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
      column-gap: .75rem;
      padding: .35rem 0;
      border-bottom: 1px solid var(--line);
   }
   .dlist-row:last-child { border-bottom: 0; }

   .dlist-row > * {
      display: grid;
      grid-template-columns: minmax(6rem, 34%) 1fr;
      min-width: 0;
      gap: .25rem 1rem;
      align-items: baseline;
      /* Grid items stretch by default, which turned every pill and every
         <code> chip into a full-width bar. The value should keep its own
         width; only text that is genuinely long should fill the track. */
      justify-items: start;
      padding: .3rem 1.5rem;
      border-bottom: 0;
      text-align: left;
      justify-self: stretch;
   }
   /* 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;
   }
   /* An icon label costs 1.25rem, so two of these sit side by side. */
   .dlist-row > *[data-icon] { grid-template-columns: 1.25rem 1fr; }

   /* A WORD label costs 6rem, and 6rem of label inside a 9rem track leaves
      about two characters for the value -- which is how "szerepkör" ended up
      set one letter per line. A column that kept its words keeps a whole row
      to put them in. */
   .dlist-row > *:not([data-icon]) { grid-column: 1 / -1; }

   .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;
      background: currentColor;
      color: var(--muted);
      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; }
}




/* =========================================================================
   9. SITE ADDITIONS
   -------------------------------------------------------------------------
   What this site's views say that the style book has no word for. Short on
   purpose; a rule that grows into a component belongs in section 4.
   ========================================================================= */

/* A caption beside or under a value, anywhere -- the style book scopes
   .hint to a form field; the views here put one next to a heading, in a
   cell, under a figure. Same size and ink as the field's. */
.hint { color: var(--muted); font-size: var(--fs-xs); }
.hint.bad, .bad { color: var(--dng); }
/* A long token (a request id, a path) wraps rather than widening its cell. */
code { overflow-wrap: anywhere; }

/* On the auth screen a message (the expired-form note, a flash) sits
   between the brand and the card; the card has no margin of its own. */
.authbox .msg { margin-bottom: 1rem; }

/* The pager draws page numbers (the owner's note 10, 2026-10-27), which the
   style book's prev / next pager does not: a numbered page is a pill, the
   current one filled. */
.pager a.pill { text-decoration: none; }
.pager a.pill:hover { color: var(--pri); box-shadow: inset 0 0 0 1px var(--pri); }
.pager .pill.on { background: var(--pri); color: var(--on-pri); }

/* A form that is a row of controls and one button (the sync form, a
   filter): the style book's .filterbar, which also stops its fields'
   bottom margin; the button sits on the baseline of the fields. */
.filterbar .actions { margin: 0; }
.filterbar .checks { padding-bottom: .55rem; }

/* The stat tiles of the Medio overview carry a bar under the figure. */
.tile .meter { margin-top: .35rem; }
.meter { height: 6px; border-radius: 3px; background: var(--paper-2); box-shadow: inset 0 0 0 1px var(--line-soft); overflow: hidden; }
.meter > i { display: block; height: 100%; background: var(--pri); }
.meter.dim { opacity: .25; }
