/* ===========================================================================
   HYPERINFRA staff console — console.css
   ---------------------------------------------------------------------------
   The stylesheet for every screen of the staff console.

   WHERE THIS COMES FROM
   Sections 01-09 are lifted directly from the public website's site.css so
   the console is the same object as the website, not a lookalike. The colours,
   the two typefaces, the paper grain, the hand-drawn pen lines and the buttons
   are byte-for-byte the same values.

   Sections 10 onward are new, and exist only in the console.

   ONE DELIBERATE RESTRAINT, worth reading before you judge the look.
   The public site draws a wobbly hand-drawn frame around every card. That is
   right for a page with four paragraphs on it. It is wrong for a screen that
   is mostly a table of IP addresses: a wobbly line beside every row makes the
   numbers harder to read, which is the one thing this screen exists to do.

   So the drawn hand is used where it carries meaning and does not fight the
   content — the sign-in card, the buttons, the icons, the section rules, the
   torn band edges — and data surfaces (tables, input boxes) stay calm. They
   still carry the family resemblance through colour, type and the slightly
   irregular corner radius. Nothing here is a rectangle with sharp corners.

   HOW THIS FILE IS ORGANISED
     01. Design tokens ........... every colour and size, named once
     02. Reset & base ............ defaults for every element
     03. Paper texture ........... the faint grain over the whole page
     04. Typography .............. headings, paragraphs, small print
     05. Hand-drawn parts ........ pen lines, sketch frames, wobbly rules
     06. Buttons & links ......... every clickable thing
     07. The console shell ....... top bar, side navigation, main area
     08. Page furniture .......... page heads, section heads, toolbars
     09. Cards ................... the boxed blocks, incl. project cards
     10. Tables .................. the address list and the staff list
     11. Forms ................... labels, inputs, validation messages
     12. Notices ................. success / problem / information messages
     13. Modals .................. add and confirm panels drawn in the page
     14. Sign-in ................. the one screen you see logged out
     15. Small pieces ............ pills, key-value blocks, copy button
     16. Utilities ............... one-job helper classes
     17. Accessibility & motion .. focus rings, reduced motion
     18. Responsive .............. phone and large-monitor adjustments
     19. Signed-in guard ......... hides the console until the server answers

   If you only want to change colours, edit section 01 and nothing else.
   =========================================================================== */


/* ===========================================================================
   01. DESIGN TOKENS
   :root means "the whole document", so these are readable everywhere.
   Every value in this block is copied from the public site's site.css.
   =========================================================================== */

:root {

  /* --- Paper colours (backgrounds) ------------------------------------- */
  --paper:        #F0EEE6;   /* the main warm ivory page background        */
  --paper-lift:   #F8F6F0;   /* slightly lighter — used for cards          */
  --paper-deep:   #E7E3D7;   /* slightly darker — used for alternate bands */

  /* --- Ink colours (text and drawn lines) ------------------------------
     Contrast against the ivory background has been checked:
       --ink   on --paper = 15.9 : 1   (body text needs at least 4.5 : 1)
       --ink-2 on --paper =  7.6 : 1
       --ink-3 on --paper =  5.5 : 1                                       */
  --ink:          #191917;   /* main text, and the colour of pen strokes   */
  --ink-2:        #4A4740;   /* secondary text                             */
  --ink-3:        #635F58;   /* quiet text: captions, small print          */
  --rule:         #D3CDBD;   /* hairline dividers                          */

  /* --- Accent colours --------------------------------------------------- */
  --clay:         #CC785C;   /* the warm terracotta accent                 */
  --clay-deep:    #A6512F;   /* darker clay, safe for text on paper        */
  --clay-wash:    #F0DCD2;   /* very pale clay, used as a highlight fill   */
  /* --kraft is for DRAWN LINES ONLY (rules, dashes, braces), never for
     text — it is too pale against ivory to be readable as type.           */
  --kraft:        #C89A6B;   /* muted tan, a second accent                 */
  --sage:         #5E6B57;   /* muted green: ticks, "done", "healthy"      */

  /* --- One more colour the website did not need -------------------------
     The console has to say "this is wrong" — a rejected password, an
     address that is not an address. The site's clay is a decorative accent
     and reads as warm rather than alarming, so a distinctly redder tone is
     introduced for genuine problems only. It is used for ICON AND TEXT,
     never as a coloured stripe down the side of a box.                    */
  --alarm:        #A32F26;   /* 7.4 : 1 on paper — safe for small text     */
  --alarm-wash:   #F5DED9;   /* very pale, used as a quiet fill            */

  /* --- Widths ----------------------------------------------------------- */
  --wrap:         1140px;    /* normal content width                       */
  --wrap-wide:    1360px;    /* wider content                              */
  --gutter:       clamp(1.25rem, 4vw, 3rem);  /* side padding, grows with screen */
  --sidebar:      232px;     /* the left-hand navigation column            */

  /* --- Vertical rhythm --------------------------------------------------- */
  --band-pad:     clamp(2rem, 4vw, 3.25rem);

  /* --- Stroke width used by all hand-drawn SVG -------------------------- */
  --pen:          1.9px;
  --pen-thin:     1.3px;

  /* --- The irregular corner radius that makes a CSS box look drawn -------
     Each corner gets a different curve. Three variants so that repeated
     elements on one screen are not identical to each other.               */
  --wobble-btn:   225px 14px 200px 16px / 16px 190px 14px 220px;
  --wobble-a:     14px 4px 12px 5px / 5px 13px 4px 14px;
  --wobble-b:     5px 13px 4px 14px / 13px 5px 14px 4px;

  /* --- Motion ------------------------------------------------------------ */
  --ease:         cubic-bezier(0.22, 0.61, 0.36, 1);
}


/* ===========================================================================
   02. RESET & BASE
   =========================================================================== */

/* Padding and border count inside an element's stated width. Without this,
   a box set to 100% wide with padding overflows its parent. */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;  /* stop iOS enlarging text on rotate */
}

body {
  margin: 0;

  /* Makes the footer sit at the bottom of the window even on a short page. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;

  background-color: var(--paper);
  color: var(--ink);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: clamp(0.95rem, 0.92rem + 0.14vw, 1.02rem);
  line-height: 1.6;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* Stops any element ever pushing the page sideways on a narrow screen. */
  overflow-x: hidden;
}

/* Images and SVGs never overflow their container. */
img,
svg {
  max-width: 100%;
  height: auto;
  display: block;
}

/* No default bullets anywhere; we add our own where wanted. */
ul, ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Form controls do not inherit the page font by default. Make them. */
input,
button,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* The hidden SVG "sprite" at the top of each page holds reusable drawings.
   It must take up no space and never be read out by a screen reader. */
.sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Hidden to the eye, still announced by screen readers. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Lets a keyboard user skip the navigation and go straight to the content. */
.skip-link {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -120%);
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: 0.7rem 1.2rem;
  font-size: 0.9rem;
  text-decoration: none;
  border-radius: 0 0 6px 6px;
  transition: transform 0.18s var(--ease);
}
.skip-link:focus { transform: translate(-50%, 0); }


/* ===========================================================================
   03. PAPER TEXTURE
   A very faint grain over the whole page, so the ivory reads as paper rather
   than as flat colour. Decorative only, and clicks pass straight through it.
   =========================================================================== */

body::after {
  content: "";
  position: fixed;              /* stays put while the page scrolls */
  inset: 0;                     /* covers the whole viewport */
  pointer-events: none;         /* never intercepts a click */
  z-index: 9;
  opacity: 0.4;
  mix-blend-mode: multiply;     /* darkens very slightly, like paper fibres */
  /* An inline SVG that generates noise — no image file needed. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.32'/%3E%3C/svg%3E");
}


/* ===========================================================================
   04. TYPOGRAPHY
   Headings use Fraunces (a warm serif). Body text uses Inter.
   clamp() means: never smaller than X, never larger than Z, scaling smoothly
   with screen width in between.
   =========================================================================== */

h1, h2, h3, h4 {
  font-family: 'Fraunces', Georgia, 'Times New Roman', serif;
  font-optical-sizing: auto;   /* lets the font adjust its shapes by size */
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;          /* avoids one lonely word on the last line */
}

h1 { font-size: clamp(1.75rem, 1.4rem + 1.5vw, 2.5rem); }
h2 { font-size: clamp(1.35rem, 1.2rem + 0.7vw, 1.75rem); }
h3 { font-size: clamp(1.05rem, 1rem + 0.28vw, 1.22rem); line-height: 1.3; }
h4 { font-size: 0.98rem; line-height: 1.3; }

p { margin: 0; }

/* An introductory paragraph — larger and lighter than normal body text. */
.lead {
  font-size: clamp(1rem, 0.96rem + 0.28vw, 1.14rem);
  line-height: 1.6;
  color: var(--ink-2);
  font-weight: 350;
}

.muted { color: var(--ink-3); }

.small {
  font-size: 0.85rem;
  line-height: 1.55;
}

.tiny {
  font-size: 0.78rem;
  line-height: 1.5;
}

/* A small all-caps label that sits above a heading. */
.eyebrow {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--clay-deep);
  margin-bottom: 0.7rem;
}

/* Anything that is a machine value — an IP address, a URL, a key. Set in a
   monospaced face so that digits line up and 0 cannot be mistaken for O. */
.mono {
  font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Roboto Mono', Menlo, Consolas, monospace;
  font-size: 0.9em;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}


/* ===========================================================================
   05. HAND-DRAWN PARTS
   The pieces that give the console the same sketched feel as the website.

   The trick used throughout: an SVG is stretched to any size with
   preserveAspectRatio="none", but each line carries
   vector-effect="non-scaling-stroke" so the pen stays the same thickness
   however far the shape is stretched. Without that, a wide box would have a
   thick top line and a thin side line.
   =========================================================================== */

/* Shared appearance for every hand-drawn line. */
.pen {
  fill: none;
  stroke: var(--ink);
  stroke-width: var(--pen);
  stroke-linecap: round;        /* rounded ends, like a felt-tip pen */
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.pen--thin    { stroke-width: var(--pen-thin); }
.pen--clay    { stroke: var(--clay); }
.pen--kraft   { stroke: var(--kraft); }
.pen--sage    { stroke: var(--sage); }
.pen--alarm   { stroke: var(--alarm); }
.pen--soft    { stroke: var(--ink-3); }
.pen--current { stroke: currentColor; }   /* takes the surrounding text colour */

/* A broken line, for something provisional rather than built. This MUST be a
   class and not a stroke-dasharray="..." attribute on the element: a CSS rule
   always beats an attribute, so an attribute pattern would be overwritten. */
.pen--dashed { stroke-dasharray: 6 8; }

/* Solid (filled) hand-drawn shapes, e.g. the dot inside an icon. */
.ink-fill   { fill: var(--ink);   stroke: none; }
.clay-fill  { fill: var(--clay);  stroke: none; }
.sage-fill  { fill: var(--sage);  stroke: none; }
.alarm-fill { fill: var(--alarm); stroke: none; }

/* --- The sketch frame: a wobbly rectangle drawn around a box ------------
   The parent needs position:relative; the frame then fills it exactly. */
.sketch-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;             /* sits behind the box's own text */
  pointer-events: none;   /* never blocks a click on the content inside */
  overflow: visible;      /* lets the pen overshoot slightly past a corner */
}

/* The drawn shape is ALSO the surface, so the paper and the pen line are
   exactly the same shape. This matters: with an ordinary rectangular
   background behind it, the square corners poke out past the places where the
   drawn line bows inwards, and the effect collapses into "a white box with a
   wonky box drawn on it". This selector is more specific than .pen, so it
   wins over that rule's fill:none. */
.sketch-frame use { fill: var(--paper-lift); }

/* --- A wobbly horizontal divider ---------------------------------------- */
.hand-rule {
  display: block;
  width: 100%;
  height: 11px;
  margin-block: clamp(1.25rem, 2.5vw, 2rem);
  overflow: visible;
}

/* --- A hand-drawn underline sitting under a word ------------------------
   Wrap the word: <span class="marked">word<svg class="mark">…</svg></span> */
.marked {
  position: relative;
  display: inline-block;
  white-space: nowrap;
}

.mark {
  position: absolute;
  left: -2%;
  width: 104%;
  bottom: -0.22em;
  height: 0.34em;
  pointer-events: none;
  overflow: visible;
}

/* --- The torn top edge of a band ---------------------------------------- */
.band__edge {
  position: absolute;
  left: 0;
  right: 0;
  top: -22px;
  width: 100%;
  height: 23px;
  display: block;
  pointer-events: none;
}


/* ===========================================================================
   06. BUTTONS & LINKS
   =========================================================================== */

/* Text links inside normal copy. */
.link {
  color: var(--clay-deep);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--clay) 45%, transparent);
  transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
}
.link:hover {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

/* Every button shares this base. The irregular border-radius is what makes a
   plain CSS box look as though it was drawn by hand. */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  padding: 0.68rem 1.35rem;
  font-family: 'Inter', sans-serif;
  font-size: 0.92rem;
  font-weight: 500;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  border: var(--pen) solid var(--ink);
  border-radius: var(--wobble-btn);
  background: transparent;
  color: var(--ink);
  transition: transform 0.16s var(--ease), background-color 0.18s var(--ease),
              color 0.18s var(--ease), box-shadow 0.18s var(--ease);
}

/* A tiny tilt and lift on hover, as if a sticker were being peeled up. */
.btn:hover {
  transform: translateY(-2px) rotate(-0.4deg);
  box-shadow: 3px 4px 0 color-mix(in srgb, var(--ink) 16%, transparent);
}
.btn:active { transform: translateY(0) rotate(0deg); box-shadow: none; }

/* Filled, high-emphasis. One per screen, at most. */
.btn--primary {
  background-color: var(--clay);
  border-color: var(--clay-deep);
  color: #FFF9F5;
}
.btn--primary:hover { background-color: var(--clay-deep); }

/* Outlined, lower emphasis. The default for most actions. */
.btn--ghost:hover { background-color: color-mix(in srgb, var(--ink) 5%, transparent); }

/* For an action that destroys something. Outlined, not filled: a filled red
   button invites the click it is warning you about. */
.btn--danger {
  border-color: var(--alarm);
  color: var(--alarm);
}
.btn--danger:hover { background-color: var(--alarm-wash); }

/* Smaller, for use inside a table row. */
.btn--small {
  padding: 0.4rem 0.85rem;
  font-size: 0.82rem;
  gap: 0.4rem;
}

/* Stretches to the full width of its container — used in forms. */
.btn--block { width: 100%; }

/* A button that is only an icon. Always needs an aria-label in the HTML. */
.btn--icon {
  padding: 0.45rem;
  width: 34px;
  height: 34px;
}

/* A disabled button states plainly that it cannot be pressed. */
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* The little arrow drawn inside some buttons. */
.btn__arrow {
  width: 17px;
  height: 11px;
  flex-shrink: 0;
  overflow: visible;
  transition: transform 0.2s var(--ease);
}
.btn:hover .btn__arrow { transform: translateX(3px); }

/* An icon sitting inside a button, before the label. */
.btn__icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  overflow: visible;
}

/* A row of buttons that wraps neatly on a narrow screen. */
.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  align-items: center;
}
.btn-row--end    { justify-content: flex-end; }
.btn-row--center { justify-content: center; }


/* ===========================================================================
   07. THE CONSOLE SHELL
   The frame every signed-in screen sits inside: a top bar across the whole
   width, a navigation column down the left, and the page itself on the right.
   =========================================================================== */

/* --- Top bar ------------------------------------------------------------- */
.top-bar {
  position: sticky;         /* stays visible while the page scrolls */
  top: 0;
  z-index: 50;
  background-color: var(--paper);
  border-bottom: 1px solid var(--rule);
}

.top-bar__inner {
  width: 100%;
  max-width: var(--wrap-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  min-height: 62px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* --- The logo, identical in construction to the website's --------------- */
.logo {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
  color: var(--ink);
  flex-shrink: 0;
}

.logo__mark {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  overflow: visible;
}

.logo__text {
  font-family: 'Inter', sans-serif;
  font-weight: 600;
  font-size: 0.95rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  line-height: 1;
  padding-top: 2px;   /* optically centres the capitals against the mark */
}

/* "Staff console", set beside the logo so it is never mistaken for the
   public website. Separated by a drawn slash rather than a border. */
.logo__suffix {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 0.95rem;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-3);
  white-space: nowrap;
}
.logo__suffix::before {
  content: "/";
  margin-inline: 0.5rem 0.45rem;
  color: var(--kraft);
}

/* --- Who is signed in, at the right-hand end of the bar ----------------- */
.whoami {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-width: 0;   /* lets a long email address shrink rather than overflow */
}

.whoami__email {
  font-size: 0.86rem;
  color: var(--ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- The two-column body ------------------------------------------------- */
.shell {
  flex: 1 0 auto;           /* takes the leftover height, pushing the footer down */
  width: 100%;
  max-width: var(--wrap-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: var(--sidebar) minmax(0, 1fr);
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: start;
  padding-block: clamp(1.5rem, 3vw, 2.5rem) clamp(2.5rem, 5vw, 4rem);
}

/* THIS ONE LINE IS LOAD-BEARING. Do not remove it.

   A grid child is born with min-width:auto, which means "never shrink below
   the natural width of what is inside me". The address table declares a
   minimum width of 620px so its columns stay readable, and scrolls sideways
   inside its own box when the screen is narrower than that.

   Without min-width:0 here, that 620px propagates outward: the main column
   refuses to be narrower than the table, the grid gives it the space, and the
   WHOLE PAGE scrolls sideways on a phone instead of just the table. The
   table's own scroll container is defeated from the outside.

   Measured on a 390px screen before this was added: the page ran 235px past
   the right-hand edge on the whitelist screen and 412px on the staff screen. */
.shell > * { min-width: 0; }

/* A page with no side navigation — the contents page. Same width and the
   same top and bottom spacing as .shell, so the two do not jump when you
   move between them, but one column instead of two. */
.shell-single {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  padding-block: clamp(1.5rem, 3vw, 2.5rem) clamp(2.5rem, 5vw, 4rem);
}

/* --- Side navigation ----------------------------------------------------- */
.side-nav {
  position: sticky;
  top: 86px;               /* clears the sticky top bar */
  display: grid;
  gap: 0.15rem;
}

.side-nav__group-label {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 1.4rem 0 0.5rem 0.8rem;
}
.side-nav__group-label:first-child { margin-top: 0; }

.side-nav__link {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.55rem 0.8rem;
  border-radius: var(--wobble-a);
  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.92rem;
  /* A transparent left border is reserved here so that marking an item
     active does not shift every other item sideways by 3px. */
  border-left: 3px solid transparent;
  transition: background-color 0.15s var(--ease), color 0.15s var(--ease);
}

.side-nav__link:hover {
  background-color: color-mix(in srgb, var(--ink) 5%, transparent);
  color: var(--ink);
}

/* The item you are currently on.
   This is the ONE place a coloured left border is used, and it is used
   because the border genuinely marks position in a vertical list — it says
   "you are here", which is exactly what a left edge in a vertical nav means.
   It is never used decoratively, or to indicate a category or a severity. */
.side-nav__link[aria-current="page"] {
  background-color: var(--paper-lift);
  border-left-color: var(--clay);
  color: var(--ink);
  font-weight: 500;
}

.side-nav__icon {
  width: 17px;
  height: 17px;
  flex-shrink: 0;
  overflow: visible;
  color: var(--ink-3);
}
.side-nav__link[aria-current="page"] .side-nav__icon { color: var(--clay-deep); }

/* A count sitting at the right-hand end of a navigation item. */
.side-nav__count {
  margin-left: auto;
  font-size: 0.76rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* --- Footer -------------------------------------------------------------- */
.console-footer {
  background-color: var(--paper-deep);
  padding-block: 1.25rem;
  font-size: 0.8rem;
  color: var(--ink-3);
}

.console-footer__inner {
  width: 100%;
  max-width: var(--wrap-wide);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
  justify-content: space-between;
}


/* ===========================================================================
   08. PAGE FURNITURE
   The heading block at the top of each screen, and the toolbar under it.
   =========================================================================== */

.page-head {
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

.page-head h1 { margin-bottom: 0.5rem; }

.page-head .lead { max-width: 62ch; }

/* A trail showing where you are, e.g. Projects / TP200 / IP whitelist. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.82rem;
  color: var(--ink-3);
  margin-bottom: 0.9rem;
}

.crumbs a {
  color: var(--ink-3);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.crumbs a:hover { color: var(--ink); border-bottom-color: var(--kraft); }

/* The separator between crumbs, drawn rather than typed. */
.crumbs__sep {
  width: 12px;
  height: 12px;
  overflow: visible;
  color: var(--kraft);
  flex-shrink: 0;
}

/* A heading with actions on the right of it. Wraps on a narrow screen. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.9rem;
  margin-bottom: 1.1rem;
}

.toolbar__title { display: flex; align-items: baseline; gap: 0.7rem; flex-wrap: wrap; }

/* A section heading inside a page. */
.section-head {
  margin-bottom: 1.1rem;
}
.section-head h2 { margin-bottom: 0.35rem; }
.section-head p  { color: var(--ink-2); font-size: 0.92rem; max-width: 68ch; }


/* ===========================================================================
   09. CARDS
   =========================================================================== */

/* The plain card: a calm surface for content that is not a table.
   No hand-drawn frame — see the note at the top of this file. */
.card {
  position: relative;
  background-color: var(--paper-lift);
  border: 1px solid var(--rule);
  border-radius: var(--wobble-a);
  padding: clamp(1.1rem, 2vw, 1.5rem);
}

/* Alternate the corner wobble so two adjacent cards are not identical. */
.card:nth-child(even) { border-radius: var(--wobble-b); }

.card h3 { margin-bottom: 0.4rem; }
.card p  { color: var(--ink-2); font-size: 0.92rem; }

/* The card variant that DOES carry the hand-drawn frame, for the few places
   where a single box is the whole point of the screen — the sign-in panel,
   and an empty state. position:relative is required so the frame can sit
   behind it, and there is deliberately no background-color: the drawn frame
   provides the surface. */
.card--drawn {
  background: none;
  border: none;
  border-radius: 0;
  padding: clamp(1.6rem, 3vw, 2.4rem);
}
.card--drawn > *:not(.sketch-frame) { position: relative; z-index: 1; }

/* --- A grid of cards ----------------------------------------------------- */
/* A fixed two-column grid rather than an automatic one. "auto-fit" was tried
   on the public site and rejected: with four items it produced three across
   and one stranded alone on a second row at common laptop widths. */
.grid {
  display: grid;
  gap: clamp(0.9rem, 1.8vw, 1.25rem);
  grid-template-columns: 1fr;
}

@media (min-width: 760px) {
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- The project card ---------------------------------------------------- */
/* One per project on the Projects screen. The whole card is a link. */
.project-card {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: transform 0.18s var(--ease), border-color 0.18s var(--ease),
              box-shadow 0.18s var(--ease);
}

.project-card:hover {
  transform: translateY(-2px) rotate(-0.15deg);
  border-color: var(--kraft);
  box-shadow: 3px 4px 0 color-mix(in srgb, var(--ink) 9%, transparent);
}

.project-card__head {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-bottom: 0.85rem;
}

/* The project's short key — TP200 — set as the loudest thing on the card. */
.project-card__key {
  font-family: 'Fraunces', Georgia, serif;
  font-size: 1.3rem;
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.project-card__icon {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  overflow: visible;
}

/* How many addresses the project has. Sits at the right of the card heading,
   opposite the project key. Quiet: it is a fact about the card, not the
   reason you are looking at it. */
.project-card__count {
  font-size: 0.8rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The two figures on a project card, side by side.
   No longer used — the cards carry one count now — but kept because the
   staff screen may want the same shape later. */
.project-card__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.9rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--rule);
}

.stat__label {
  display: block;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.2rem;
}

.stat__value {
  font-size: 0.95rem;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The arrow at the bottom of a project card, hinting it is clickable. */
.project-card__go {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 1rem;
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--clay-deep);
}
.project-card__go svg { width: 17px; height: 11px; overflow: visible; transition: transform 0.2s var(--ease); }
.project-card:hover .project-card__go svg { transform: translateX(3px); }


/* ===========================================================================
   10. TABLES
   The address list and the staff list. These are the screens people actually
   work in, so legibility beats decoration everywhere in this section.
   =========================================================================== */

/* The wrapper is what scrolls sideways on a narrow screen — never the page.
   Without this, a wide table pushes the whole document sideways. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--rule);
  border-radius: var(--wobble-a);
  background-color: var(--paper-lift);
  -webkit-overflow-scrolling: touch;
}

.table {
  width: 100%;
  border-collapse: collapse;
  min-width: 620px;   /* below this the columns are squeezed, so scroll instead */
}

.table thead th {
  text-align: left;
  font-size: 0.71rem;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
  background-color: var(--paper-deep);
}

.table tbody td {
  padding: 0.8rem 1rem;
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
  vertical-align: middle;
  color: var(--ink-2);
  font-size: 0.9rem;
}

/* The last row should not draw a line against the container's own edge. */
.table tbody tr:last-child td { border-bottom: none; }

.table tbody tr:hover { background-color: color-mix(in srgb, var(--ink) 3%, transparent); }

/* THIS ONE IS LOAD-BEARING TOO. Do not remove it.

   A cell may hold text somebody typed, and nothing obliges them to type a
   space. A label of 120 characters with no space, no hyphen and no
   punctuation in it is legitimate — 120 is the most the server accepts — and
   an unbreakable run like that decides the table's MINIMUM width.

   That defeats .table-wrap from the inside. The wrapper has overflow-x:auto
   so that a wide table scrolls in its own box and never moves the document,
   and that works when the table is wide because its columns are wide. It does
   not work when one cell cannot be made narrower: the table pushes the
   wrapper open, and the whole page scrolls sideways.

   Measured on the whitelist screen with a single 120-character label before
   this was added: the page ran 742px past the right-hand edge at 390px, 253px
   at 1280px and 138px at 1440px, and the row's own Remove button sat at
   x=1030 in a 390px window, where nobody could press it.

   `anywhere` rather than `break-word` on purpose: only `anywhere` lets the
   break count when the browser works out the smallest the cell could be, which
   is the number that sets the table's width. `break-word` breaks the line but
   still reserves the unbroken width, so the page keeps scrolling.

   There is a narrower version of this rule for the address column inside the
   max-width:700px block below. It is left where it is: it says something
   slightly different — that an IPv6 range may break on a phone — and this rule
   already covers it at every width.

   THE NUMBERS ABOVE WERE MEASURED WITH THE CHECK'S OWN LIST ON SCREEN — five
   rows, one of them carrying the 120-character label. They depend on how many
   rows are showing, so they are written down once, here, beside the rule they
   justify. Anywhere else that needs them points at this comment rather than
   quoting a different figure measured in a different state. */
.table tbody td { overflow-wrap: anywhere; }

/* AND THE EXCEPTION, which is not optional either.

   overflow-wrap is INHERITED, so the rule above reaches the button inside the
   actions cell as well. Below 700px that cell is deliberately allowed to wrap
   (`white-space: normal`, further down) so a long button label can take two
   lines. The two together let the browser decide the button could be ONE
   CHARACTER wide, and automatic table layout then gives the column exactly
   that: the Remove button collapsed from 102x31 to 63x113 and read down the
   page a letter at a time — R, e, m, o, v, e — on ordinary rows, at 390px.

   Nothing unbreakable can ever appear in this column: it holds button labels
   this project writes, not text anybody types. So it is turned back off here.
   `.table td.cell-actions` scores (0,2,1) against (0,1,2) above, so it wins on
   specificity without !important and regardless of the order of the two.

   Wrapping at spaces still works, so "Change my password" can still take two
   lines when a screen needs it. It is only breaking INSIDE a word that is
   turned off. */
.table td.cell-actions { overflow-wrap: normal; }

/* The column holding the address itself: the most important cell in the row. */
.table td.cell-primary {
  color: var(--ink);
  font-weight: 500;
}

/* The actions column, pushed to the right and never wrapping. */
.table th.cell-actions,
.table td.cell-actions {
  text-align: right;
  white-space: nowrap;
  width: 1%;              /* shrink to fit the buttons */
}

/* A cell holding a date or a count, so digits line up down the column. */
.table td.cell-num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* A quiet note under the main value in a cell, e.g. who added it.

   The font family is restated rather than inherited. These notes sit inside
   the address cell, which is monospaced so that digits line up — and without
   this line, an ordinary English sentence like "Administrator, signed in just
   now" inherits that monospace and comes out looking like code. */
.cell-sub {
  display: block;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: 0.78rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ink-3);
  margin-top: 0.15rem;
}

/* --- The table on a phone -------------------------------------------------
   Four columns cannot fit in 350px. The first attempt let the table keep a
   620px minimum and scroll sideways inside its own box, which LOOKED right —
   but the browser still laid the document out 625px wide and merely clipped
   the excess, leaving 235px of dead paper to the right of every phone screen
   and a table nobody would think to swipe.

   So the least important column is dropped instead, and what it held is
   restated underneath the label where there is room for it. Nothing is lost,
   the table keeps its real table semantics for screen readers, and the page
   is genuinely 390px wide rather than pretending to be.

   Which column is least important is a judgement: the address and what it is
   are why you are on the screen, and Remove is what you came to press. When
   something was added matters least, and it is the longest column. */

/* Restated detail, shown only once the date column has gone. */
.only-narrow { display: none; }

@media (max-width: 700px) {
  /* No longer forced wider than the screen, so nothing scrolls sideways. */
  .table { min-width: 0; }

  /* Drop the two columns that matter least, heading and cells together. */
  .cell-when { display: none; }   /* when it was added / last signed in */
  .cell-role { display: none; }   /* whether they can manage staff      */

  /* And put what they held back, underneath, where there is room. */
  .only-narrow { display: block; }

  /* Column headings may wrap. Left as nowrap they set the column's minimum
     width from the heading rather than from the data — "Can manage staff"
     alone was demanding 159px of a 350px screen. */
  .table thead th { white-space: normal; }

  /* The buttons stack instead of sitting side by side. Two full-size buttons
     on one line were claiming 265px, which is most of the screen for the
     least important column on it. */
  .table th.cell-actions,
  .table td.cell-actions { white-space: normal; }
  .table td.cell-actions .btn {
    display: flex;
    width: 100%;
    justify-content: center;
    /* A button's label may wrap onto a second line here. Everywhere else a
       button stays on one line, which is right — but "Change my password"
       kept on one line demands 188px of a 350px screen, and that sets the
       whole column's minimum. Two short lines cost nothing and keep every
       word, where shrinking to an icon would lose them. */
    white-space: normal;
    text-align: center;
    line-height: 1.25;
  }
  .table td.cell-actions .btn + .btn { margin-top: 0.35rem; }

  /* Last resort for a very long address: allow it to break rather than force
     the column wider than the screen. An IPv6 range is the case in point. */
  .table td.cell-primary { overflow-wrap: anywhere; }

  /* Tighter padding, so the remaining columns have more to work with. */
  .table thead th,
  .table tbody td { padding-inline: 0.7rem; }
}

/* --- The empty state ----------------------------------------------------- */
/* Shown in place of the table when there is nothing in the list yet. It says
   what the list is for and offers the one action that fills it. */
.empty {
  text-align: center;
  padding: clamp(2rem, 5vw, 3.25rem) 1.5rem;
}

.empty__art {
  width: 108px;
  height: 78px;
  margin: 0 auto 1.1rem;
  overflow: visible;
}

.empty h3 { margin-bottom: 0.45rem; }

.empty p {
  color: var(--ink-2);
  font-size: 0.92rem;
  max-width: 44ch;
  margin-inline: auto;
  margin-bottom: 1.4rem;
}


/* ===========================================================================
   11. FORMS
   =========================================================================== */

/* Vertical spacing between fields. */
.form { display: grid; gap: 1.05rem; }

/* Two fields side by side on a wide screen, stacked on a narrow one. */
.form-row {
  display: grid;
  gap: 1.05rem;
  grid-template-columns: 1fr;
}
@media (min-width: 620px) {
  .form-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.field { display: grid; gap: 0.35rem; }

.field__label {
  font-size: 0.84rem;
  font-weight: 500;
  color: var(--ink);
}

/* The word "optional" beside a label — stated on the field that IS optional,
   rather than marking every required field with an asterisk. */
.field__optional {
  font-weight: 400;
  color: var(--ink-3);
  font-size: 0.8rem;
}

/* A sentence under a label explaining what to type. Shown always, not only
   after something has gone wrong. */
.field__hint {
  font-size: 0.79rem;
  color: var(--ink-3);
  line-height: 1.45;
}

.input {
  width: 100%;
  padding: 0.62rem 0.8rem;
  background-color: var(--paper-lift);
  border: 1px solid var(--rule);
  border-radius: var(--wobble-a);
  color: var(--ink);
  font-size: 0.93rem;
  line-height: 1.4;
  transition: border-color 0.16s var(--ease), background-color 0.16s var(--ease);
}

.input::placeholder { color: color-mix(in srgb, var(--ink-3) 70%, transparent); }

.input:hover { border-color: var(--kraft); }

.input:focus {
  outline: none;                 /* replaced by the ring below, not removed */
  border-color: var(--clay);
  background-color: #FFF;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--clay) 22%, transparent);
}

/* An input holding a machine value — an IP address — is set monospaced. */
.input--mono {
  font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', 'Roboto Mono', Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

/* A field that has been rejected. The border reddens and an icon plus a
   sentence appear beneath it. Note there is no coloured stripe down one side
   of anything: the state is carried by the border all the way round, by the
   icon, and by words. */
.input[aria-invalid="true"] {
  border-color: var(--alarm);
  background-color: var(--alarm-wash);
}
.input[aria-invalid="true"]:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--alarm) 22%, transparent);
}

/* The sentence under a rejected field. */
.field__error {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  font-size: 0.81rem;
  color: var(--alarm);
  font-weight: 500;
}

.field__error svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  margin-top: 0.16rem;
  overflow: visible;
}

/* A checkbox with its label beside it. */
.check {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  cursor: pointer;
}

.check input {
  width: 17px;
  height: 17px;
  margin: 0.16rem 0 0;
  accent-color: var(--clay);
  flex-shrink: 0;
  cursor: pointer;
}

.check__text { font-size: 0.88rem; color: var(--ink-2); }
.check__text strong { color: var(--ink); font-weight: 500; }


/* ===========================================================================
   12. NOTICES
   A message about what just happened, or about the state of something.

   DELIBERATELY NOT a box with a coloured bar down its left-hand edge. That
   pattern is everywhere and has stopped carrying any meaning. Here the kind
   of message is told by three things that survive being glanced at: the
   ICON, its COLOUR, and a bold WORD naming the kind. The box itself is the
   same shape whatever the message.
   =========================================================================== */

.notice {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.8rem 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--wobble-a);
  background-color: var(--paper-lift);
  font-size: 0.88rem;
  color: var(--ink-2);
  margin-bottom: 1.1rem;
}

.notice__icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  margin-top: 0.1rem;
  overflow: visible;
}

.notice__body { min-width: 0; }   /* lets long text wrap instead of overflowing */

/* The bold word that names the kind of message, e.g. "Saved." */
.notice__label {
  font-weight: 600;
  color: var(--ink);
  margin-right: 0.3rem;
}

/* Something worked. Sage icon, sage label. */
.notice--done { background-color: color-mix(in srgb, var(--sage) 8%, var(--paper-lift)); }
.notice--done .notice__label { color: var(--sage); }

/* Something is wrong. Red icon, red label, and a slightly redder ground so
   it is distinguishable at a glance without a stripe. */
.notice--problem {
  background-color: var(--alarm-wash);
  border-color: color-mix(in srgb, var(--alarm) 35%, var(--rule));
}
.notice--problem .notice__label { color: var(--alarm); }

/* Worth knowing, nothing is wrong. Clay icon. */
.notice--info { background-color: var(--clay-wash); }
.notice--info .notice__label { color: var(--clay-deep); }

/* Close button at the right end of a dismissible notice. */
.notice__close {
  margin-left: auto;
  background: none;
  border: none;
  padding: 0.15rem;
  cursor: pointer;
  color: var(--ink-3);
  flex-shrink: 0;
  border-radius: 4px;
}
.notice__close:hover { color: var(--ink); }
.notice__close svg { width: 14px; height: 14px; overflow: visible; }


/* ===========================================================================
   13. MODALS
   The add and confirm panels. These are drawn inside the page — they are
   never the browser's own grey alert/confirm box, which cannot be styled,
   cannot be branded, and looks like a security warning.
   =========================================================================== */

/* The dimmed sheet behind the panel. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 1.25rem;
  background-color: color-mix(in srgb, #191917 46%, transparent);
  /* Hidden by default; the .is-open class reveals it. */
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.18s var(--ease), visibility 0.18s var(--ease);
}

.modal.is-open { opacity: 1; visibility: visible; }

.modal__panel {
  position: relative;
  width: 100%;
  max-width: 480px;
  max-height: calc(100vh - 2.5rem);
  overflow-y: auto;             /* a long panel scrolls inside itself */
  background-color: var(--paper);
  border: var(--pen) solid var(--ink);
  border-radius: var(--wobble-a);
  padding: clamp(1.25rem, 3vw, 1.75rem);
  box-shadow: 6px 8px 0 color-mix(in srgb, var(--ink) 18%, transparent);
  transform: translateY(8px) rotate(-0.2deg);
  transition: transform 0.18s var(--ease);
}

.modal.is-open .modal__panel { transform: translateY(0) rotate(-0.2deg); }

/* A wider panel, for one that holds a table or a long explanation. */
.modal__panel--wide { max-width: 620px; }

.modal__head {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  margin-bottom: 1rem;
}

/* The icon at the top-left of a panel, saying what kind of thing this is. */
.modal__icon {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  overflow: visible;
}

.modal__title { margin-bottom: 0.25rem; }

.modal__sub { font-size: 0.87rem; color: var(--ink-3); }

.modal__body { font-size: 0.92rem; color: var(--ink-2); }
.modal__body p + p { margin-top: 0.75rem; }

.modal__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  justify-content: flex-end;
  margin-top: 1.35rem;
}

/* On a phone the buttons stack and fill the width, so neither is a small
   target next to the other. The confirming action is listed last in the HTML
   and therefore sits at the bottom, under the thumb. */
@media (max-width: 480px) {
  .modal__actions { flex-direction: column-reverse; }
  .modal__actions .btn { width: 100%; }
}

/* The thing being acted on, quoted back inside a confirmation so you can see
   exactly what you are about to remove. */
.modal__subject {
  margin: 0.9rem 0;
  padding: 0.7rem 0.9rem;
  background-color: var(--paper-deep);
  border-radius: var(--wobble-b);
  font-size: 0.9rem;
  color: var(--ink);
  word-break: break-word;
}


/* ===========================================================================
   14. SIGN-IN
   The only screen anybody sees while logged out.
   =========================================================================== */

.signin {
  flex: 1 0 auto;
  display: grid;
  place-items: center;
  padding: clamp(2rem, 6vw, 4.5rem) var(--gutter);
}

.signin__inner {
  width: 100%;
  max-width: 420px;
}

/* The logo above the panel, centred, larger than in the top bar. */
.signin__brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1.75rem;
  text-align: center;
}

.signin__brand .logo__mark { width: 42px; height: 42px; }

.signin__title {
  font-size: clamp(1.5rem, 1.3rem + 0.8vw, 1.9rem);
  margin-bottom: 0.35rem;
}

/* The note under the sign-in panel: who to ask about a forgotten password,
   and the way back to the public website. It never carries an account name. */
.signin__note {
  margin-top: 1.25rem;
  text-align: center;
  font-size: 0.82rem;
  color: var(--ink-3);
  line-height: 1.6;
}


/* ===========================================================================
   15. SMALL PIECES
   =========================================================================== */

/* --- A pill: a short status word --------------------------------------- */
/* Kind is carried by the dot's colour AND the word, never by colour alone,
   so it still reads correctly to somebody who cannot distinguish the hues. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.6rem 0.2rem 0.5rem;
  border-radius: 999px;
  font-size: 0.76rem;
  font-weight: 500;
  white-space: nowrap;
  border: 1px solid var(--rule);
  background-color: var(--paper-lift);
  color: var(--ink-2);
}

.pill__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  background-color: var(--ink-3);
}

.pill--ok    { color: var(--sage);      border-color: color-mix(in srgb, var(--sage) 40%, var(--rule)); }
.pill--ok    .pill__dot { background-color: var(--sage); }

.pill--stale { color: var(--clay-deep); border-color: color-mix(in srgb, var(--clay) 45%, var(--rule)); }
.pill--stale .pill__dot { background-color: var(--clay); }

.pill--bad   { color: var(--alarm);     border-color: color-mix(in srgb, var(--alarm) 40%, var(--rule)); }
.pill--bad   .pill__dot { background-color: var(--alarm); }

/* A pill naming a role rather than a state — no dot. */
.pill--role  { padding-inline: 0.6rem; color: var(--ink-2); }

/* --- A key/value block, e.g. the public query URL ----------------------- */
.kv {
  background-color: var(--paper-lift);
  border: 1px solid var(--rule);
  border-radius: var(--wobble-a);
  padding: clamp(1rem, 2vw, 1.35rem);
}

.kv__row + .kv__row {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid color-mix(in srgb, var(--rule) 65%, transparent);
}

.kv__label {
  /* Holds a small icon beside its text. It must be a flex row: the reset
     makes every svg display:block, so as a plain block this label would put
     the icon on a line of its own above the words. */
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.71rem;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 0.4rem;
}

/* The value and its copy button, side by side. The value is allowed to
   scroll on its own if it is a long URL, rather than widening the page. */
.kv__value {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.kv__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  white-space: nowrap;
  padding: 0.45rem 0.65rem;
  background-color: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--wobble-b);
  color: var(--ink);
  scrollbar-width: thin;
}

/* A secret shown as dots until revealed. */
.kv__text--masked { letter-spacing: 0.12em; color: var(--ink-3); }

/* --- The "copied" confirmation ------------------------------------------
   A normal element in the page, announced politely to screen readers.
   Never a browser pop-up. */
.copy-status {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-left: 0.6rem;
  font-size: 0.8rem;
  color: var(--sage);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}
.copy-status.is-visible { opacity: 1; }
.copy-status svg { width: 14px; height: 14px; overflow: visible; }

/* --- A checklist with hand-drawn ticks ---------------------------------- */
.ticks { display: grid; gap: 0.65rem; }

.tick {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.7rem;
  align-items: start;
  color: var(--ink-2);
  font-size: 0.9rem;
}

.tick__icon {
  width: 19px;
  height: 19px;
  margin-top: 0.2rem;
  flex-shrink: 0;
  overflow: visible;
}

/* --- A boxed aside explaining something in plain language --------------- */
.callout {
  position: relative;
  padding: 1rem 1.2rem;
  margin: 1.25rem 0;
  background-color: var(--clay-wash);
  border-radius: var(--wobble-a);
}

.callout p { color: var(--ink-2); font-size: 0.89rem; }
.callout p + p { margin-top: 0.6rem; }

.callout__label {
  display: block;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--clay-deep);
  margin-bottom: 0.4rem;
}


/* ===========================================================================
   16. UTILITIES
   One-job classes, so no HTML file needs a style="..." attribute.
   =========================================================================== */

.mt-0   { margin-top: 0 !important; }
.mt-sm  { margin-top: 0.75rem; }
.mt-md  { margin-top: 1.25rem; }
.mt-lg  { margin-top: 2rem; }
.mb-0   { margin-bottom: 0 !important; }
.mb-sm  { margin-bottom: 0.75rem; }
.mb-md  { margin-bottom: 1.25rem; }
.mb-lg  { margin-bottom: 2rem; }

/* Hides an element completely. Used for messages that are in the page from
   the start and only shown when they have something to say. */
[hidden] { display: none !important; }

/* Pushes an element to the far end of a flex row, e.g. a status pill sitting
   at the right of a card heading. */
.push-right { margin-left: auto; }

/* --- Icon colour ---------------------------------------------------------
   Every shape in sprite.js is drawn with "currentColor", meaning "whatever
   colour the surrounding text is". These classes set that colour on the
   <svg> that holds the icon, so ONE definition of an icon can appear in ink
   in the sidebar and in red inside a warning, with no duplicate copies. */
.ic--ink   { color: var(--ink); }
.ic--soft  { color: var(--ink-3); }
.ic--clay  { color: var(--clay-deep); }
.ic--kraft { color: var(--kraft); }
.ic--sage  { color: var(--sage); }
.ic--alarm { color: var(--alarm); }

/* --- Icon size -----------------------------------------------------------
   overflow:visible matters on every one of these: the drawn lines overshoot
   their corners on purpose, and without it the browser clips the overshoot
   and the hand-drawn effect is lost. */
.ic {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  overflow: visible;
}
.ic--sm { width: 15px; height: 15px; }
.ic--lg { width: 26px; height: 26px; }


/* ===========================================================================
   17. ACCESSIBILITY & MOTION
   =========================================================================== */

/* A clear, on-brand focus ring for anybody navigating with a keyboard. */
:focus-visible {
  outline: 2px dashed var(--clay-deep);
  outline-offset: 3px;
  border-radius: 2px;
}

/* An input already draws its own focus ring (section 11), so the dashed
   outline would double up on it. */
.input:focus-visible { outline: none; }

/* Give the sticky top bar room so an anchor link does not land under it. */
[id] { scroll-margin-top: 86px; }

/* Respect the operating-system setting "reduce motion". */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .btn:hover,
  .project-card:hover { transform: none; }
}


/* ===========================================================================
   18. RESPONSIVE
   Written desktop-down for the shell, because the sidebar is the desktop
   case and collapsing it is the exception.
   =========================================================================== */

/* --- Below a small laptop: the sidebar becomes a strip above the content --
   A hamburger menu was rejected here: with only four destinations, hiding
   them behind a button costs a tap and gains nothing. */
@media (max-width: 900px) {

  .shell {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
  }

  .side-nav {
    position: static;             /* no longer sticky when it is a strip */
    display: flex;
    gap: 0.3rem;
    overflow-x: auto;             /* the strip scrolls, never the page */
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--rule);
    scrollbar-width: thin;
  }

  /* The group headings only make sense in a vertical list. */
  .side-nav__group-label { display: none; }

  .side-nav__link {
    flex-shrink: 0;
    /* The left border marked position in a column; in a row, an underline
       is what marks position, so the border moves to the bottom. */
    border-left: none;
    border-bottom: 3px solid transparent;
    border-radius: var(--wobble-a) var(--wobble-a) 0 0;
    padding-inline: 0.7rem;
  }

  .side-nav__link[aria-current="page"] {
    border-left-color: transparent;
    border-bottom-color: var(--clay);
  }

  .side-nav__count { display: none; }
}

/* --- Phones -------------------------------------------------------------- */
@media (max-width: 620px) {

  /* The suffix "/ Staff console" is dropped: at this width the logo and the
     signed-in email are competing for the same bar. */
  .logo__suffix { display: none; }

  /* The email is shown, but the sign-out button matters more, so the email
     is allowed to shrink hard before the button gives up any width. */
  .whoami__email { max-width: 34vw; }

  .toolbar { align-items: stretch; }
  .toolbar .btn-row { width: 100%; }
  .toolbar .btn-row .btn { flex: 1 1 auto; }
}

/* --- Very large monitors --------------------------------------------------
   The content is already centred by its max-width, so all that is wanted is
   a little more room and slightly larger text. Without this the console
   leaves a wide empty margin on a 2560px monitor and looks unfinished. */
@media (min-width: 1800px) {
  :root {
    --wrap:      1240px;
    --wrap-wide: 1560px;
    --sidebar:   252px;
  }
  body { font-size: 1.06rem; }
}


/* ===========================================================================
   19. THE SIGNED-IN GUARD
   ---------------------------------------------------------------------------
   WHAT PROBLEM THIS SOLVES. Every console page except the sign-in screen is
   only meant for somebody who has signed in, and the page cannot know whether
   they have until it has asked the server - which takes a moment. Without this
   rule, somebody who is signed out would see the whole console appear, fully
   drawn and apparently usable, for those few milliseconds before the answer
   came back and sent them to the sign-in screen. That is not a security hole
   (every page arrives empty; all the real information is behind the API, which
   refuses them) but it is a screen that lies about what is going on, and this
   project has already been bitten once by a control that looked pressable and
   was not.

   HOW IT WORKS. Each console page carries data-console-guard="checking" on
   its own <html> element, written into the file rather than added by a script,
   so it is already true the instant the browser starts drawing and there is no
   moment at all when it is not. assets/console.js removes it once the server
   has answered: either the person is signed in and the console appears, or
   they are not and the browser has already been sent to the sign-in screen.

   WHY visibility AND NOT display. visibility:hidden keeps the elements in the
   layout, so nothing jumps or reflows at the moment they appear. display:none
   would collapse the page to nothing and then push it back out, which reads as
   a flicker.

   IF JAVASCRIPT IS OFF nothing ever removes the attribute, so the console
   stays covered. That is why every console page also carries a <noscript>
   block, OUTSIDE the three parts hidden here, saying plainly that this console
   needs JavaScript. A blank page with an explanation on it is honest; a blank
   page without one is a page that looks broken.
   =========================================================================== */

[data-console-guard="checking"] .top-bar,
[data-console-guard="checking"] .shell,
[data-console-guard="checking"] .console-footer {
  visibility: hidden;
}

/* The message shown to a browser with JavaScript turned off. It sits outside
   the covered parts above, so it is the one thing such a browser can see.

   It is a class rather than a style="..." attribute on the element because the
   gate sends a Content-Security-Policy with no 'unsafe-inline' in it, which
   means the browser refuses inline styles - deliberately, since refusing them
   is most of what that policy buys. */
.noscript-notice {
  margin: 1.5rem;
}

/* ===========================================================================
   SECTION 21 — the projects screen, once projects became editable
   ---------------------------------------------------------------------------
   Added 2026-09-03, when an administrator gained the ability to create, rename
   and delete projects. Two things on that screen had nowhere to sit before:
   a button beside the heading, and a row of actions on each card.

   APPENDED AS A NEW SECTION, with no existing rule edited. That is the same
   discipline the public site's staff button follows and for the same reason:
   a stylesheet whose every existing byte is unchanged can be PROVED so with a
   file comparison, which is worth more than tidiness.
   =========================================================================== */

/* The heading and the New project button, side by side on a wide screen and
   stacked on a narrow one.

   The button goes UNDER the heading on a phone rather than beside it, because
   at 390px a heading and a button on one line leaves the heading about eleven
   characters wide. */
.page-head--with-action {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}

.page-head--with-action > div { flex: 1 1 22rem; }

@media (max-width: 620px) {
  .page-head--with-action > .btn { width: 100%; justify-content: center; }
}

/* The row of actions at the bottom of a project card.

   MARGIN-TOP: AUTO is what keeps the actions on the bottom edge of every card
   however long its description is. Without it a card with a one-line
   description puts its buttons halfway up, and a row of cards looks ragged.
   It works because .card is already a flex column. */
.project-card__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: auto;
  padding-top: 0.9rem;
}

/* "Manage list" keeps its arrow and its existing look; it just stops being the
   only thing on the line. push-right on the last button would fight the wrap,
   so the link takes the space instead. */
.project-card__actions .project-card__go { margin-right: auto; }

/* The card is a link no longer - it is a div holding a link and two buttons -
   so the hover it had as an <a> has to be asked for explicitly. */
.project-card:hover { border-color: var(--ink-soft, currentColor); }

/* ===========================================================================
   SECTION 22 — the language switcher
   ---------------------------------------------------------------------------
   Added 2026-09-03, at the owner's request: English and Chinese, switched from
   the bottom right of the footer.

   A PILL OF TWO BUTTONS, not a select. With two languages there is nothing to
   hide behind a dropdown, and a select would take three actions to change one
   setting. The active half is marked by weight and background rather than by a
   coloured stripe down its side - that pattern is banned in this project and
   would be the wrong signal anyway.
   =========================================================================== */

/* The footer's right-hand group: the switcher, then the hostname. Grouped so
   the switcher sits at the bottom RIGHT as asked, with the hostname beside it
   rather than being pushed onto its own line. */
.console-footer__right {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.lang-switch {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--rule, rgba(0, 0, 0, 0.18));
  border-radius: 999px;
  overflow: hidden;
  /* The footer sits on a slightly darker paper than the page; the pill sits on
     the page colour so it reads as a control rather than as text. */
  background: var(--paper, #f4f1ea);
}

.lang-switch__button {
  appearance: none;
  border: 0;
  background: transparent;
  cursor: pointer;
  font: inherit;
  font-size: 0.78rem;
  line-height: 1;
  padding: 0.34rem 0.7rem;
  color: var(--ink-soft, #6b6459);
  transition: background-color 120ms ease, color 120ms ease;
}

/* The active half. Marked by a filled background and a heavier weight - two
   signals rather than one, so it still reads for somebody who cannot tell the
   two backgrounds apart. */
.lang-switch__button.is-active {
  background: var(--ink, #2b2721);
  color: var(--paper, #f4f1ea);
  font-weight: 600;
}

.lang-switch__button:not(.is-active):hover {
  background: rgba(0, 0, 0, 0.05);
  color: var(--ink, #2b2721);
}

/* Keyboard focus has to be visible on both halves, including the filled one
   where an outline in the ink colour would disappear. */
.lang-switch__button:focus-visible {
  outline: 2px solid var(--clay, #b4542f);
  outline-offset: -2px;
}

/* On a phone the footer stacks, and the switcher goes with the hostname rather
   than being stranded on a line of its own. */
@media (max-width: 620px) {
  .console-footer__right {
    width: 100%;
    justify-content: space-between;
  }
}
