/*
  Graticula's console stylesheet: one token layer, then components built out of it.

  <b>Redesigned 2026-08-17 to an owner-supplied visual target</b> — a navy sidebar, a bright
  workspace, teal as the working accent and one blue used only for *public*. The brief was explicit
  that this is not a repaint: *"Do NOT make every element use the same font weight, color, border
  treatment, or visual importance."* So the file is ordered tokens → shell → components, and a
  component that needs a new colour takes one from the token block rather than inventing a hex.

  <b>What it replaced, and why the replacement is structural.</b> The old sheet was one flat list
  of selectors that had grown a section at a time; it had four places setting a panel's padding and
  two palettes' worth of greens. Both of today's UI defects (D-46 instances 8 and 10) were shapes
  rebuilt because finding the existing one was harder than writing another. Tokens plus a short
  component list is the cheapest defence against the next one.

  <b>What is deliberately not here.</b> No gradient as a surface, no shadow above 8 px of blur, no
  rounded corner above 8 px, and no badge for anything that is not a state. Those are the four
  things that turn an operations console into a template, and the brief named them.
*/

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

:root {
  /*
    <b>Two families of surface, because there are two environments.</b> The sidebar is navy and
    everything it holds is light-on-dark; the workspace is near-white and everything in it is
    dark-on-light. Keeping them as separate token sets is what stops a component drifting into the
    wrong one — a "muted text" that works on both would be washed out on one of them.
  */
  /*
    <b>The base is the owner's own number.</b> 2026-08-17: *"soldaki menünün renkleri daha şık. oradaki
    ana renk #171b49, bizdeki #111d34."* They are right about why, and the reason is the hue rather
    than the lightness — the two are within a few points of each other for brightness, but ours sat at
    210° (a blue-grey slate) and theirs at 234° (an indigo). Slate reads as *chrome*; indigo reads as
    *a product decided on a colour*.
    <b>The derived tokens moved with it rather than being kept.</b> A line and a hover surface mixed
    from slate against an indigo base is what makes a palette look almost-right — so `--nav-line`,
    `--nav-on` and the two greys are re-derived at the new hue.
  */
  --nav-base: #171b49;
  --nav-deep: #101336;
  --nav-line: #272c66;
  --nav-ink: #eceefb;
  --nav-muted: #9ea4d2;
  /*
    <b>Lightened 2026-08-19: 3.73:1 on `--nav-base`, which is the dark twin of the `--faint` bug
    above.</b> When `--faint` was fixed for the light surfaces the parallel token on the rail was
    never re-measured. It carries the item count beside a sidebar row — a second fact, but a fact.
    #868dbe is 5.09:1 on #171b49 and stays a step below `--nav-muted` (#9ea4d2, 6.74:1).
  */
  --nav-faint: #868dbe;
  --nav-on: #1e2359;

  --paper: #f6f8fa;
  --surface: #ffffff;
  --surface-2: #f1f4f8;
  --surface-3: #e7ecf3;
  --ink: #101826;
  --muted: #566275;
  /*
    <b>Darkened 2026-08-18 from #8b96a7, which was 2.99:1 on white and below AA.</b> A design review
    caught it on two *new* uses that carry information — a tab's item count and the *by whom* under a
    date — and then it turned out to be twenty uses, nearly all of them text: rail counts, meter
    labels, menu notes, the crossing-surface hint under a layer page. So it was never a decoration
    token; it was a second muted grey that happened to be too light to read.

    <b>#6b7787 is 4.55:1 and the same hue.</b> Nothing structural moves — it stays a step lighter than
    `--muted` (#566275, 5.89:1), which is what it is for: a second fact inside a cell whose first fact
    is already muted. A token that cannot be read is not a lighter shade of a token that can.

    <b>Darkened again 2026-08-19, because 4.55:1 was measured against one ground and used on four.</b>
    Three design reviews the same night measured the same failure independently, and so did an
    arithmetic check afterwards:

    | ground | #6b7787 | #616b7a |
    | --- | --- | --- |
    | `--surface` #ffffff | 4.55 | 5.40 |
    | `--paper` #f6f8fa | **4.28** | 4.71 → 5.07 |
    | `--surface-2` #f1f4f8 | **4.13** | 4.89 |
    | `--surface-3` #e7ecf3 | **3.83** | 4.54 |

    The three failures are not edge cases: `--paper` is the page's own background, and `h4, div.label`
    sits directly on it for every section label on Operations and Sources, as does `.tabstrip .count`
    for the scope counts on Studio's content screen and the group tabs. The 2026-08-18 fix was right
    about the token and checked it in one place.

    <b>#616b7a clears AA on the worst ground it is used on, which is the number that matters.</b> It is
    the lightest value that does — 4.54:1 on `--surface-3` — so the hierarchy above survives: still a
    step lighter than `--muted`, and 5.07:1 against 5.80:1 on the page's own ground.
  */
  --faint: #616b7a;
  --rule: #e3e8ef;
  --rule-strong: #cfd7e3;

  /* <b>The edge of a control, which is not the same thing as a line between two blocks.</b>
     WCAG 2.1 SC 1.4.11 asks 3:1 of the visual information that identifies a component, and a
     text box here has no fill of its own -- its border is the whole of what says *this is a
     box you type in*. Measured 2026-09-05 by computing the ratios rather than judging them:

       edge                      on #ffffff   on #f1f4f8   on #e7ecf3
       --rule        #e3e8ef           1.23         1.12         1.04
       --rule-strong #cfd7e3           1.45         1.31         1.22
       --control-edge #7e889a          3.57         3.24         3.01

     <b>A new token rather than a darker `--rule-strong`.</b> That one is used seventeen times
     and most of them are decoration -- a hover state, a separator dot, a dashed drop zone --
     which the standard does not ask anything of and which a darker grey would make heavier for
     nothing. This one is used only where a boundary is the control.

     <b>#7e889a is the lightest colour on this palette's own hue that clears 3.0 everywhere</b>,
     found by walking the ramp rather than by picking a grey that looked about right: the least
     ink that satisfies the rule, so the form does not turn into a grid of boxes. */
  --control-edge: #7e889a;

  /*
    <b>One working accent and one that means something.</b> Teal is *the product doing something* —
    primary buttons, the active section, focus. Blue appears in exactly one place, the `public`
    sharing state, because that is the one fact on these screens worth a second hue. Violet is the
    third and rarest: it marks the Studio side of the switch, so the two environments are told apart
    by hue and not only by which one is filled in.
  */
  --accent: #0d7d70;
  --accent-hot: #12a293;
  --accent-soft: #e3f2f0;
  --accent-line: #b8ded8;

  --blue: #2f56c8;
  --blue-soft: #eaefff;
  --blue-line: #c3cff5;

  --violet: #6a4bc0;
  --violet-soft: #f0ebfb;

  /*
    <b>Cyan is the sidebar's accent and teal is the workspace's, and they are not the same colour.</b>
    Owner refinement pass 2026-08-17: *"use a brighter cyan/teal accent for the selected navigation
    item"*, and separately that the page had gone monochrome. A single teal doing both jobs is what
    made it so — on navy it reads as grey-green, so the selected section had no presence. Cyan is
    bright enough to hold its own against a dark surface and would be garish on white, which is
    exactly why there are two.
  */
  --cyan: #2ad3c4;
  --cyan-deep: #0fb3ba;

  /*
    <b>The primary action's first stop, and it is not `--cyan-deep` — D-233.</b> White on
    `--cyan-deep` is 2.57:1 and the button's label is ordinary text by WCAG's measure, so the
    gradient did not reach 4.5:1 until 53% of its own width. This is the least ink on the same
    hue and saturation that clears it: the ramp walked at 1% steps, where 29% lightness gives
    4.49 and 28% gives 4.79. `--cyan-deep` keeps its own uses, which are decoration and a
    logotype rather than a ground for text.
  */
  --primary-from: #0b7f84;
  --indigo: #2a3f7a;
  --indigo-glow: #3b52a8;

  /*
    Status: hue *and* lightness differ, so a state is never carried by colour alone.

    <b>Two greens, for the reason there are two teals.</b> Owner 2026-08-17: *"service health'taki
    donut'ın renkleri, alttaki lejant renkleri güzel"* — theirs are brighter than ours were, and the
    reason ours were dark is that the same token was doing two jobs: text on a pale badge, where it
    must be dark enough to read, and a dot or an arc, where dark reads as muddy. So `--ok` stays the
    text colour and `--ok-dot` is the indicator, and the same for the other two.
  */
  --ok-dot: #21b56a;
  --warn-dot: #e0a020;
  --alert-dot: #e0503f;

  --ok: #1c7d4d;
  --ok-soft: #e6f4ec;
  --ok-line: #b6ddc6;
  --warn: #8a5a08;
  --warn-soft: #fdf2e0;
  --warn-line: #e8d3a8;
  --alert: #a5342a;
  --alert-soft: #fceceb;
  --alert-line: #edc4c0;

  --mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /*
    <b>Five sizes and three weights, and that is the hierarchy.</b> The brief asked for strong
    typographic hierarchy and warned against identical typography for every information level. A
    service name is 15/600; its type and counts are 12.5/400 muted; a section label is 11/600
    uppercase and faint. Nothing else is allowed a size.
  */
  /*
    <b>Raised about a tenth on 2026-08-17, in the tokens rather than per component.</b> Owner polish
    pass: *"increase the overall UI scale slightly (~10%). The reference feels more confident and
    less compressed."* Every size on these screens is one of these five, so the whole interface moves
    together and the ratios between the levels — which are the hierarchy — are unchanged. Editing
    twenty components by hand is how a type scale stops being one.
  */
  --t-page: 29px;
  --t-head: 17px;
  --t-body: 15px;
  --t-small: 13.5px;
  --t-label: 11.5px;

  --gap-1: 4px;
  --gap-2: 8px;
  --gap-3: 12px;
  --gap-4: 16px;
  --gap-5: 22px;
  --gap-6: 32px;

  --round: 5px;
  --round-lg: 8px;

  /*
    <b>Two shadows, both nearly invisible.</b> Depth here comes from the border and the surface,
    not from a glow; the brief's list of things to avoid has "excessive shadows" on it and this is
    what the restraint looks like as a number.
  */
  --lift: 0 1px 2px rgba(16, 24, 38, .05);
  --lift-2: 0 4px 12px -6px rgba(16, 24, 38, .18);

  accent-color: var(--accent);
}

* { box-sizing: border-box; }

/*
  <b>`hidden` has to win, and it did not.</b> The browser's rule for the attribute is
  `[hidden] { display: none }` at user-agent weight, which any author rule with a `display` beats —
  so `#surfaces { display: flex }` kept the Server/Studio switch on screen for a reader allowed only
  one surface, although the router had set `hidden` on it. Found 2026-08-17 the first time a
  publisher account existed to look with; it had been false the whole time. Written as a class
  rather than `#surfaces[hidden]` because the next element given both would fail the same way and
  nobody would connect the two (D-46 instance 9).
*/
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: var(--t-body)/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}

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

/*
  A visible focus ring, on one rule rather than per control. Keyboard reachability is not a feature
  to remember per element.
*/
:focus-visible { outline: 2px solid var(--accent-hot); outline-offset: 2px; border-radius: 3px; }

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

/*
  <b>Sidebar, then everything else.</b> Navigation is a column and the work is a column, which is
  the reference's arrangement and the reason it survives a narrow window: the sidebar collapses
  to a strip of icons rather than wrapping into the content. <b>The reader collapses it; no width
  does.</b> This said *at 980 px* until 2026-09-11, when D-221's measurements found no rule at
  980 or anywhere else — the strip is the 64-pixel state the toggle chooses.
*/
/* <b>232, not 248.</b> Handoff 2026-09-04: sixteen pixels back to the work column, which is where
   the screens that need width are — the members table wants 1100 and the symbology editor 1180. No
   sidebar label wraps at 232; measured against the longest, *Anonymous view*. */
#shell { display: grid; grid-template-columns: 232px 1fr; min-height: 100vh; }
#shell.tight { grid-template-columns: 64px 1fr; }

/*
  <b>Navy into indigo, with two glows and a graticule over them.</b> The first pass was one flat
  navy and the owner was right about it: *"the current sidebar is too flat and empty… the sidebar
  should become one of the strongest elements of the product identity."* Four layers, painted
  bottom-up, none of which is an image:

  1. a base navy, so text contrast is decided by one colour rather than by wherever a gradient
     happens to be;
  2. an indigo wash from the top-left, which is where the eye enters the column;
  3. a cyan glow low down, behind the decorative element, so the bottom of a tall empty column is
     not a dead rectangle;
  4. the graticule grid, masked to fade in downwards.

  <b>Restraint is in the numbers.</b> No stop is above 22% opacity and the whole thing resolves to
  one perceived colour from two feet away. The brief's line was *keep it professional — do not make
  it neon or gaming-like*, and that is what the ceiling on those numbers is for.
*/
#rail {
  background:
    radial-gradient(920px 420px at -12% -6%, rgba(88, 96, 214, .42), transparent 62%),
    radial-gradient(560px 460px at 12% 104%, rgba(42, 211, 196, .14), transparent 68%),
    linear-gradient(178deg, #1c2058 0%, var(--nav-base) 46%, #12153a 100%);
  border-right: 1px solid var(--nav-line);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

/*
  <b>The graticule grid is gone, by owner instruction 2026-08-17: *"grid kalksın."*</b> It was here as
  a reference to the product's own name — a graticule is the net of meridians and parallels
  (ADR-032) — and it was competing with the illustration for the same job. The illustration carries
  that reference better: each of its plates has a net drawn on it, in perspective, which is what a
  graticule actually looks like on a surface. Two textures saying one thing is one texture too many.
*/

/*
  <b>The decorative element, and it is the product's own subject.</b> The reference has an isometric
  stack of translucent layers low in the sidebar; ours is the same idea drawn as three skewed
  rectangles — a GIS layer stack, which is what a service *is* here. CSS transforms and gradients,
  no asset, no request.

  <b>Understated is a measurement, not an intention.</b> It sits at 0.5 opacity over the cyan glow,
  below the navigation, `pointer-events: none`, and it is the first thing to go when the column is
  collapsed. If it competes with a section name, the numbers are wrong.
*/
/*
  <b>The layer stack, and an atmosphere around it.</b> Owner polish pass: make the illustration more
  visible and *"introduce a very subtle cyan/blue/violet geospatial glow or dot/grid atmosphere
  around it. Do not make it neon."* Two additions and one number: a dot field behind the stack, a
  cyan-violet glow under it, and the opacity from 0.5 to 0.78.

  <b>The dots are a survey field, which is the honest version of *geospatial atmosphere*.</b> A
  radial-gradient repeated on a 22-pixel pitch — the same idea as the graticule higher up, at a
  different scale, so the two read as one texture rather than two decorations.
*/
/*
  <b>The illustration's box, and it sits higher than it did.</b> Owner 2026-08-17: *"biraz daha
  yukarıda olması lazım."* It was anchored 118 pixels off the bottom, which put it in the Collapse
  button's lap on a short window; 196 gives it the empty middle of the column, which is what the
  reference does with the space.

  <b>Everything else about it is in the markup now.</b> The plates, their meshes and the markers are
  one inline SVG — see index.html for why the CSS version's regular dot lattice was a limit rather
  than a choice.
*/
/*
  <b>It gives way to the navigation, since 2026-09-06.</b> It was anchored 236 pixels off the
  bottom — a number tuned when Server's rail held six rows. Publish made it seven, and at the
  owner's window size the plates sat behind *Members*, *Roles*, *Operations* and *Logs*. The
  labels were still on top (`#tabs` carries `z-index: 1`) and still unreadable, which is the
  worse half: nothing was broken enough to notice from a test.

  <b>Laid out rather than positioned, so the next row cannot do it again.</b> The rail is a flex
  column; the art is a flex item that takes what is left between the navigation and the Collapse
  button and is allowed to shrink to nothing. A magic offset would have needed retuning the day
  somebody adds an eighth screen, and nobody would connect the two.
*/
#railArt {
  flex: 0 1 auto; min-height: 0; align-self: stretch;
  width: 100%; height: auto; margin: auto 0 24px;
  pointer-events: none; opacity: .92;
}

/* Below this there is no leftover space worth an illustration, and a squeezed one reads as a
   rendering fault rather than as a decoration. */
@media (max-height: 760px) { #railArt { display: none; } }

#shell.tight #railArt { display: none; }

#brand {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  padding: 20px var(--gap-4) 22px;
  margin-bottom: var(--gap-2);
  border-bottom: 1px solid rgba(255, 255, 255, .06);
  position: relative;
  z-index: 1;
}

#brand .mark {
  width: 28px; height: 28px; flex: none;
  border-radius: 7px;
  background: linear-gradient(148deg, var(--cyan), var(--cyan-deep) 55%, #2a5fb0);
  display: grid; place-items: center;
  color: #fff; font-size: 13px; font-weight: 700;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .18);
}

/*
  <b>Studio's column is violet, and the reason is that the switch already said so.</b> Owner
  2026-08-18: Server keeps its indigo, Studio gets something else, colour my choice. The choice is
  not free — the environment switch has been teal for Server and violet for Studio since it was
  built, so a violet column makes the hue say the same thing in two places instead of two things in
  two places. A reader who learns one gets the other for nothing.

  <b>Same construction, same brightness band, different hue.</b> `#281748` sits within a few points
  of `#171b49` for lightness and about 50 degrees away in hue: the two read as one product at two
  stations rather than as two applications. Every derived token is re-mixed at the new hue, for the
  reason the last change was — a line mixed from indigo against a violet base is what makes a
  palette look almost-right.

  <b>The illustration does not change.</b> Its plates run teal to violet in both surfaces, because it
  is the product's subject and not the environment's badge.
*/
:root[data-surface="studio"] {
  --nav-base: #281748;
  --nav-deep: #1c0f36;
  --nav-line: #3d2668;
  --nav-ink: #f1ecfb;
  --nav-muted: #b3a4d8;
  /* 3.93:1 on Studio's violet base, same finding as Server's. #9c8cc8 is 5.36:1 and stays below
     `--nav-muted` (#b3a4d8). */
  --nav-faint: #9c8cc8;
  --nav-on: #33205c;
}

:root[data-surface="studio"] #rail {
  background:
    radial-gradient(920px 420px at -12% -6%, rgba(146, 92, 226, .40), transparent 62%),
    radial-gradient(560px 460px at 12% 104%, rgba(42, 211, 196, .12), transparent 68%),
    linear-gradient(178deg, #301c57 0%, var(--nav-base) 46%, #1f1139 100%);
}

/*
  The selected section keeps its shape and changes its light: orchid where Server is cyan, so the two
  columns differ by the same amount their switch halves do.
*/
:root[data-surface="studio"] #tabs a[data-tab] { color: #c6bbe4; }
:root[data-surface="studio"] #tabs .ico { color: #a394cf; }
:root[data-surface="studio"] #tabs a[data-tab]:hover .ico { color: #ded4f7; }
:root[data-surface="studio"] #tabs a[data-tab][aria-current="page"] {
  background: linear-gradient(96deg, rgba(180, 145, 255, .22), rgba(120, 90, 210, .24));
  box-shadow: inset 3px 0 0 #b491ff, var(--lift);
}
:root[data-surface="studio"] #tabs a[data-tab][aria-current="page"] .ico { color: #c9adff; }
:root[data-surface="studio"] #brand .mark {
  background: linear-gradient(148deg, #c9adff, #8a5fe0 55%, #5b3fc4);
}
:root[data-surface="studio"] #collapse { color: #d9d1f2; }


#brand h1 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -.01em;
  color: var(--nav-ink);
  white-space: nowrap;
}

#shell.tight #brand h1 { display: none; }

/*
  <b>The section list, which is the tab strip moved into the sidebar.</b> Same links, same router,
  same `aria-current` — the element kept its id so nothing in the router changed. A section is a
  row with a left edge, not a pill: the edge shows selection without adding a shape, which is what
  the brief meant by obvious selection without excessive borders.
*/
#tabs {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--gap-2) var(--gap-2) var(--gap-4);
  position: relative;
  z-index: 1;
}

#tabs a[data-tab] {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 12px 15px;
  border-radius: 7px;
  color: #bcc1e8;
  font-size: var(--t-body);
  text-decoration: none;
  white-space: nowrap;
  transition: background .12s, color .12s;
}

#tabs a[data-tab]:hover {
  background: rgba(255, 255, 255, .055);
  color: #fff;
  text-decoration: none;
}

/*
  <b>The selected section is a filled row with a cyan edge and a cyan glyph.</b> The first pass gave
  it a slightly lighter background and a 3-pixel bar, which the owner read as no presence at all —
  correct: on navy, a teal bar and a 6% lift are both nearly invisible. Three signals now, and none
  of them is only colour: the surface lifts, the glyph goes cyan, and the label goes to full white
  at a heavier weight.
*/
#tabs a[data-tab][aria-current="page"] {
  background: linear-gradient(96deg, rgba(42, 211, 196, .18), rgba(96, 104, 220, .24));
  box-shadow: inset 3px 0 0 var(--cyan), var(--lift);
  color: #fff;
  font-weight: 600;
}
#tabs a[data-tab][aria-current="page"] .ico { color: var(--cyan); opacity: 1; }

/*
  <b>And the count goes up with the label, because the selected row's ground is lighter.</b> The rule
  above lifts the surface with a teal-to-indigo gradient, which takes the effective background from
  `--nav-base` #171b49 to about #2b477d on Server and #533c88 on Studio. `--nav-faint` was fixed for
  the *base* and measures 2.86:1 and 2.77:1 against those — worse than before the row was selected,
  which is the wrong direction for the row you are on. #c3c8ea is 5.55:1 and 5.39:1, and stays quieter
  than the label's full white beside it. Measured 2026-08-19, after a design review's own measurement
  said 3:1 and my first check said 5.09 — both were reading a different pair, and the gradient is why.
*/
#tabs a[data-tab][aria-current="page"] .count { color: #c3c8ea; }

/*
  The glyph, from a token so a section cannot arrive without one. Brighter than the label on purpose
  — the owner asked for icon contrast, and an icon at the same weight as its text is decoration.
*/
#tabs .ico { width: 21px; flex: none; text-align: center; font-size: 17px; color: #949bd4;
             opacity: 1; }
#tabs a[data-tab]:hover .ico { color: #d6d9f7; }
#tabs .label { font-size: var(--t-body); letter-spacing: 0; text-transform: none;
               font-weight: inherit; color: inherit; margin: 0; }
#collapse .label { font-size: var(--t-small); letter-spacing: 0; text-transform: none;
                   font-weight: inherit; color: inherit; margin: 0; }
#shell.tight #tabs .label { display: none; }
#shell.tight #tabs a[data-tab] { justify-content: center; padding: 10px 0; gap: 0; }

#tabs .count {
  margin-left: auto;
  font: 500 var(--t-small)/1 var(--mono);
  color: var(--nav-faint);
}

#shell.tight #tabs .count { display: none; }

/*
  <b>No rule above Collapse.</b> Owner 2026-08-17: *"collapse gridin içinde kaldığı için silik
  duruyor."* The grid is gone in the same pass, and with it the reason this hairline existed — it was
  separating the button from a texture. It is now the only line in a column that has none, which reads
  as a leftover, and the whitespace above the button already separates it from the navigation.
*/
#railFoot {
  margin-top: auto;
  padding: var(--gap-3) var(--gap-2) var(--gap-4);
  position: relative;
  z-index: 1;
}

/*
  <b>Collapse reads now.</b> Owner 2026-08-17: *"collapse çok silik."* It was `--nav-faint` at the
  small size — a control drawn as though it were a caption. It is a control: the label goes to the
  navigation's own ink at 500, and the arrow gets its own line-height so the two sit on one baseline.
  Still quiet relative to a section, because it is not navigation.
*/
#collapse {
  width: 100%;
  display: flex; align-items: center; gap: 11px;
  background: none; border: 0; cursor: pointer;
  padding: 10px 14px; border-radius: 7px;
  color: #d3d7f4; font: 500 var(--t-body)/1 var(--sans);
}
/*
  <b>The arrow points at what pressing it will do.</b> Owner 2026-08-18: *"collapse olunca ok öbür
  yöne mi dönse?"* Yes — an arrow on a toggle names the next state, not the current one, so a
  collapsed column shows the arrow that widens it. Flipped rather than swapped for another character:
  one glyph that turns is one glyph, and the two directions cannot drift apart.
*/
#collapse .arrow { font-size: 15px; line-height: 1; opacity: .8;
                   transition: transform .16s ease-out; }
#shell.tight #collapse .arrow { transform: scaleX(-1); }
#collapse:hover { background: rgba(255, 255, 255, .06); color: #fff; }
#collapse:hover .arrow { opacity: 1; }
#shell.tight #collapse .label { display: none; }
#shell.tight #collapse { justify-content: center; }

/*
  <b>An ambient tint under the workspace, and the panels stay white.</b> Owner polish pass: *"the main
  workspace is currently too uniformly gray. Introduce an extremely subtle ambient cyan/violet tint or
  gradient in the page background, especially around the upper content area."*

  <b>Two washes, both under 4%, and neither is a surface.</b> Cyan enters from the left where the
  content starts and violet from the right where the primary action sits, so the tint agrees with
  where the eye already goes. `background-attachment: fixed` keeps it a property of the window rather
  than of the scroll position — a gradient that slides under a long table reads as a rendering fault.
*/
#main {
  min-width: 0; display: flex; flex-direction: column;
  background:
    radial-gradient(1100px 520px at 6% -8%, rgba(42, 211, 196, .07), transparent 70%),
    radial-gradient(900px 460px at 96% -12%, rgba(122, 92, 220, .06), transparent 72%),
    var(--paper);
  background-attachment: fixed;
}

/* --------------------------------------------------------------------- top bar */

header {
  display: flex;
  align-items: center;
  gap: var(--gap-4);
  padding: 0 var(--gap-5);
  height: 58px;
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  position: sticky; top: 0; z-index: 900;
}

header .right { margin-left: auto; display: flex; align-items: center; gap: var(--gap-3); }

/*
  <b>The environment switch, and its two halves are told apart by hue.</b> ADR-034: an
  administrator moves between Server and Studio constantly, and the switch is only in the document
  for somebody who may be in both. Filled is where you are; teal is Server and violet is Studio, so
  the answer to *which one am I in* does not depend on noticing which button is darker.
*/
#surfaces { display: flex; gap: 3px; padding: 3px; background: var(--surface-2);
            border-radius: var(--round); border: 1px solid var(--rule); }
#surfaces a {
  padding: 5px 14px; border-radius: 3px; font-size: var(--t-small); font-weight: 500;
  color: var(--muted); text-decoration: none;
}
#surfaces a:hover { color: var(--ink); text-decoration: none; }
#surfaces a[data-surface="server"][aria-current] { background: var(--accent); color: #fff; }
#surfaces a[data-surface="studio"][aria-current] { background: var(--violet); color: #fff; }

#healthDot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--faint); }
#healthDot.ok { background: var(--ok); }
#healthDot.degraded { background: var(--warn); }
#healthDot.unusable { background: var(--alert); }
#healthLine { font-size: var(--t-small); color: var(--muted); }

#who { font-size: var(--t-small); color: var(--muted); text-align: right; line-height: 1.35; }
#who b { display: block; color: var(--ink); font-size: var(--t-body); font-weight: 600; }

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

/*
  <b>The workspace uses the width it has.</b> Owner refinement pass: *"the current content area feels
  slightly compressed toward the top-left… use the available desktop width more confidently."* The
  padding grows with the viewport instead of being one number, and the ceiling is high enough that a
  wide screen is filled and low enough that a line of prose never runs to 200 characters.
*/
.wrap { padding: var(--gap-6) clamp(var(--gap-5), 3vw, 44px); max-width: 1720px; }

/*
  <b>A page says what it is.</b> The brief asked for a strong heading and a secondary line, and the
  pair is worth more than it looks: this console has eight screens that were previously
  distinguishable only by which table was on them.
*/
.pagehead { display: flex; align-items: flex-start; gap: var(--gap-4); margin-bottom: var(--gap-6); }

/*
  <b>A stronger title, and the strongest thing on the page.</b> 30 pixels at 650 against a service
  name's 15.5 at 650: the difference is size and nothing else, which is what makes them read as the
  same voice at two levels rather than as two decisions.
*/
.pagehead h2 {
  margin: 0; font-size: 33px; font-weight: 650; letter-spacing: -.028em; line-height: 1.1;
}
.pagehead p { margin: 6px 0 0; color: var(--muted); font-size: 15.5px; }
.pagehead .right {
  margin-left: auto; display: flex; align-items: center; gap: var(--gap-2);

  /* <b>`flex: none`, because the default let a verb break in half.</b> The title block beside it is
     `flex: 0 1 auto` too, so a long subtitle shrank this slot below its buttons' natural width and
     a group's page printed *Add / member* and *Share a / service* on two lines each at 1366px. The
     title is the thing with a good line break in it; a button label is not. */
  flex: none;
}

.lede { color: var(--muted); max-width: 88ch; margin: 0 0 var(--gap-4); }

.panel {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--round-lg);
  box-shadow: var(--lift);
  margin-bottom: var(--gap-4);
}

.panel > table { margin: 0; }

.pad { padding: var(--gap-4) var(--gap-5); }

/*
  A section label: the smallest, faintest thing on the page, and the reason a panel does not need a
  heavier heading to be legible.
*/
/*
  <b>`.label` is a section label and nothing else, and naming it that was a mistake for one build.</b>
  The sidebar's rows carry a `.label` span for their text, so this rule shouted every section name —
  SERVICES, DATA SOURCES, COLLAPSE — in 11-pixel uppercase. A class named after a generic English
  word will collide; the fix is the scope, because renaming one of the two would leave the next
  `.label` to collide again.
*/
h4, div.label, .widget > .label, .rail-head {
  margin: 0 0 var(--gap-3);
  font-size: var(--t-label); font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--faint);
}
.page h4 { padding-bottom: var(--gap-2); border-bottom: 1px solid var(--rule); }
.page h4 + h4, .page > * + h4 { margin-top: var(--gap-6); }

.hint { font-size: var(--t-small); color: var(--muted); margin: 0 0 var(--gap-3); max-width: 78ch; }
.hint:last-child { margin-bottom: 0; }
.page .hint { max-width: 100%; }

/*
  <b>`text-transform: none` because a value is never a label, and one screen proved it.</b>
  `label.field` uppercases — it is a two-word caption over a control — and `.val` is used *inside* one
  to put a sentence or a fact under the input. So the geodatabase publish screen, which echoes the
  service name into the URL it is about to create, rendered
  `/REST/SERVICES/HOSTED/PROJECT_INFORMATION/FEATURESERVER` in capitals: an operator could not tell
  whether they had typed `Project_Information`, `project_information` or `PROJECT_INFORMATION`, which
  is the one thing that echo exists to show. Design review 2026-08-19. Reset here rather than at the
  call site, because it is wrong everywhere the pattern is used and elsewhere it was only shouting.
*/
.val { font-family: var(--mono); font-size: var(--t-small); color: var(--muted);
       text-transform: none; letter-spacing: normal; }
.name { font-weight: 600; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.mono { font-family: var(--mono); }

code { font-family: var(--mono); font-size: .92em; background: var(--surface-2);
       padding: 1px 5px; border-radius: 3px; color: var(--muted); }

.bad-inline { color: var(--alert); font-size: var(--t-small); }

/*
  A fact worth noticing that is not a failure -- the geodatabase report's flattened
  elevation (D-107). `.bad-inline` would read as *this layer went wrong* and it did not:
  it published, and something about it was lost on the way in.
*/
.warn-inline { color: var(--warn); font-size: var(--t-small); }

.row { display: flex; gap: var(--gap-2); flex-wrap: wrap; align-items: flex-end;
       margin-bottom: var(--gap-3); }

/* ---------------------------------------------------------------------- tables */

table { width: 100%; border-collapse: collapse; font-size: var(--t-body); }

/*
  <b>The header is a label row, not a band.</b> Owner refinement pass: the table *"still feels too
  much like the old interface"*. A filled grey header with a heavy rule under it is the shape of a
  2008 grid; this is uppercase 11-pixel labels on a barely-tinted surface with one hairline, so the
  first strong thing on the screen is a service name rather than the word SERVICE.
*/
th {
  text-align: left;
  padding: 14px var(--gap-4) 12px;
  font-size: var(--t-label); font-weight: 600; letter-spacing: .07em; text-transform: uppercase;
  color: var(--faint);
  background: linear-gradient(to bottom, #fafbfd, var(--surface-2));
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}
th:first-child { border-top-left-radius: var(--round-lg); }
th:last-child { border-top-right-radius: var(--round-lg); }

/*
  <b>Taller rows and a lighter separator.</b> The brief asked for row height, vertical alignment and
  subtle row separators in one breath, and they are one thing: at 12 pixels of padding a full-weight
  rule is needed to tell rows apart, and at 18 the eye does it from the spacing, so the rule can drop
  to a tint. Density is unchanged in the sense that matters — the same nine services fit a screen.
*/
/*
  <b>Vertical padding is the row's presence and horizontal padding is the table's budget.</b> The
  10% scale-up pushed the actions column off a 1440-pixel viewport, and the fix is not narrower rows
  — it is 16 pixels of gutter instead of 22, six columns' worth of which is 72 pixels back. Row
  height, which is what the eye reads as generous, is untouched.
*/
td { padding: 15px var(--gap-4); border-bottom: 1px solid #eef1f6; vertical-align: middle; }
tr:last-child td { border-bottom: 0; }

/*
  <b>The preview and the name are one unit, so the gutter between them is small.</b> At the row's own
  padding they read as two columns that happen to be adjacent; at 14 the picture belongs to the name
  beside it, which is what the brief meant by the preview being integrated into the row.
*/
#services td:first-child { padding-right: 14px; }

/*
  <b>The name column takes the width, and its caption does not wrap.</b> `look_EarlyAlert · 3 layers,
  1 group` was breaking across two lines and making one row taller than the rest — a ragged table
  reads as a bug. Giving the column 42% and the caption `nowrap` fixes both, and the states beside it
  need only what their badges are wide.
*/
/*
  <b>The name column takes what is left rather than a fixed share.</b> A 42% width was holding the
  caption on one line and, after the scale-up, shoving the actions off the screen — `auto` plus a
  non-wrapping caption does the first job without the second. The states beside it need only what
  their badges are wide, which is what `width: 1%` on a nowrap cell asks for.
*/
#services .rowmeta { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#services td:nth-child(3), #services td:nth-child(4),
#services td:nth-child(5), #services td:nth-child(6) { width: 1%; white-space: nowrap; }

/*
  <b>The row is clickable and says so once, on hover.</b> Not a cursor change plus a colour plus an
  arrow: one signal. The click itself asks *what was clicked* rather than carrying a list of
  exceptions, which is how a Stop button once opened a service page (D-46).
*/
tr.pick { cursor: pointer; }
tr.pick td { transition: background .1s; }

/*
  <b>Hover tints the row and leaves a teal edge on it.</b> One signal was a background change, which
  on a near-white surface is almost nothing; the edge is what makes *this row is the one under the
  pointer* readable at a glance without darkening the row enough to fight the previews for attention.
*/
tr.pick:hover td { background: #f7fafc; }
tr.pick:hover td:first-child { box-shadow: inset 3px 0 0 var(--accent-hot); }

/*
  <b>`nowrap` was what made two tables wider than the window.</b> Measured 2026-08-19, after the closed
  menu sheets were fixed and the overflow did not go away: the actions cell held two or three buttons
  and a row menu on one unbreakable line, so Server's services list came to 1100 pixels and Studio's
  content list to 1107 against a 1024 viewport — and the page scrolled sideways, carrying the
  navigation column out with it.

  <b>Wrapping rather than a scroll container, because these tables carry row menus.</b> A
  `widetable` would clip an open dropdown — a scroll box clips absolutely positioned descendants — so
  the fix has to make the row narrower rather than hide it. Two buttons over two lines in a cell nobody
  reads as prose is a fair trade for a page that fits.

  <b>And `display: flex` on the `<td>` was the wrong way to do it</b> — that replaces `table-cell`, so
  the browser wraps the cell in an anonymous one and the content escapes the column instead of wrapping
  inside it. Measured: the buttons still stuck out, now at right=1091 with no ancestor growing to match,
  which is the signature of content leaving its cell. Removing `nowrap` is the whole fix: the buttons
  are inline-blocks with whitespace between them and wrap on their own.
*/
td.acts { text-align: right; }
td.acts > * + * { margin-left: var(--gap-1); }

.empty { color: var(--muted); font-size: var(--t-small); padding: var(--gap-5) var(--gap-4); }

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

button, .btn {
  font: 500 var(--t-small) var(--sans);
  padding: 7px 14px;
  border: 1px solid var(--control-edge);
  border-radius: var(--round);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}
button:hover { background: var(--surface-2); }
button:disabled { opacity: .45; cursor: default; }
button:disabled:hover { background: var(--surface); }

/*
  <b>The primary action carries the product's gradient, and it is the only gradient in the
  workspace.</b> Owner refinement pass: *"visually give it the stronger cyan → blue/violet treatment
  from the approved design."* One element having it is what makes it read as *the* action; two would
  make it a theme, which is on the list of things to avoid.

  <b>Its first stop is `--primary-from` and not `--cyan-deep` — [D-233](../../../docs/architecture-debt.md).</b>
  White on `--cyan-deep` is **2.57:1**, and the label is 15px at weight 600, which is ordinary text
  by WCAG's measure and not large. The gradient only reached 4.5:1 at **53% of its own width**, so
  *Sign in*, *Add a connection…*, *Go* and *Resume* all failed 1.4.3 across their leading half on
  every screen. `--primary-from` is the least ink on this same hue and saturation that clears it —
  the ramp walked at 1% steps, 29% lightness gives 4.49 and 28% gives 4.79 — which is how
  `--control-edge` was chosen for [D-215](../../../docs/architecture-debt.md) and for the same
  reason: a colour picked by eye is a colour that drifts back.

  <b>A darker stop rather than a text shadow</b>, which is what the web usually reaches for and is
  the worst of the three options the row named: it trades a ratio anybody can compute for one
  nobody can.
*/
button.primary {
  background: linear-gradient(96deg, var(--primary-from), #2f6fd0 62%, #5a49c4);
  border-color: transparent; color: #fff; font-weight: 600;
  padding: 11px 21px; font-size: var(--t-body); letter-spacing: -.005em;
  box-shadow: 0 1px 2px rgba(16, 24, 38, .12), 0 6px 16px -10px rgba(47, 111, 208, .7);
  transition: filter .12s, box-shadow .12s;
}
button.primary:hover {
  background: linear-gradient(96deg, var(--primary-from), #2f6fd0 62%, #5a49c4);

  /*
    <b>Hover darkens rather than brightens — D-233's second half, which nothing had named.</b>
    `brightness(1.06)` lightened the ground and left the text at white, because white is already
    clipped: the middle stop went 4.88 → 4.41 and the whole button failed AA *on hover* even
    before its first stop was corrected. So the state that says *you are about to press this* was
    the least readable one. Darkening filters the label too — white becomes #f0f0f0 — and the
    worst stop still measures 4.67:1. A filled button that deepens under the pointer is also the
    ordinary affordance; brightening one was never more than a habit.
  */
  filter: brightness(.94);
  box-shadow: 0 1px 2px rgba(16, 24, 38, .14), 0 10px 22px -12px rgba(47, 111, 208, .8);
}
button.primary .plus { font-weight: 400; margin-right: 7px; opacity: .9; }

button.tiny { padding: 5px 10px; font-size: 12px; }
button.ghost { border-color: transparent; background: none; color: var(--muted); }
button.ghost:hover { background: var(--surface-2); color: var(--ink); }

/*
  <b>Destructive actions do not compete.</b> The brief: *"Delete should NOT compete visually with
  normal actions."* So `danger` is not a red button — it is an ordinary one whose text turns red on
  hover, and it lives in the overflow menu where the row is concerned.
*/
button.danger { color: var(--alert); }
button.danger:hover { background: var(--alert-soft); border-color: var(--alert-line); }

/*
  The overflow menu: `<details>` rather than a script-driven popover, because the element already
  does the open/close state and the Escape key, and a menu is not worth a state machine.
*/
.menu { position: relative; display: inline-block; }
.menu > summary {
  list-style: none; cursor: pointer;
  padding: 5px 9px; border: 1px solid transparent; border-radius: var(--round);
  color: var(--muted); font-size: 15px; line-height: 1;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary:hover { background: var(--surface-2); color: var(--ink); }
.menu[open] > summary { background: var(--surface-2); border-color: var(--rule); }
/*
  <b>A closed menu takes no space, and it was taking 450 pixels.</b> Measured 2026-08-19 while chasing
  a horizontal overflow the design reviews found on four screens: a closed `<details>` still gave its
  `.sheet` a layout box — `display: block`, `offsetParent` non-null, 450px wide, its right edge at
  x=1083 in a 1024 viewport — so ten row menus were what pushed `document.body.scrollWidth` to 1100 on
  Server's services list and 1107 on Studio's content list. The tables themselves fit: 820 pixels
  inside an 820-pixel panel.

  <b>Which is why neither of those tables is wrapped in `widetable`.</b> Wrapping them would have
  hidden the symptom and clipped every open menu, since a scroll container clips absolutely positioned
  descendants. Members and the group's Content tab *are* wrapped, because those tables are genuinely
  wider than their panels and carry no row menu.
*/
.menu:not([open]) .sheet { display: none; }

.menu .sheet {
  position: absolute; right: 0; top: calc(100% + 4px); z-index: 800;
  min-width: 210px; padding: var(--gap-1);
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--round);
  box-shadow: var(--lift-2);
  text-align: left;
}
.menu .sheet button {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 8px 10px; border-radius: 3px; font-weight: 400;
}
.menu .sheet button:hover { background: var(--surface-2); }
.menu .sheet .note { padding: 6px 10px 8px; color: var(--faint); font-size: 11.5px; }

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

input, select, textarea {
  font: var(--t-small) var(--sans);
  padding: 7px 10px;
  border: 1px solid var(--control-edge);
  border-radius: var(--round);
  background: var(--surface);
  color: var(--ink);
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
textarea { font-family: var(--mono); width: 100%; line-height: 1.45; }
select { padding-right: 6px; }

label.field {
  display: flex; flex-direction: column; gap: 3px;
  font-size: var(--t-label); letter-spacing: .05em; text-transform: uppercase;
  color: var(--faint); font-weight: 600;
}

/* A number reads as a sentence, with the unit outside the box. */
/* Sign-in providers — ADR-088: a provider's way in on the sign-in panel, and a checkbox label in a form. */
#signinProviders { margin: 4px 0 14px; }
/* A secondary button, drawn as one: the form's own Sign in is the page's one primary action. */
#signinProviders a.provider {
  display: block; text-align: center; margin-bottom: 8px; text-decoration: none;
  padding: 10px 16px; border: 1px solid var(--control-edge); border-radius: var(--round);
  background: var(--surface); color: var(--ink); font-weight: 600;
}
#signinProviders a.provider:hover { background: var(--surface-2); }
#idpForm .check { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
#idpForm .row input[readonly] { flex: 1; font-family: var(--mono, monospace); }
#idpForm input[type=text], #idpForm input[type=url], #idpForm input[type=password] { width: 100%; max-width: 520px; }
#idpForm h5 { margin: 18px 0 4px; }
#idpForm .hint { margin: 2px 0 8px; }
.idpissuer code { overflow-wrap: anywhere; }
.idpissuer .hint { display: block; margin: 0; }
#idpGroups .valuesgrid input[type=text] { width: 100%; min-width: 14ch; }
#idpGroups .valuesgrid select { min-width: 16ch; }
#idpGroups .tablewrap { overflow-x: auto; max-width: 100%; }
#idpGroups .picker { min-width: 0; max-width: 100%; }
#idpGroups .row.left, #idpForm .row.left { justify-content: flex-start; }
#idpForm #idpIssuer, #idpForm #idpClient, #idpForm #idpUserBase, #idpForm #idpUserFilter { font-family: var(--mono, monospace); }
#view-signin .tablewrap { overflow-x: auto; max-width: 100%; }
#idpForm [aria-invalid="true"] { border-color: var(--alert); }
@media (max-width: 480px) {
  /* The reply URL and redirect URI were cut to their host at 390 px: the box takes the line, Copy the next. */
  .idpform .row { flex-wrap: wrap; }
  .idpform .row input[readonly] { flex: 1 1 100%; }
}
/* The .sr-only header of the provider list escaped the card at 390 px: its absolute box needs a positioned card. */
#view-signin .panel { position: relative; }

/* At a phone's width the list scrolls inside its card, as the Domains screen's does. */
@media (max-width: 760px) {
  #view-signin .panel { overflow-x: auto; }
  #view-signin .panel table { min-width: 560px; }
  #view-signin .panel table:has(td.empty) { min-width: 0; }
  #view-signin .panel table:has(td.empty) thead { display: none; }
}

/* Where a shared domain is used — ADR-087. A short list under its count, one field a line. */
.domainuses { margin: 4px 0 0; padding-left: 1.1em; font-size: var(--t-small); }
.domainuses li { padding: 2px 0; }
#valuesShared { max-width: 100%; }

/* At a phone's width the table scrolls inside its card rather than the page sideways — design review
   2026-09-24 measured 539 px of page in a 390 px window — and an empty one does not size itself by its head. */
@media (max-width: 760px) {
  #view-domains .panel { overflow-x: auto; }
  #view-domains .panel table { min-width: 460px; }
  #view-domains .panel table:has(td.empty) { min-width: 0; }
  #view-domains .panel table:has(td.empty) thead { display: none; }
  .valuesedit { overflow-x: auto; }
}

/* The map ground — ADR-086. The available services never move; the stack is the drawing order, top first. */
.groundpick { border: 0; padding: 0; margin: var(--gap-3) 0 0; max-width: 620px; }
.groundpick legend { padding: 0; margin-bottom: var(--gap-2); font-size: var(--t-body); }
.groundopt {
  display: grid; grid-template-columns: auto 1fr; column-gap: var(--gap-2);
  align-items: center; min-height: 44px;
}
.groundopt input { width: 18px; height: 18px; margin: 0; }
.groundopt .warn-inline { grid-column: 2; }
.groundstack { margin: var(--gap-2) 0 0; padding: 0; list-style: none; max-width: 620px; }
.groundstack li {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px var(--gap-2);
  min-height: 44px; border-bottom: 1px solid var(--rule);
}
.groundstack .groundname { font-weight: 600; }
.groundstack .warn-inline { flex-basis: 100%; order: 3; padding-bottom: 6px; }
.groundmove { margin-left: auto; display: inline-flex; gap: 4px; }
.groundmove button { min-width: 44px; min-height: 44px; }
@media (max-width: 600px) { #view-settings .row .tiny { min-height: 44px; } }

.setting { display: flex; align-items: center; gap: var(--gap-3); padding: 7px 0; max-width: 620px; }
.setting .q { flex: 1; font-size: var(--t-body); }
/* 116px suits a number with a unit beside it, which is what every other setting on this page is. A
   checkbox is not one: measured 2026-08-19, the delete lock came out 116 pixels wide with its own
   Locked label pushed to the far side of the box, so the row read as two unrelated things. */
.setting input:not([type=checkbox]), .setting select {
  width: 116px; text-align: right; font-family: var(--mono); }
.setting .u { width: 68px; color: var(--muted); font-size: var(--t-small); }

.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 2px var(--gap-5); max-width: 580px; }
.grid2 label { display: flex; align-items: center; gap: var(--gap-2); padding: 5px 0;
               font-size: var(--t-body); }
.grid2 label.off { color: var(--faint); }

.toolbar {
  display: flex; align-items: center; gap: var(--gap-3);
  padding: var(--gap-3) var(--gap-4);
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--round-lg);
  margin-bottom: var(--gap-4);
  box-shadow: var(--lift);
}
.toolbar input[type=search], .toolbar input[type=text] { flex: 0 1 360px; }
/*
  <b>Wrapping, because one of these is a sentence.</b> `nowrap` was right for the short values it was
  written for and made the page scroll sideways at 1024px on the Anonymous view, whose toolbar carries
  *"Three requests per layer: the service document, the layer document, and a count query."* Measured
  2026-08-19: `body.scrollWidth` 1054 against a 1024 viewport, and no other toolbar overflowed — so the
  rule was one long value away from being wrong rather than wrong in general. A toolbar is a row of
  facts, and a fact that needs two lines takes two.
*/
.toolbar .val { min-width: 0; }

/* Ten rows a page on Server's listings — owner 2026-08-18. Sits under the panel it pages,
   right-aligned, because that is where a reader's eye finishes a table. Nothing but the two
   arrows and which rows are on screen: a pager that offers page numbers on a list of a
   thousand offers a hundred links nobody clicks. */
.pager {
  display: flex; align-items: center; justify-content: flex-end; gap: var(--gap-3);
  margin: calc(-1 * var(--gap-2)) 0 var(--gap-4);
  padding: 0 var(--gap-2);
}
.pager .val { white-space: nowrap; }
.pager button { min-width: 2.2em; }

dl.facts { display: grid; grid-template-columns: auto 1fr; gap: 6px var(--gap-4); margin: 0;
           font-size: var(--t-small); }
dl.facts dt { color: var(--muted); }
/*
  <b>`overflow-wrap: anywhere`, not `word-break: break-all`.</b> `break-all` breaks every word
  at the edge whether it needs to or not, so a facts list 900 pixels wide split `JPEG` into
  `JPE` and `G`. `anywhere` breaks only a word that cannot fit — which is what these values
  need, because a path or a format list has no spaces to break at and a wkid has nothing to
  gain from being split at all.
*/
dl.facts dd { margin: 0; font-family: var(--mono); overflow-wrap: anywhere; }
dl.facts dd .mono { font-weight: 600; color: var(--ink); }
dl.facts dd .why { font-family: var(--sans); font-weight: 400; word-break: normal;
                   color: var(--muted); }

.group { border-top: 1px solid var(--rule); padding-top: var(--gap-4); margin-top: var(--gap-4); }
.group:first-child { border-top: 0; padding-top: 0; margin-top: 0; }

/* ---------------------------------------------------------------------- states */

/*
  <b>A badge is for a state, and for nothing else.</b> The brief: *"Do not turn every piece of
  metadata into a badge."* Two things on these screens are states — whether a service answers, and
  who may read it — so two families of badge exist and a count or a name never gets one. The dot
  carries the same information as the colour, so a state is not colour alone.
*/
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500;
  padding: 3px 9px 3px 8px;
  border-radius: 4px;
  border: 1px solid var(--rule);
  background: var(--surface-2);
  color: var(--muted);
  white-space: nowrap;
}
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor;
                flex: none; opacity: .8; }

/*
  <b>A sharing scope gets a glyph instead of a dot.</b> Owner 2026-08-17: *"sharing'deki icon mantığı
  güzel."* It is — *public* and *private* are the two facts on this screen somebody scans for, and a
  globe and a padlock are read before a word is. The dot is suppressed where an icon replaces it, so
  a badge never carries both; and the icon inherits `currentColor`, so the three scopes keep the
  hue that already told them apart.
*/
.pill.withicon::before { display: none; }
.pill svg { width: 12px; height: 12px; flex: none; }
.pill svg * { vector-effect: non-scaling-stroke; }

/* The same treatment on a button: a verb reads faster with its shape beside it. */
button .ico { font-size: 9px; margin-right: 7px; opacity: .75; vertical-align: 1px; }
button:hover .ico { opacity: 1; }

.pill.p-started, .pill.p-ok, .pill.p-usable, .pill.p-active {
  background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok);
}
.pill.p-stopped, .pill.p-degraded, .pill.p-disabled {
  background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn);
}
.pill.p-unusable, .pill.p-delete { background: var(--alert-soft); border-color: var(--alert-line);
                                   color: var(--alert); }

/* <b>Running and refusing, which is a state and not a status — D-232.</b> It sits *beside* the
   started pill rather than replacing it, so it is deliberately the quieter of the two: amber
   like `stopped`, because the consequence is the same to a client, and not red, because nothing
   has failed and the end is already scheduled. `--warn` carries the tone; the row that draws it
   says why in its title. */
.pill.p-refusing { background: var(--warn-soft); border-color: var(--warn-line);
                   color: var(--warn); margin-left: 6px; }

.pill.p-public { background: var(--blue-soft); border-color: var(--blue-line); color: var(--blue); }
.pill.p-organization { background: var(--violet-soft); border-color: #d9cdf3; color: var(--violet); }
.pill.p-private { background: var(--surface-3); border-color: var(--rule-strong);
                  color: var(--muted); }

/* Not a state: hosted/registered is a fact about where the data lives, so no dot and no colour. */
.pill.p-hosted, .pill.p-registered { background: var(--surface-2); color: var(--muted); }
.pill.p-hosted::before, .pill.p-registered::before { display: none; }

.swatch { display: inline-block; width: 9px; height: 9px; border-radius: 2px;
          margin-right: 7px; vertical-align: 0; border: 1px solid rgba(0, 0, 0, .3); }

/* ------------------------------------------------------- Server's own furniture */

/*
  <b>Folders, operational widgets, and the service list, in two columns.</b> The left column is
  *where am I and how is it* and the right is *what is here*. The brief asked for clear separation
  between navigation, operational information and service data, and this is that separation as a
  grid rather than as more borders.
*/
#serverGrid { display: grid; grid-template-columns: 282px minmax(0, 1fr); gap: var(--gap-5);
              align-items: start; }
/*
  <b>`minmax(0, 1fr)` and not `1fr`, which is the whole of why this page scrolled sideways.</b> A bare
  `1fr` is `minmax(auto, 1fr)`, and `auto` as a minimum means **min-content** — so the column could
  never be narrower than the widest thing in it, whatever that thing's own `overflow` said. The desktop
  rule above already had it right; the narrow one, which is the case that needs it, did not.

  Measured 2026-08-19 by hiding subtrees until `document.body.scrollWidth` fell: services list 1100
  against a 1024 window, and the last thing standing was the table's own header row setting a
  min-content the grid then honoured. Wrapping the table in `widetable` did nothing until this changed,
  because the panel was being sized to its content rather than sizing its content.
*/
@media (max-width: 1080px) { #serverGrid { grid-template-columns: minmax(0, 1fr); } }

#folderRail { background: var(--surface); border: 1px solid var(--rule);
              border-radius: var(--round-lg); overflow: hidden; box-shadow: var(--lift);
              margin-bottom: var(--gap-4); }

.rail-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--gap-3) var(--gap-3) var(--gap-3) var(--gap-4);
  border-bottom: 1px solid var(--rule);
  margin: 0;
}

.rail-item {
  display: flex; align-items: center; gap: 11px;
  padding: 11px var(--gap-4);
  border-left: 3px solid transparent;
  color: var(--ink); font-size: var(--t-body); text-decoration: none;
}
.rail-item:hover { background: var(--surface-2); text-decoration: none; }
.rail-item { transition: background .1s; }
.rail-item.on {
  background: linear-gradient(96deg, var(--accent-soft), #f2f8fd);
  border-left-color: var(--accent);
  font-weight: 600;
}
/*
  <b>The rail's icons, and the root is not a folder.</b> Owner 2026-08-17: *"folders ekranının
  simgeleri güzel."* The reference draws a house for the site root and a folder for the rest, which
  is a real distinction rather than decoration: the root is *where a service lives when nobody put
  it anywhere*, and giving it the same icon as `turkiye` would say it is one folder among four.
*/
.rail-item svg { width: 15px; height: 15px; flex: none; color: var(--faint); }
.rail-item.on svg { color: var(--accent); }
.rail-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-count { margin-left: auto; font: var(--t-small)/1 var(--mono); color: var(--faint); }
.rail-item.on .rail-count { color: var(--accent); }

/*
  <b>An operational widget, and it reports what the server actually measures.</b> The brief asked
  for Service health and Server resources. Health is real — the listing carries every service's
  status. Resources are real too, but not as the reference's CPU/Memory/Disk percentages: this
  server reports process CPU time, managed heap bytes and tile-cache size, and there is no disk
  quota to be a percentage of. So the widget shows the figures it has with their units, and there
  are no sparklines, because a sparkline claims a history nothing here keeps.
*/
.widget { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--round-lg);
          box-shadow: var(--lift); padding: var(--gap-4); margin-bottom: var(--gap-4); }
.widget > .label { margin-bottom: var(--gap-3); display: flex; align-items: center;
                   gap: var(--gap-2); }

/*
  <b>A paragraph of caveats became one glyph with a tooltip.</b> Owner polish pass: *"the long
  explanatory paragraph makes the widget feel like a debug panel. Move detailed explanations into an
  info tooltip or compact help affordance."* The text is unchanged and still one hover away — these
  numbers genuinely need it, since CPU is a ratio since start and the lines are the page's own
  samples — but a widget is not the place to read three sentences.
*/
.widget .why {
  margin-left: auto; width: 17px; height: 17px; flex: none;
  border-radius: 50%; border: 1px solid var(--control-edge);
  color: var(--faint); background: var(--surface);
  font: 600 11px/15px var(--sans); text-align: center; text-transform: none; letter-spacing: 0;
  cursor: help;
}
.widget .why:hover { color: var(--accent); border-color: var(--accent-line);
                     background: var(--accent-soft); }

/*
  <b>A ring, because a proportion reads faster as an arc than as a column of percentages.</b> Owner
  refinement pass: *"use a compact donut/ring visualization… this should look like an operational
  monitoring widget, not a statistics table."* One `conic-gradient` and a hole punched with a
  pseudo-element: no SVG, no library, no canvas, and the arc lengths are the real counts.
*/
.donutrow { display: flex; align-items: center; gap: var(--gap-4); margin-bottom: var(--gap-4); }

.donut {
  width: 84px; height: 84px; flex: none; border-radius: 50%;
  position: relative;
  background: conic-gradient(var(--ok-dot) 0turn var(--started),
                             var(--warn-dot) var(--started) 1turn);
}

/*
  <b>The reference draws a sliver of amber and red at nine-of-nine started, and this does not.</b>
  Its data is 9/0/0 too, so those arcs are decoration — a ring that shows a fault where there is
  none is the same lie as a sparkline with no history behind it, and the whole point of the arc is
  that its length is the count.
*/
.donut::after {
  content: ""; position: absolute; inset: 11px; border-radius: 50%;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--rule);
}
.donut.none { background: var(--surface-3); }

.tally b { display: block; font-size: 33px; font-weight: 650; letter-spacing: -.03em; line-height: 1; }
.tally span { font-size: var(--t-small); color: var(--muted); }

/*
  A legend row: a coloured dot, a name, the count in tabular figures, then the share. Four columns of
  different weight, which is the hierarchy the brief asked for at this scale — the count is the
  answer and the percentage is context.
*/
.legendrow { display: flex; align-items: center; gap: var(--gap-2); font-size: var(--t-small);
             padding: 5px 0; border-top: 1px solid var(--rule); }
.legendrow:first-child { border-top: 0; }
.legendrow .key { display: flex; align-items: center; gap: 8px; color: var(--muted); }
.legendrow .key::before { content: ""; width: 8px; height: 8px; border-radius: 50%;
                          background: var(--faint); }
.legendrow.ok .key::before { background: var(--ok-dot); }
.legendrow.warn .key::before { background: var(--warn-dot); }
.legendrow.alert .key::before { background: var(--alert-dot); }
.legendrow b { margin-left: auto; font: 600 var(--t-body)/1 var(--mono); color: var(--ink);
               font-variant-numeric: tabular-nums; }
.legendrow em { font-style: normal; color: var(--faint); width: 42px; text-align: right;
                font-variant-numeric: tabular-nums; }

/* A state this server cannot enter is dimmed rather than dropped — see drawHealthWidget. */
.legendrow.impossible { color: var(--faint); }
.legendrow.impossible b { color: var(--faint); font-weight: 400; }

/*
  <b>A resource row: name, sparkline, value.</b> The brief asked for the reference's sparklines and,
  in the same breath, for the real metrics to be preserved rather than replaced by invented
  percentages. Both are possible: the line is drawn from samples this page has actually taken since
  it opened, so it is a real history of a real measurement rather than a shape.
*/
/*
  <b>The value is the thing, and it now looks like it.</b> Owner polish pass: *"make the values and
  charts more visually prominent."* The number goes to 15 in the monospace face at 600, the chart to
  28 pixels tall, and the metric's name drops to the label size — so the row reads value first, shape
  second, name last, which is the order somebody scanning an operations widget wants.
*/
.meter { display: grid; grid-template-columns: 52px 1fr 82px; align-items: center;
         gap: var(--gap-3); padding: 10px 0; font-size: var(--t-small);
         border-top: 1px solid var(--rule); }
.meter:first-child { border-top: 0; }
.meter .k { color: var(--faint); font-size: var(--t-label); font-weight: 600;
            letter-spacing: .04em; text-transform: uppercase; }
.meter .t { font: 600 15px/1 var(--mono); color: var(--ink); text-align: right;
            font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.meter svg { display: block; width: 100%; height: 28px; overflow: visible; }
.meter svg path.line { fill: none; stroke: var(--violet); stroke-width: 1.75;
                       stroke-linejoin: round; stroke-linecap: round; }
.meter svg path.area { fill: rgba(106, 75, 192, .12); stroke: none; }
.meter.warm svg path.line { stroke: var(--cyan-deep); }
.meter.warm svg path.area { fill: rgba(15, 179, 186, .12); }
.meter .waiting { font-size: 11px; color: var(--faint); }

/*
  The picture box. It holds a map this server rendered — `/admin/thumbnail`, D-58 — drawn from
  every feature of the cover layer and its own symbology, cached, and shown at two sizes from one
  render. It was a canvas the browser filled from a sample of up to 800 features until 2026-09-02,
  which for a dense layer drew a fraction of it and read as *nearly empty*. The border is the
  lightest one available, because the picture is the thing and its frame is not.
*/
/*
  <b>The preview sits on the row rather than in a box on it.</b> Owner refinement pass: it *"should
  feel integrated into the row rather than placed inside a generic gray rectangle."* So the grey
  fill is gone — the canvas is white like the row, held by a hairline and a shadow small enough to
  read as a lift rather than a frame. The geometry keeps its own colours, which is the point: cyan,
  yellow and pink coming out of the data are most of this screen's personality (ADR-033 §5b).
*/
/*
  <b>Measured against the reference rather than chosen.</b> The polish pass asked for a 10% scale-up
  *and* for the populated table to be compared with the approved screenshot — and those pull opposite
  ways here, because the reference's own rows are tighter than what a proportional increase produces.
  Its preview is about 82×50 in a 77-pixel row; a straight +10% on my previous 112×78 gave 122×84 in a
  121-pixel row, which is half again as tall as the thing it was being compared to.

  <b>So the type keeps the increase and the picture does not.</b> 104×70 in a ~101-pixel row: the
  names, labels and folder panel are a tenth larger as asked, and the row is much closer to the
  reference's density. The two instructions cannot both be satisfied exactly, and this is the split —
  said out loud because the alternative is silently picking one.
*/
/*
  <b>`object-fit: cover` because the picture is one size and the slots are two — D-58.</b> The
  server renders 336x224 so a high-density screen has pixels to use and one cached picture serves
  both the 104x70 list slot and the 168x112 one on a service's own page. Without this an `<img>`
  would be stretched to whichever box it landed in; the ratios are close enough that `cover`
  crops nothing anybody can see and guarantees no distortion when a future slot is shaped
  differently.
*/
.thumb { display: block; width: 104px; height: 70px; border-radius: 7px;
         object-fit: cover;
         background: var(--surface);
         box-shadow: 0 0 0 1px var(--rule), 0 1px 2px rgba(16, 24, 38, .05); }
tr.pick:hover .thumb { box-shadow: 0 0 0 1px var(--rule-strong), 0 2px 6px -2px rgba(16, 24, 38, .12); }

/*
  <b>While the picture is being fetched, the slot says so — D-58, from the design review.</b> A
  thumbnail that has not arrived is a flat box, and a flat box is exactly what a genuinely sparse
  layer's picture looks like: on a cold cache an operator reads a table of them as *there is
  nothing in any of these*. The sweep clears `data-thumb` as it fills each one, so this selector
  is true only while the picture is outstanding and needs no second attribute to be maintained.

  <b>A slow shimmer rather than a spinner.</b> Forty spinners in a table is a fairground; what is
  needed is only that *waiting* and *empty* stop looking the same. `prefers-reduced-motion` gets
  the flat tint without the movement, which still separates the two.
*/
img.thumb[data-thumb]:not([src]) {
  background: linear-gradient(100deg,
              var(--surface) 30%, var(--rule) 50%, var(--surface) 70%) var(--surface);
  background-size: 300% 100%;
  animation: thumb-waiting 1.4s ease-in-out infinite;
}

@keyframes thumb-waiting {
  from { background-position: 150% 0; }
  to   { background-position: -50% 0; }
}

@media (prefers-reduced-motion: reduce) {
  img.thumb[data-thumb]:not([src]) { animation: none; background: var(--rule); }
}

.thumb.empty { position: relative; background: repeating-linear-gradient(
                 135deg, #f7f9fb 0 6px, #f1f4f8 6px 12px); }
.thumb.empty::after { content: "—"; position: absolute; inset: 0; display: grid;
                      place-items: center; color: var(--faint); font-size: 12px; }

/*
  <b>The reason in the box, when it is short enough to be read there — D-58.</b> The canvas this
  replaced wrote *stopped* where the picture would be; the hatch's em dash means *nothing to
  draw*, which is a different fact and must keep its own mark. D-58's own text calls hover text
  "a weak place to carry a correction", so a correction that fits goes in the box.
*/
.thumb.empty[data-note]::after { content: attr(data-note); font-size: 10.5px;
                                 letter-spacing: .02em; }

/*
  <b>The name is the row, and its metadata is a caption.</b> Four differences at once, which is what
  the brief meant by *not just font sizes*: 15.5 against 12.5, 650 against 400, ink against muted,
  and the counts in the monospace face because they are quantities. The separator is a hairline dot
  at rule strength — visible enough to group, too faint to read as punctuation.
*/
td .name { display: block; font-size: 17px; font-weight: 650; letter-spacing: -.012em;
           color: var(--ink); line-height: 1.25; }
.rowmeta { display: block; font-size: var(--t-small); color: var(--muted); font-weight: 400;
           margin-top: 3px; }
.rowmeta .sep { color: var(--rule-strong); margin: 0 7px; }
.rowmeta .count { font-family: var(--mono); }

/* --------------------------------------------------------------------- the map */

#mapPanel { display: none; margin-bottom: var(--gap-4); }
#mapPanel.on { display: block; }
#view { height: 460px; background: var(--surface-2); border-radius: var(--round-lg);
        overflow: hidden; }
#view:empty::after { content: "the map loads when a layer is shown"; display: grid;
                     place-items: center; height: 100%; color: var(--faint);
                     font-size: var(--t-small); }
#legend { display: flex; flex-wrap: wrap; gap: var(--gap-3); padding: var(--gap-3) 0 0;
          font-size: var(--t-small); color: var(--muted); }
#basemapBar { display: flex; align-items: center; gap: var(--gap-2); padding: 0 0 var(--gap-3); }
#basemapBar .lbl { font-size: var(--t-label); letter-spacing: .05em; text-transform: uppercase;
                   color: var(--faint); font-weight: 600; }
#basemapBar .val { color: var(--muted); }

/* ------------------------------------------------------------- the layer editor */

#editHead { display: flex; align-items: center; gap: var(--gap-4); margin-bottom: var(--gap-4); }
#editHead .crumbs { font-size: var(--t-body); color: var(--muted); }
#editHead .crumbs a { color: var(--muted); }
#editHead .crumbs a:hover { color: var(--accent); }
#editHead .crumbs b { color: var(--ink); font-weight: 600; }
#editHead .right { margin-left: auto; display: flex; align-items: center; gap: var(--gap-3); }

#editDirty { font-size: 11.5px; font-weight: 600; color: var(--warn);
             border: 1px solid var(--warn-line); background: var(--warn-soft);
             border-radius: 3px; padding: 2px 8px; }
#editCancel { font-size: var(--t-small); color: var(--muted); }

/*
  A floor under the frame, so moving between a long page and a short one does not resize the panel
  and shift the left column out from under the pointer.
*/
#edit { display: grid; grid-template-columns: 196px 1fr; align-items: stretch; min-height: 440px;
        background: var(--surface); border: 1px solid var(--rule); border-radius: var(--round-lg);
        box-shadow: var(--lift); overflow: hidden; }
#editNav { border-right: 1px solid var(--rule); background: var(--surface-2); padding: var(--gap-2) 0; }
#editNav a { display: block; padding: 9px var(--gap-4); border-left: 3px solid transparent;
             color: var(--ink); font-size: var(--t-body); }
#editNav a:hover { background: var(--surface-3); text-decoration: none; }
#editNav a[aria-current] { background: var(--surface); border-left-color: var(--accent);
                           font-weight: 500; }

/*
  <b>The way to the other surface's pages for the same layer.</b> ADR-034 §5c promises that each
  surface links to the other's, so the split never means a dead end — a publisher hunting for
  Sharing in Server would otherwise find four pages and no clue. Set apart above a rule, because it
  leaves this environment and a link that does that should not look like a sibling.
*/
#editNav a.crossing { margin-top: 10px; padding-top: var(--gap-3);
                      border-top: 1px solid var(--rule); color: var(--muted);
                      font-size: var(--t-small); }
#editNav a.crossing:hover { color: var(--ink); }
#editNav a.crossing .in { display: block; font-size: 11px; color: var(--faint); }

/*
  <b>Two pages, one rule.</b> A service with no layers has a settings page too, and giving it its
  own padding is how two screens that should look identical start not to.
*/
#editPages, #servicePages, #servicePagesBody { padding: var(--gap-5); min-width: 0; }

/*
  <b>A service's settings wear the layer editor's frame.</b> D-61 moved them here; giving them their
  own grid and nav styling would be D-46 again in the same file, so `#serviceEdit` is `#edit` and
  `#serviceNav` is `#editNav` by selector rather than by copy.
*/
#serviceEdit { display: grid; grid-template-columns: 196px minmax(0, 1fr); align-items: stretch;
               overflow: hidden; }
#serviceNav { border-right: 1px solid var(--rule); background: var(--surface-2);
              padding: var(--gap-2) 0; }
#serviceNav a { display: block; padding: 9px var(--gap-4); border-left: 3px solid transparent;
                color: var(--ink); font-size: var(--t-body); }
#serviceNav a:hover { background: var(--surface-3); text-decoration: none; }
#serviceNav a[aria-current] { background: var(--surface); border-left-color: var(--accent);
                              font-weight: 500; }

/*
  <b>One column, because a settings page is a column.</b> The layer editor's pages are not bounded —
  they hold an endpoint list and a wall of field pills that want the width — but this page is four
  numbers and three sentences, and letting those run the full width put the prose in one part of the
  panel and the boxes in another.
*/
#servicePages { max-width: 780px; }

/*
  <b>One screen at a time, and both of these were missing for one build of the redesign.</b> All
  eight views are in the document and the router moves a class; dropping `.view` while rewriting the
  stylesheet stacked every screen into one page — the same symptom D-44 produced when a
  Content-Security-Policy blocked the whole sheet, and the same misreading: it looks like a layout
  bug rather than a missing rule.
*/
.view { display: none; }
.view.on { display: block; }

.page { display: none; }
.page.on { display: block; }

/*
  The summary of what is in force, as a footer to the page rather than a fourth row of the list
  above it: everything in it can be changed by the boxes, which is the opposite of the fixed facts.
*/
#limNote { margin-top: var(--gap-4); padding-top: var(--gap-3); border-top: 1px solid var(--rule); }

/*
  <b>The note under the service list is a tinted strip, which is what the reference does with it.</b>
  It was a paragraph of grey text at the bottom of a page, i.e. the shape of something nobody reads.
  A tint, a teal edge and an ⓘ make it a place rather than a leftover — and the tint is 4% of the
  accent, because a full-strength callout for a sentence about previews would be shouting.
*/
#serviceNote {
  display: flex; gap: var(--gap-3); align-items: flex-start;
  margin: var(--gap-4) 0 0; padding: var(--gap-3) var(--gap-4);
  background: linear-gradient(96deg, var(--accent-soft), #f4f8fd 70%);
  border: 1px solid var(--accent-line); border-left-width: 3px;
  border-radius: var(--round);
  color: var(--muted); max-width: none;
}
#serviceNote::before { content: "ⓘ"; color: var(--accent); font-size: 14px; line-height: 1.3;
                       flex: none; }
#serviceNote:empty { display: none; }

/* -------------------------------------------------------------- dialog & drawer */

/*
  <b>One dialog, for a value that exists once.</b> The drawer below is for a form you fill and
  dismiss; this is for a secret you read and lose. `::backdrop` is the element's own, so there is no
  scrim div to keep in sync with anything.
*/
#issued { border: 1px solid var(--rule); border-radius: var(--round-lg); padding: var(--gap-5);
          max-width: 560px; background: var(--surface); color: var(--ink);
          box-shadow: 0 24px 60px -30px rgba(16, 24, 38, .5); }
#issued::backdrop { background: rgba(16, 24, 38, .42); }
/*
  <b>Not a section label, which is what the global `h4` rule had made it.</b> `h4, div.label, …` sets
  11.5px uppercase in `--faint`, so the one fact this dialog exists to get right — *who* the secret is
  for — rendered as chrome while the value nobody should mix up between people was 15px and bordered.
  Measured 2026-08-19. It is a heading here and is sized like one.
*/
#issued h4 { padding-bottom: var(--gap-2); border-bottom: 1px solid var(--rule);
              font-size: var(--t-head); text-transform: none; letter-spacing: 0;
              color: var(--ink); }
#issuedValue { width: 22ch; font-family: var(--mono); font-size: 15px; letter-spacing: .04em;
               text-align: center; }

/* ------------------------------------------------------------------ new item */
/*
  The New item dialog. Chrome first — a header, a scrolling body, a footer — because both of the
  owner's screenshots have exactly that and the body is the only part that scrolls: their *Create a
  feature layer* keeps Back and Next planted at the bottom with five options above them.
*/
#addItem { border: 1px solid var(--rule); border-radius: var(--round-lg); padding: 0;
           width: 680px; max-width: 94vw; background: var(--surface); color: var(--ink);
           box-shadow: 0 24px 60px -30px rgba(16, 24, 38, .5); }
#addItem::backdrop { background: rgba(16, 24, 38, .42); }

/* Only the screen that holds a table, and it goes back to 680px on the way out. */
#addItem.wide { width: 980px; }

/*
  <b>The table scrolls inside its own box, and the page never does.</b> A geodatabase's feature-class
  names run to forty characters and there are five columns; on a narrow window that is wider than the
  dialog, and a body that scrolls sideways takes the footer with it.
*/
.widetable { overflow-x: auto; }
.widetable table { min-width: 640px; }
/* A panel that scrolls keeps its own padding rather than losing it to the scroll box. */
.panel.widetable { padding-bottom: 0; }

/*
  The route table's two problems: a pattern that cannot wrap because it has no spaces, and a last
  column starved by the first taking everything. Breaking at the slashes gives the pattern somewhere
  to fold, and holding the governance clause on one line stops a 67-row table becoming 9,700px.
*/
/*
  Overview's two columns: the layer list, and the details beside it. `minmax(0, 1fr)` on the first for
  the reason the services grid needed it — a bare `1fr` cannot shrink below its content's min-content and
  a table's min-content is wide. Stacks below 1100px, where a 300px aside beside a five-column table is
  two things squeezed rather than two things side by side.
*/
.itemgrid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: var(--gap-5);
            align-items: start; }
@media (max-width: 1100px) { .itemgrid { grid-template-columns: minmax(0, 1fr); } }

/* The left column stacks: what the item is, then what is in it. */
.itemmain { display: flex; flex-direction: column; gap: var(--gap-5); min-width: 0; }

/* The address, readable and copyable in one row. The field is wide and the button does not shrink. */
.urlrow { display: flex; gap: var(--gap-2); align-items: center; }
.urlrow input { flex: 1 1 auto; min-width: 0; font-family: var(--mono); font-size: var(--t-small);
                background: var(--surface-2); }

/* A group layer's glyph, and the indent that says a layer sits under it. `parentLayerId` is in the
   service document and the table was ignoring it, so a nested layer read as a sibling. */
.rowicon { display: inline-flex; vertical-align: -3px; margin-right: 6px; color: var(--muted); }
.rowicon svg { width: 16px; height: 16px; }
td.name.nested { padding-left: 36px; }

/* The item's own header: the preview the content list already generates, beside what the item is. */
.itemhead { display: flex; gap: var(--gap-4); align-items: flex-start; }
.itemhead > div { min-width: 0; }
.itemhead b { font-size: var(--t-head); }
.itemhead .hint { margin: var(--gap-2) 0 0; }

/*
  <b>Three tiers, where there were two of the same.</b> The name is `--t-head`, the subtitle
  `--t-small` in `--muted`, and the dates were also `--t-small` in `--muted` — so the second and third
  lines read as one grey block. `--faint`'s own note says what it is for: *a second fact inside a cell
  whose first fact is already muted*. One size down and one step of grey, both from the scale.
*/
.footnote { margin-top: var(--gap-1); font-size: var(--t-label); color: var(--faint); }

/*
  <b>Bigger only here.</b> 104×70 is right beside a table row and reads as a mistake beside three lines
  of text with nothing else in the panel. 168×112 is the same ratio and the height of the text block it
  sits next to. Scoped, because the base rule also draws every thumbnail on the content list.
*/
#serviceHead .thumb { width: 168px; height: 112px; }

/* The section heading above the layer panel, where a header row used to be. Same treatment as the
   aside's own headings, so the page names a section one way. */
.itemmain > h4 { margin: 0; }
.urlrow button { flex: 0 0 auto; }

/*
  <b>A pill that is a button, and looks like the pill it replaced.</b> ADR-034 §5l makes the sharing
  scope on a content row the control that changes it — the reader was going to press it anyway. So the
  button carries no chrome of its own and the pill inside keeps its hue, its glyph and its border; what
  it gains is a cursor, a focus ring, and a real keyboard path, which a `span` had none of.
*/
.pillbtn { background: none; border: 0; padding: 0; margin: 0; font: inherit; cursor: pointer; }
.pillbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 999px; }

/* The groups an item is shared with, as removable chips. Small, because the list is usually short and a
   table of two rows is a table for the sake of one. */
.chips { display: flex; flex-wrap: wrap; gap: var(--gap-2); margin: var(--gap-2) 0 var(--gap-3); }
.chip { display: inline-flex; align-items: center; gap: var(--gap-1);
        padding: 3px 4px 3px 10px; border: 1px solid var(--rule-strong); border-radius: 999px;
        background: var(--surface-2); font-size: var(--t-small); }
.chip button { line-height: 1; padding: 2px 5px; }

/*
  <b>The geodatabase screens, and both rules are borrowed from tables above rather than invented.</b>

  `.gdbpick` is the selection table: five short, controlled-vocabulary columns beside one column of
  feature-class names fifty characters long. The short ones do not wrap, for the reason `#services`
  gives — `MultiPolygon Z` was breaking in two while `MultiPolygon` did not, and measured row heights
  came out 77.5, 77.5, 54.25, 77.5, 54.25 down the same table. A ragged table reads as a bug.

  `.gdbreport` is the per-layer outcome, where a refusal arrives as whatever the datastore said. A
  provider message can carry a sixty-character identifier with no space in it, which sets a hard
  minimum on its column and pushes the table past the dialog — measured at 1440×900, not only on a
  small screen. `overflow-wrap: anywhere` is what `.routetable` already does with a long path.
*/
.gdbpick td:not(:nth-child(2)), .gdbpick th:not(:nth-child(2)) { white-space: nowrap; }
.gdbreport td:last-child { overflow-wrap: anywhere; }
.gdbreport td:nth-child(2) { white-space: nowrap; }

.routetable td:first-child { overflow-wrap: anywhere; }
.routetable td:last-child { white-space: nowrap; }
.routetable table { min-width: 780px; }

/*
  <b>A row's primary cell carries the real control.</b> A clickable `<tr>` is a mouse affordance and
  nothing else: the roles table had no keyboard path to its own function at all — tabbing from *New
  role* skipped all five rows and landed in the editor below. So the name is a button where the row
  opens something on this page, and a link where the row is an address. It is styled as the text it
  replaced, because it is the row's name and not a second control beside it.
*/
.rowname { background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
           font-weight: inherit; text-align: left; cursor: pointer; }
.rowname:hover { text-decoration: underline; }
.rowname:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
/*
  <b>Only where the whole row is clickable.</b> This was written for `tr.pick`, where a coloured link
  would compete with the row's own affordance — and applied to every `td.name a`, including the service
  page's Overview, whose rows are *not* clickable. Measured 2026-08-19: the link `look_EarlyAlert_sites`
  and the plain label `Reports` rendered identically, `--ink` at weight 600 with no underline, so the
  only signal that one was a link and the other was not was an absence elsewhere in the row.
*/
tr.pick td.name a { color: inherit; text-decoration: none; }
tr.pick td.name a:hover { text-decoration: underline; }

/* And everywhere else a link in a name cell looks like one. */
td.name a { color: var(--accent); text-decoration: none; }
td.name a:hover { text-decoration: underline; }

.dialoghead { display: flex; align-items: center; justify-content: space-between;
              gap: var(--gap-4); padding: var(--gap-4) var(--gap-5);
              border-bottom: 1px solid var(--rule); }
.dialoghead h3 { font-size: var(--t-head); margin: 0; }

/* Capped rather than tall: the dialog must not grow past the viewport on a laptop, and their
   screen 2 is already 900 pixels of options. */
.dialogbody { padding: var(--gap-5); max-height: min(68vh, 620px); overflow-y: auto; }

.dialogfoot { display: flex; align-items: center; gap: var(--gap-2);
              padding: var(--gap-4) var(--gap-5); border-top: 1px solid var(--rule);
              background: var(--surface-2); border-radius: 0 0 var(--round-lg) var(--round-lg); }
.dialogfoot .fill { flex: 1; }
/* Empty on the first screen, where the only way out is the close button — theirs is the same. */
.dialogfoot:empty { display: none; }

/*
  The drop zone. Dashed, because a dashed edge is the one border convention that reads as *put
  something here* rather than *this is a box*, and it is the one their screen uses.
*/
.dropzone { border: 1px dashed var(--control-edge); border-radius: var(--round-lg);
            background: var(--surface-2); padding: var(--gap-6) var(--gap-5);
            text-align: center; display: grid; justify-items: center; gap: var(--gap-3); }
.dropzone p { margin: 0; font-size: var(--t-body); }
.dropzone .val { font-size: var(--t-small); color: var(--faint); }
.dropzone svg { width: 30px; height: 30px; color: var(--faint); }
/* Only while a file is over it: the affordance has to answer, or a drag ends in a guess. */
.dropzone.over { border-color: var(--accent); background: var(--accent-soft); }
.dropzone.over svg { color: var(--accent); }

/* Two columns, as theirs, collapsing rather than squeezing — a 48px icon beside two lines of prose
   has no room to give at 520px. */
.newtiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: var(--gap-4); margin-top: var(--gap-5); }
@media (max-width: 620px) { .newtiles { grid-template-columns: minmax(0, 1fr); } }

/*
  <b>One tile spans, and the rule retires itself.</b> The design review's answer to *does a single
  tile in a two-column grid read as room to grow* was no — it reads as a layout that half-rendered,
  because an equal-width void beside it carries no signal of intent. `:only-child` spans it today and
  stops applying the moment there is a second tile, so the two-column mechanism stays for when
  ADR-034 §5j's grid has something to put in it.
*/
.newtiles > .newtile:only-child { grid-column: 1 / -1; }

/*
  The *or* between the drop zone and the tiles. A rule with a word sitting in it, because the two
  halves of screen one are alternatives and nothing in the layout said so.
*/
.orbar { display: flex; align-items: center; gap: var(--gap-3); margin: var(--gap-5) 0 0;
         font-size: var(--t-small); color: var(--faint); }
.orbar::before, .orbar::after { content: ""; flex: 1; height: 1px; background: var(--rule); }

/* The heading takes focus on every screen change, so it must not draw a ring as though it were a
   control. The screen reader hears it; the eye should not be told to click it. */
#addItemTitle:focus { outline: none; }
#addItemTitle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.newtile { display: grid; grid-template-columns: 46px minmax(0, 1fr); gap: var(--gap-3);
           align-items: start; text-align: left; padding: var(--gap-3); cursor: pointer;
           border: 1px solid transparent; border-radius: var(--round); background: none;
           font: inherit; color: inherit; width: 100%; }
.newtile:hover { border-color: var(--rule-strong); background: var(--surface-2); }
.newtile .glyph { display: grid; place-items: center; width: 46px; height: 46px;
                  background: var(--surface-3); border-radius: var(--round); color: var(--muted); }
.newtile .glyph svg { width: 22px; height: 22px; }
.newtile b { display: block; font-weight: 600; }
.newtile span { display: block; margin-top: 2px; font-size: var(--t-small); color: var(--muted); }

/*
  The radio list. One accent, and it is the one this console already uses for a chosen thing — the
  selected option gets the accent edge and a tinted ground, which is how the rest of the page marks
  a current row.
*/
.pickrow { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: var(--gap-3);
           padding: var(--gap-4); border: 1px solid var(--rule); border-radius: var(--round);
           margin-top: var(--gap-2); cursor: pointer; }
.pickrow:first-of-type { margin-top: 0; }
.pickrow:hover { border-color: var(--rule-strong); }
.pickrow.on { border-color: var(--accent); background: var(--accent-soft); }
.pickrow input { margin: 3px 0 0; accent-color: var(--accent); }
.pickrow b { display: block; font-weight: 600; }

/*
  <b>16 pixels, because without this they were 573.</b> `icon()` returns a bare `svg` with no dimensions
  — every other caller sizes it (`.pill svg`, `.rail-item svg`, `.dropzone svg`, `.newtile .glyph svg`)
  and the Share dialog's scope rows did not, so a single padlock filled the dialog and pushed the rest of
  the screen out of view. Measured by the design review 2026-08-19: 573.47 × 573.47, three times. This is
  what the owner was looking at when they asked whether a designer had done it.
*/
.pickrow svg { width: 16px; height: 16px; flex: none; vertical-align: -2px; }

/*
  <b>The description has its own class, because a bare descendant selector matched both spans.</b>
  `.pickrow span` also hit the wrapper around the `b`, so the scope's label and the sentence explaining it
  rendered at the same 13.5px in the same muted grey — no hierarchy between *what you are choosing* and
  *what it means*. The same markup is New item's route picker, so both were affected.
*/
.pickrow .lede { display: block; margin-top: 3px; font-size: var(--t-small); color: var(--muted); }

/* Dialog text is `--ink`, not the browser's default black. Nothing was overriding the UA rule, so every
   dialog's base colour was a coincidence rather than the token. */
dialog { color: var(--ink); }
/* Named as a question, because their heading is one: *Select an option to create an empty feature
   layer.* A list of five nouns with no question above it is a list of five nouns. */
.picklede { margin: 0 0 var(--gap-4); font-size: var(--t-body); }

#drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: 620px; max-width: 96vw;
  background: var(--surface); border-left: 1px solid var(--rule); z-index: 1500;
  transform: translateX(100%); transition: transform .16s ease-out;
  display: flex; flex-direction: column;
  box-shadow: -18px 0 44px -30px rgba(16, 24, 38, .45);
}
#drawer.on { transform: none; }
#drawer header { height: auto; padding: var(--gap-4) var(--gap-5); align-items: flex-start;
                 gap: var(--gap-3); position: static; }
#drawer header h2 { margin: 0; font-size: var(--t-head); font-weight: 600; }
#drawerBody { overflow-y: auto; padding: var(--gap-5); flex: 1; }

/* ---------------------------------------------------------------------- toast */

#toast {
  position: fixed; right: var(--gap-5); bottom: var(--gap-5); max-width: 480px; z-index: 2000;
  background: var(--ink); color: #fff; font-size: var(--t-small);
  padding: 11px var(--gap-4); border-radius: var(--round);
  box-shadow: 0 18px 40px -22px rgba(16, 24, 38, .6);
  opacity: 0; transform: translateY(6px); pointer-events: none;
  transition: opacity .14s, transform .14s;
}
#toast.on { opacity: 1; transform: none; }
#toast.bad { background: var(--alert); }
#toast.good { background: var(--ok); }

/* ═══════════════════════════════════════════════════════════════════════ a phone, D-273
   <b>Below 760 pixels the sidebar is the rail, whatever the reader chose.</b> Measured 2026-09-16 at
   390 px: the 232-pixel navigation column stayed, which left 158 pixels of work, and a page heading's
   sentence broke after every word. The collapsed state already exists and is drawn — the toggle has
   chosen it since the console was written — so this is that state made the only one where a wider one
   does not fit, rather than a second narrow design nobody reviews.

   <b>The toggle goes with it.</b> A control whose two states look identical is a control that teaches a
   reader it does nothing; the rail is the only width here, so there is nothing to toggle.

   <b>Everything else is the same screens.</b> The panes that give up a column already do so at 1,100 and
   1,180; what is added here is the frame around them — the heading stacks, the toolbars wrap, the
   toast spans the width instead of sitting under the thumb's corner, and the work's side padding drops
   to the gutter these pages use everywhere else. */
@media (max-width: 760px) {
  #shell, #shell.tight { grid-template-columns: 56px 1fr; }

  /* The rail's own furniture, in the state `#shell.tight` already defines for it. */
  #brand h1, #railArt, #tabs .label, #tabs .count, #collapse { display: none; }
  #tabs a[data-tab] { justify-content: center; padding: 10px 0; gap: 0; }

  .wrap { padding: var(--gap-4) var(--gap-3); }

  /* The heading and its controls are a column: side by side, the sentence broke after every word. */
  .pagehead { flex-direction: column; gap: var(--gap-3); }
  .pagehead .right { align-self: stretch; }
  .pagehead .segmented { align-self: flex-start; }

  /* A pair of fields is one field; `.grid2` is the form idiom these screens share. */
  .grid2 { grid-template-columns: minmax(0, 1fr); max-width: none; }

  /* The layer and service editors put their navigation above their page rather than beside it. */
  /* <b>Both editors, as the comment says.</b> Only the layer's selectors were here until the design review
     of 2026-09-24 found the service's navigation still a column at 390 pixels, its page 200 wide and the
     page size box cut off at the edge. `#serviceEdit` is `#edit` by selector everywhere else in this file. */
  #edit, #serviceEdit { grid-template-columns: minmax(0, 1fr); }
  #editNav, #serviceNav { border-right: 0; border-bottom: 1px solid var(--rule); display: flex; overflow-x: auto; }
  #editNav a, #serviceNav a { border-left: 0; border-bottom: 3px solid transparent; white-space: nowrap; }
  #editNav a[aria-current], #serviceNav a[aria-current] { border-left-color: transparent; border-bottom-color: var(--accent); }
  #editPages, #servicePages, #servicePagesBody { padding: var(--gap-4) var(--gap-3); }

  /* Full width, because 480 pixels of toast beside a 16-pixel margin is the width of the screen. */
  #toast { left: var(--gap-3); right: var(--gap-3); bottom: var(--gap-3); max-width: none; }

  /* <b>The top bar wraps, and Sign out comes back on screen.</b> Both phone reviews found it first and
     measured the same thing: `header` is a 58-pixel row that does not wrap, its content is 432 pixels
     wide in a 334-pixel column, so `#who` and Sign out sat off the right edge — and the page scrolled
     98 pixels sideways to reach them, which drags the rail off with it. The health line takes its own
     row and truncates rather than wrapping to five lines over the breadcrumb. */
  header { height: auto; min-height: 52px; flex-wrap: wrap; row-gap: 4px;
           padding: var(--gap-2) var(--gap-3); gap: var(--gap-3); }
  #healthLine { flex: 1 1 100%; order: 3; min-width: 0;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #who { max-width: 40vw; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #who b { display: inline; margin-right: 6px; }

  /* A toolbar is a row of filters; at this width it is a column of them, and the search box takes the
     line it needs. On Groups the New button drew over the row count — `no gro⟨button⟩`. */
  .toolbar { flex-wrap: wrap; }
  .toolbar input[type=search], .toolbar input[type=text] { flex: 1 1 100%; min-width: 0; }

  /* <b>A tab strip scrolls rather than losing its last tab.</b> `.segmented` is `flex: none` and
     `nowrap`, so `#serviceTabs` at 405 pixels in a 302-pixel column cut Settings off entirely, and
     `#visModes` cut Tiles — the tile preview could not be chosen at all. */
  .segmented { flex: 0 1 auto; min-width: 0; max-width: 100%;
               overflow-x: auto; scrollbar-width: none; }
  .pagehead .segmented { align-self: stretch; }

  /* Two tables are not in `.widetable`, so they pushed their own card open instead of scrolling in it. */
  #view-roles .panel, #view-sources .panel { overflow-x: auto; }
  #view-roles .panel table, #view-sources .panel table { min-width: 460px; }

  /* The basemap bar: Use and None were off the edge, so a basemap could be neither set nor cleared. */
  #basemapBar { flex-wrap: wrap; }
  #basemapBar .val, #basemapInput { flex: 1 1 100%; min-width: 0; }
  #basemapSet, #basemapClear { flex: 1 1 auto; }

  /* <b>Forty pixels for anything a finger presses.</b> Every row control was 20 to 28 pixels tall. This
     is the touch target only — the type size and the spacing between rows are unchanged. */
  .tiny, .pillbtn, .menu > summary, #surfaces a, .segmented a, .segmented button, #editNav a {
    min-height: 40px; display: inline-flex; align-items: center; justify-content: center;
  }
  #editNav a { justify-content: flex-start; flex: 0 0 auto; }

  /* The symbology strip keeps the page's gutter, and its state sentence is read rather than cut. */
  .symstrip { padding: 0 var(--gap-3); }
  .symstrip .symstate { white-space: normal; }

  /* <b>A row menu is a sheet at the bottom.</b> It was absolutely positioned and 450 pixels wide inside
     a 310-pixel scroller: opening it looked like nothing happening, with the sheet at x 461 and Delete
     past the edge — the clipping `.menu .sheet`'s own comment predicted. The script stops positioning
     it below this width, so these rules are the whole of it: across the bottom, where a thumb is, and
     scrolling inside itself when it is tall. */
  .menu[open] .sheet { position: fixed; left: var(--gap-3); right: var(--gap-3);
                       top: auto; bottom: var(--gap-3); width: auto; min-width: 0;
                       max-height: 60vh; overflow-y: auto; }
  .menu .sheet button, .menu .sheet a { min-height: 40px; display: flex; align-items: center; }

  /* The note under a destructive item is a sentence; held on one line it read *…never remove‸*. */
  .menu .sheet .note { white-space: normal; }

  /* <b>The actions stay where the thumb is.</b> Reaching the row menu on the services table cost a
     550-pixel sideways swipe, and the column it is in is the one every row is opened from. The shadow
     is the edge that says the other columns pass under this one rather than ending there. */
  .widetable table td:last-child, .widetable table th:last-child {
    position: sticky; right: 0; z-index: 1; background: var(--surface);
    box-shadow: -10px 0 10px -10px rgba(16, 24, 38, .25);
  }

  /* <b>The open menu's own cell comes forward.</b> A sticky cell makes a stacking context, so the
     fixed sheet is trapped inside the cell it is in and every later row's pinned cell paints over it —
     measured with `elementFromPoint`: a *Map* button of the row below sat on top of *The layer
     document*. Raising the sheet does nothing; raising its cell is what works. */
  .widetable table td:last-child:has(.menu[open]) { z-index: 3; }

  /* The row under the pointer keeps its highlight in the pinned column too, rather than showing a
     white patch where the later rule wins. */
  tr.pick:hover td:last-child { background: #f7fafc; }

  /* <b>Not on the logs, where the last column is the resource and pinning it is a column of dashes.</b>
     There is no row menu there, so there is nothing to keep in reach. */
  #view-logs .widetable table td:last-child, #view-logs .widetable table th:last-child {
    position: static; box-shadow: none;
  }

  /* Two more scope strips that are not `.segmented`, and two more tables that are not in `.widetable`. */
  .tabstrip { overflow-x: auto; scrollbar-width: none; }
  .tabstrip a { flex: 0 0 auto; white-space: nowrap; min-height: 40px;
                display: inline-flex; align-items: center; }
  #view-groups .panel, #view-anonymous .panel { overflow-x: auto; }
  #view-groups .panel table, #view-anonymous .panel table { min-width: 460px; }

  /* <b>An empty state is a sentence, not a table.</b> Dropping the minimum width was not enough: the
     global `th { white-space: nowrap }` still sized the table from a header with nothing under it, so
     the sentence was clipped at 504 pixels in a 308-pixel panel. With no rows to label, the header is
     not a header. */
  #view-groups .panel table:has(td.empty),
  #view-anonymous .panel table:has(td.empty) { min-width: 0; }
  #view-groups .panel table:has(td.empty) thead,
  #view-anonymous .panel table:has(td.empty) thead { display: none; }

  /* The controls the first pass missed, measured at 20 to 34 pixels. */
  /* `td a:has(.pill)` is the visibility link, which is a pill inside a link and was 20 pixels tall; the
     first version of this rule named a class the markup does not have. */
  .rowname, #signout, #logMore, td a:has(.pill) { min-height: 40px;
    display: inline-flex; align-items: center; }
}

/* ------------------------------------------------------------- the two card forms */

/*
  <b>Two panels, one set of rules.</b> The forced password change is the same shape as signing in —
  a narrow centred card of labelled fields — and its first version got none of this because the
  selectors named `#signin`. It rendered as labels and boxes side by side, spilling out of the card
  (D-46 again, in the same file as instance 8).
*/
#signin, #mustchange { max-width: 380px; margin: 72px auto; }
#signin .panel, #mustchange .panel { padding: var(--gap-5); }
#signin h2, #mustchange h2 { margin: 0 0 var(--gap-2); font-size: 19px; font-weight: 600; }
#signin label, #mustchange label { display: block; font-size: var(--t-small); color: var(--muted);
                                   margin: var(--gap-3) 0 var(--gap-1); }
#signin input, #signin button,
#mustchange input, #mustchange button { width: 100%; }
#signin button, #mustchange button { margin-top: var(--gap-4); }

/*
  <b>The conversion's losses, and a derived document beside them.</b> ADR-033 accepted a
  lossy projection onto the ArcGIS face and the mitigation is that it says what it lost —
  so this reads as a list of statements, not as an error state. Amber rather than red: none
  of it is broken, and a page that cried wolf about a dashed line would train its reader to
  stop looking at the one that matters.
*/
.losses {
  margin: 6px 0 0; padding: 0 0 0 18px;
  color: var(--ink); font: var(--t-small)/1.55 var(--sans);
}
.losses li { margin: 4px 0; }
.losses li::marker { color: var(--warn-dot); }

#symLoss {
  margin-top: 14px; padding: 12px 14px;
  border: 1px solid var(--rule); border-left: 3px solid var(--warn-dot);
  border-radius: var(--round); background: var(--surface);
}
#symLoss h4 { margin: 0; }

/* The colours an ArcGIS client will use, D-99. Above the document rather than beside it:
   the colour is the answer and the JSON is the evidence, and a reader who wants the quad
   has it on the chip's title. */
.swatches {
  display: flex; flex-wrap: wrap; gap: var(--gap-2);
  margin: 8px 0 0;
}
.swatches .swatch {
  /* <b>Stated, because a rule elsewhere states it.</b> `.swatch` at the top of this file is a
     9x9 status dot and it is the only rule that declares a width — so without these two lines
     it wins the cascade here and every legend chip collapses to nine pixels while its patch and
     its label spill out over the next one. Measured on `ci_many`'s three-class legend. */
  width: auto; height: auto;

  display: inline-flex; align-items: center; gap: var(--gap-2);
  padding: 4px 10px 4px 4px;
  border: 1px solid var(--rule); border-radius: var(--round);
  background: var(--surface); color: var(--ink);
  font: var(--t-small)/1.4 var(--sans);
}
/* A checkerboard behind the patch, so a half-transparent fill reads as half transparent
   rather than as a paler colour. Drawn in the panel's own tones, so it is a texture rather
   than a second palette. */
.swatches .patch {
  width: 18px; height: 18px; flex: 0 0 18px; border-radius: 3px;
  border: 2px solid transparent;
  background-color: var(--surface);
  background-image:
    linear-gradient(45deg, var(--rule) 25%, transparent 25%, transparent 75%, var(--rule) 75%),
    linear-gradient(45deg, var(--rule) 25%, transparent 25%, transparent 75%, var(--rule) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
  box-shadow: inset 0 0 0 1px var(--rule);
  position: relative; overflow: hidden;
}
.swatches .patch .fill { position: absolute; inset: 0; }

/* D-99: the shape a client will see. A line is a band across the chip rather than a filled
   square, and a point is a dot in the middle of one — both drawn from the same colour, so a
   reader compares colours across shapes without having to discount the shape. */
.swatches .patch.stroke .fill { inset: 6px 0; border-radius: 1px; }
.swatches .patch.mark .fill { inset: 4px; border-radius: 50%; }

/* A document this server derived, shown as output because that is what it is. */
pre.doc {
  margin: 8px 0 0; padding: 12px 14px; overflow: auto; max-height: 320px;
  border: 1px solid var(--rule); border-radius: var(--round);
  background: var(--surface); color: var(--ink);
  font: 12px/1.5 var(--mono); white-space: pre;
}

/* The roles editor — ADR-035 §4f, two sections of grouped ticks. Dense on purpose: eighteen
   privileges today and more later, and an operator comparing two roles reads them as a list. */
.rolesection { margin-top: var(--gap-4); }
.rolesection h4 { display: flex; align-items: center; gap: var(--gap-3); margin-bottom: var(--gap-2); }
.rolesection h4 .val { font-weight: 400; }
.rolesection h4 button { margin-left: auto; }
.rolegroup { border: 1px solid var(--rule); border-radius: var(--round-lg);
             padding: var(--gap-3); margin-bottom: var(--gap-3); background: var(--surface); }
.rolegrouphead { display: flex; align-items: center; gap: var(--gap-3);
                 font-weight: 600; font-size: var(--t-small); margin-bottom: 6px; }
.roleprivilege { display: flex; align-items: baseline; gap: var(--gap-3); padding: 3px 0; }
.roleprivilege .mono { min-width: 22em; }
.roleprivilege input[disabled] + .privilegetext .mono { color: var(--muted); }

/* <b>Capped by a user type: muted, not disabled.</b> The 1c handoff asks for these greyed with a
   note, and the prototype greys the *text* and leaves the control — which is the honest reading.
   The role does carry the privilege and it does work, for a member whose type has no ceiling in
   the way; ADR-018 §1's intersection is what the note names. A disabled box would say *you cannot
   grant this*, and that is a different and false sentence. */
.roleprivilege.capped .mono { color: var(--muted); }

/* Amber rather than the neutral chip colour, because this one is a caution and the two beside it
   — `needs` and `includes` — are facts about the privilege rather than about who it reaches. */
.cappednote { color: var(--warn); }

/* <b>The name and its notes on one line, the sentence under them.</b> D-100: what a privilege
   does is a sentence, and a sentence in a row of chips is a chip nobody reads. The wrapper
   exists so the checkbox stays aligned with the name rather than with the middle of a
   two-line block. */
.privilegetext { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--gap-3); }
.privilegewhat { flex-basis: 100%; color: var(--muted); font-size: 0.92em; max-width: 62em; }

/* ---------------------------------------------------------------- the selected row

   <b>Written 2026-08-18 after a design review, and it repairs two screens.</b> `loadGroups` and
   `loadRoles` both write `class="pick on"` onto the row they have open and there was no rule for
   it — so clicking a row changed nothing visible, and the panel it opens is below the fold on a
   1366-wide window. The pair mirrors the hover rules above: a soft fill for the row, and a bar on
   the leading edge, which is the one mark that survives a colour-blind reader and a printout. */
tr.pick.on td { background: var(--accent-soft); }
tr.pick.on td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* ---------------------------------------------------------------- an editor's own head

   The markup on the Groups and Roles editors was written expecting a flex row with its last child
   pushed right — two sibling divs, actions and an exit — and used `class="head"`, which does not
   exist. So the buttons stacked on two left-aligned lines. Named `editorhead` rather than `head`
   because `pagehead` already owns the top of a screen and two classes one letter apart is how the
   next person picks the wrong one. */
.editorhead { display: flex; align-items: center; gap: var(--gap-3); margin-bottom: var(--gap-3); }
.editorhead > :last-child { margin-left: auto; }
.editorhead h3 { margin: 0; }

/* ---------------------------------------------------------------- a group's page

   <b>The `#groupEditor` rules that were here are gone with the panel.</b> They existed to give a
   panel's non-table children the 16px gutter that `th`/`td` carry themselves — a problem a page does
   not have, because a page's children are already inside the workspace padding.

   Two small additions, both needed by the tabs and both general:
     `.faint` for a second fact inside a cell that already carries one — *by whom* under a date, where
       `.val` is already spent on the date itself.
     `tr.after` for the line between the members who can manage a group and the members who cannot. It
       is the reference's *Group role* filter drawn as a boundary, which is what nine rows want
       instead of a filter. */

/* The second fact in a cell whose first fact is already `.val` — *by whom* under a date. `--faint`
   rather than `--muted` so the two are distinguishable, which is only defensible now that `--faint`
   can be read. */
.faint { color: var(--faint); font-size: var(--t-small); }

/* <b>A labelled band where a hairline was.</b> `tr.after` drew `border-top: 1px solid var(--rule)`
   directly against the bottom rule every `td` already carries — two near-identical hairlines stacked,
   which is invisible. And it depended on a sort that put the managers last, so it landed on the first
   row of page two and nowhere near the boundary. A row that says what it separates survives a page
   break, which a rule does not. */
tr.groupband > td {
  padding: 10px var(--gap-4) 5px; border-top: 1px solid var(--rule);
  color: var(--muted); font-size: var(--t-small); background: var(--surface-2);
}

/* Members beside services: the relation is the subject, so the two halves are read together.
   Collapses to one column where there is not room for two three-column tables. */
/* ═══════════════════ a list to choose from and a panel to work in, 2026-09-05 ═══════════════════

  <b>The 1c handoff asks two screens for the same shape and they are the same class.</b> Roles is
  *list left (click selects), privilege editor right*; Data sources is *table left, register form
  right with the test result inline*. Measured off the prototype: `minmax(0,1fr) 380px` for the
  sources and `minmax(0,1fr) minmax(360px,440px)` for the roles, gap 22 — which is `--gap-5`
  exactly, so the token was already the design's.

  <b>One class rather than two, because the difference between 380 and 360–440 is not a decision.</b>
  Writing both would be two numbers to keep in step and nothing to keep them there — D-46's subject.

  <b>`align-items: start`, or the short side stretches.</b> A grid item fills its row by default, so
  a 200-pixel form beside a twenty-row table grows a 600-pixel white box under itself.
*/
/* <b>The connection test's answer, in the form that asked it.</b> The 1c handoff: *table left,
   register form right with the test result inline (green/amber/red box)*. It was a pill and a
   sentence in a row, which reads as a status line rather than as the answer to the button above
   it — and the three tones are already in the palette, so the design's colours and this page's
   are the same colours rather than two sets that look alike.

   <b>Three tones for four outcomes, and the grouping is the honest one.</b> `Usable` is green;
   `CannotConnect` is red, because nothing about the data is known and the next move is a network
   or a password; `InsufficientPrivilege` and `UnusableGeometry` are amber, because the connection
   worked and what was found is the problem. Red for *cannot reach* and amber for *reached and it
   is wrong* is the distinction an administrator acts on. */
.testresult {
  margin-top: var(--gap-4); padding: 11px 13px; border-radius: 5px;
  border: 1px solid var(--rule); background: var(--surface-2);
  font-size: 13.5px; line-height: 1.5; color: var(--ink);
}

.testresult > b { display: block; margin-bottom: 2px; }
.testresult.ok { background: var(--ok-soft); border-color: var(--ok-line); }
.testresult.ok > b { color: var(--ok); }
.testresult.warn { background: var(--warn-soft); border-color: var(--warn-line); }
.testresult.warn > b { color: var(--warn); }
.testresult.alert { background: var(--alert-soft); border-color: var(--alert-line); }
.testresult.alert > b { color: var(--alert); }

.pickpair {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(360px, 420px);
  gap: var(--gap-5); align-items: start;
}

.pickpair > .panel { margin-bottom: 0; }

/* Same trap as `.grouppair`, and the same fix before it is measured: `1fr` cannot shrink below
   min-content, and the left side of both of these holds a table. */
@media (max-width: 1180px) { .pickpair { grid-template-columns: minmax(0, 1fr); } }

.grouppair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap-5);
             margin-top: var(--gap-4); }
/* Same trap, same fix, before it is measured rather than after: `1fr` cannot shrink below
   min-content, and this pair holds a table. */
@media (max-width: 1100px) { .grouppair { grid-template-columns: minmax(0, 1fr); } }

/* The facts, which were two paragraphs of grey hint carrying one word each. A label/value pair is
   the reference's Details block reduced to what we have: the standing and what the group confers. */
.facts2 { display: grid; grid-template-columns: auto 1fr; gap: 4px var(--gap-4);
          font-size: var(--t-small); margin: var(--gap-3) 0; }
.facts2 dt { color: var(--muted); }
.facts2 dd { margin: 0; font-weight: 600; }
.facts2 dd .val { font-weight: 400; }

/* ---------------------------------------------------------------- the tab strip

   <b>The one new component a group's page needed</b>, and the budget was one. Everything else on
   that page has an owner already: `.crumbs` and `.pagehead` for the frame, `.facts2` for Overview,
   `.setting` for Settings, `.picker` for both pickers, `pagerFor` for paging.

   <b>Not the surface strip and not the editor's left column.</b> `#surfaces` fills its current pill
   because filled means *environment*, and these four are not four environments. `#editNav` was the
   near miss: free CSS and consistent with both editors, but it spends 196px of the left edge on the
   tab that most needs width, and that left edge is exactly where Content's filter rail goes when
   maps arrive. Two nested left rails is the shape to avoid.

   An underline rather than a box, because the strip sits under a title and a boxed tab would compete
   with the panel below it for the same edge. */

.tabstrip {
  display: flex; gap: var(--gap-5); align-items: stretch;
  border-bottom: 1px solid var(--rule); margin-bottom: var(--gap-5);
}
.tabstrip a {
  padding: 8px 1px 9px; color: var(--muted); text-decoration: none;
  font-size: var(--t-body); border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tabstrip a:hover { color: var(--ink); }
.tabstrip a[aria-current="page"] {
  color: var(--ink); font-weight: 600; border-bottom-color: var(--accent);
}
.tabstrip .count { color: var(--faint); font-family: var(--mono); font-size: var(--t-small); }
.tabstrip a[aria-current="page"] .count { color: var(--muted); }

/* Overview's reachability sentence: a comparison of two numbers, so prose with the numbers set
   strongly — not a donut, which is the Services screen's idiom for a distribution of states.

   <b>Named `.reach`, because `.tally` is already taken and taking it broke the sentence.</b> The
   Server overview's big-number stat is `.tally`, and its rule at line 986 is `.tally b { display:
   block; font-size: 33px }`. Wearing that class turned every strong number *and the word* `group`
   into a 33-pixel block on its own line: six lines of poster where a sentence was written. Found in
   the design review of 2026-08-18, which is also the first time anybody read the sentence. */
.reach { margin: 0 0 var(--gap-5); font-size: 15.5px; max-width: 88ch; }
.reach b { font-weight: 650; }
.reach .off { color: var(--warn); }

/* A sentence does not fit a 116px right-aligned number field.

   <b>258px did not fit either, and the house rule depended on it.</b> This console puts the
   consequence in the option's own text, so a box that clips its options defeats the rule that
   justifies them: measured, half the labels — including two of the three defaults — rendered as
   `By invitation — an owner or m▾`. Fixed on both sides. The box is wider, and the labels are shorter
   with the consequence moved into the row's hint where it can wrap. */
.setting.wide { max-width: 760px; }
/* A checkbox is not a text box: the 300px meant for a field left about 150 pixels of dead space
   between the delete lock and its own Locked/Not locked label, so the row read as two unrelated
   things rather than one control and its state. Measured 2026-08-19. */
/* <b>`max-width` beside the `width`, because a fixed width in a flexible row is a clip waiting
   for a narrow container.</b> [D-216](../../../docs/architecture-debt.md) was raised when a design
   review measured this rule overflowing a 359-pixel panel by 51 px on the symbology page, hidden
   with no scrollbar. That instance is gone: ADR-053 rebuilt that screen and its *Change:* row is
   no longer a `.setting.wide`. Checked 2026-09-05 rather than assumed — the only `.setting.wide`
   rows left are the ten in `#groupSettings`, which sits in a full-width view under this rule's own
   `max-width: 760px`, so nothing overflows today.
   <b>The declaration goes in anyway and the row is closed as prevention, not repair.</b> A control
   that cannot give up width is one narrow column away from being clipped again, and the next
   person to put a wide row in a rail will not know that. `min-width: 0` is what lets it actually
   shrink inside a flex row, which `max-width` alone does not. */
.setting.wide select, .setting.wide input:not([type=checkbox]) {
  width: 300px; max-width: 100%; min-width: 0; text-align: left;
}
.setting.wide label { display: inline-flex; align-items: center; gap: var(--gap-2); }
.setting.wide .u { width: auto; flex: 0 0 auto; }
.setting.wide .q { max-width: 400px; }

/* ---------------------------------------------------------------- the add page

   <b>The rail slot the group Content tab reserved, spent on the one thing we have data for.</b>
   `#serverGrid`'s shape, and the same media query: below 1100px the rail stacks above the table rather
   than squeezing the rows that carry a 104px picture. */

.addgrid { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: var(--gap-5); }
@media (max-width: 1100px) { .addgrid { grid-template-columns: minmax(0, 1fr); } }

/* The Publish screen's contents tree — ADR-057, and the shape is ArcGIS Pro's contents pane
   because that is the reference the owner named for this screen.

   <b>A tree rather than a list, since 2026-09-06.</b> It was a flat row per layer, and the
   design study it was built from had a root, disclosure triangles, a tick and a symbol under
   each layer. The owner put the two side by side and asked whether they were the same screen;
   they were not. */

.pubroot { display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border-bottom: 1px solid var(--rule); font-weight: 600; }
.pubroot .pubglyph { color: var(--accent); }

/* <b>The map's reference, and it is a label rather than a second name.</b> The per-layer badge
   is styled as `.pubrow .pubsr`, and `.pubroot` is a different ancestor — so the root's badge
   matched no rule at all and inherited the row's own 600 weight, arriving at the same visual
   weight as the word *Map* beside it. A found review, 2026-09-07: the badge exists to be read at
   a glance as secondary, and unstyled it read as a second heading. */
.pubroot .pubsr { margin-left: auto; flex: 0 0 auto;
  font: var(--t-tiny)/1 var(--mono); font-weight: 400; color: var(--faint); }
.pubmapname { flex: 1; }

.pubmenu { border: 0; background: none; cursor: pointer; color: var(--faint);
  padding: 2px 6px; border-radius: var(--round); font-size: 15px; line-height: 1; }
.pubmenu:hover { background: var(--surface-2); color: var(--ink); }

/* The triangle is a button, so it is reachable without a mouse and announces its own state. */
.pubtwist { border: 0; background: none; cursor: pointer; color: var(--faint);
  padding: 0; width: 14px; font-size: 10px; line-height: 1; flex: none; text-align: left; }
.pubtwist:hover { color: var(--ink); }

.pubtick { margin: 0; flex: none; }
.pubglyph { color: var(--faint); flex: none; }

/* <b>The reprojection mark, and only where there is one.</b> An arrow beside every layer is
   decoration; beside the three whose tables are stored in something else it is the answer to
   *what is this service doing to my data*. */
.pubwarp { color: var(--accent); font-size: 13px; flex: none; cursor: help; }

/* The symbol, under the layer it belongs to — the row the disclosure triangle reveals. */
.pubsym { display: flex; align-items: center; gap: 9px; width: 100%;
  border: 0; background: none; cursor: pointer; text-align: left;
  padding: 3px 10px 5px 44px; color: var(--muted); font-size: var(--t-small); }
.pubsym:hover { background: var(--surface-2); color: var(--ink); }
.pubnode.grouped .pubsym { padding-left: 62px; }

.pubswatch { flex: none; width: 22px; height: 14px; }
.pubswatch.fill { background: var(--sw-fill); border: 1.5px solid var(--sw-line);
  border-radius: 2px; }
.pubswatch.line { height: 0; border-top: 2.5px solid var(--sw-line); }
.pubswatch.dot { width: 12px; height: 12px; border-radius: 50%;
  background: var(--sw-fill); border: 1.5px solid var(--sw-line); }

/* <b>The picture, and it is the server's.</b> `POST /admin/publish/preview` draws the
   composition out of the databases with the renderer the served faces use, so this is the
   service before the service exists. The chequerboard is what says the ground is transparent
   rather than white — a preview on white would promise a background the service does not
   serve. */
/* <b>The chequerboard went with the empty state it explained.</b> There is a map under the
   drawing now, so transparency is not a thing to signal — it is how the composition gets to sit
   on a ground. */
.pubcanvas { position: relative; min-height: 300px; flex: 1 1 auto; overflow: hidden;
  padding: 0; background: var(--surface-2); }

#pubMap { position: absolute; inset: 0; }
#pubMap .ol-viewport { border-radius: 0; }

/* <b>Over the map and out of the way of it.</b> The picture must not swallow the drag, the
   scroll or the pointer — panning the ground under it is the whole reason the map is here. */
.pubcanvas > img { position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 1; }

.pubmapsays { position: absolute; left: 10px; bottom: 10px; z-index: 2;
  max-width: 60%; padding: 6px 10px; border-radius: var(--round);
  background: rgba(255, 255, 255, .92); border: 1px solid var(--rule);
  font-size: var(--t-small); color: var(--muted); box-shadow: var(--lift); }

/* Where a table dropped on the map will land. */
.pubcanvas.dropping { outline: 2px dashed var(--accent); outline-offset: -6px; }

/* <b>The layer's number, where the layer is.</b> `FeatureServer/2` is how a client addresses
   it, and a group and its children interleave — so counting rows is not reading the index. It
   was a list under the preview repeating every name until 2026-09-06, which is one fact in two
   places and the owner said so. Muted and monospaced: it is an address, not a heading, and it
   must not compete with the name beside it. */
.pubindex { flex: none; width: 15px; text-align: right;
  font: var(--t-tiny)/1 var(--mono); color: var(--faint); }

/* The reference is set on the map's properties, so its box is a dialog field like any other —
   monospaced, because what goes in it is a code or a definition and both are read character by
   character. It was a rule for a toolbar control until 2026-09-07; ADR-057 §5n moved it. */
#mapprops #pubSrid { font-family: var(--mono); }

/* What the server says about the code that was typed: its name, or that it does not know it.
   The width cap was for the toolbar, where it sat between two buttons; in the dialog the
   sentence has the width of the dialog and capping it would wrap a name for no reason. */
.pbsridsays { font-size: var(--t-small); color: var(--muted); }
.pbsridsays.bad { color: var(--alert); }

/* A value the dialog reports rather than asks for. */
/* <b>And a refusal reads as one here too.</b> `.pbsridsays.bad` colours the same sentence on the
   map's properties; without this rule the Publish dialog showed *this server cannot project to…*
   in unalarmed black, at the one moment it matters most. The same fact in two places, styled in
   one — which is the shape of the defect this pair of controls was built to end. */
.pbreadonly.bad { color: var(--alert); }

.pbreadonly { display: block; padding: 7px 0 0; font: 500 var(--t-body)/1 var(--mono);
  color: var(--ink); }

/* The contents tree's context menu. */
#pubmenu {
  position: fixed; z-index: 60; min-width: 178px; padding: 4px;
  background: var(--surface); border: 1px solid var(--rule-strong);
  border-radius: var(--round); box-shadow: var(--lift-2);
}
#pubmenu button {
  display: block; width: 100%; text-align: left; border: 0; background: none;
  padding: 7px 10px; border-radius: 3px; cursor: pointer;
  font-size: var(--t-small); color: var(--ink);
}
/* Hover and focus are the same state here, because the menu takes focus now: the arrow keys
   move through it and a highlight nobody can see is the same as no highlight. */
#pubmenu button:hover,
#pubmenu button:focus-visible { background: var(--surface-2); outline: none; }
#pubmenu button:disabled { color: var(--faint); cursor: default; background: none; }
#pubmenu hr { border: 0; border-top: 1px solid var(--rule); margin: 4px 2px; }

/* The Publish dialog's *What it can do* block — ADR-057 §5g.

   A fieldset because it is one question with several answers, and a legend because a screen
   reader announcing "checkbox, Create" without it has told the reader nothing about what is
   being created. `.tick` next door is a table-cell width and is deliberately not reused: 34px
   on a label would crush the sentence beside the box. */

.pbcaps { border: 1px solid var(--rule); border-radius: var(--round); padding: 10px 12px 4px;
  margin: 4px 0 0; }
.pbcaps legend { padding: 0 6px; font: 600 var(--t-label)/1 var(--sans); color: var(--faint);
  text-transform: uppercase; letter-spacing: .04em; }
.pbcaps .row { gap: var(--gap-3); }
.pbcaps .hint { margin: 6px 0 8px; }

.pbtick { display: flex; align-items: baseline; gap: 8px; flex: 0 1 auto; cursor: pointer; }
.pbtick input { margin: 0; flex: 0 0 auto; }
.pbtick input:disabled { cursor: default; }

/* A disabled box that is ticked on purpose reads as *settled*, not as broken — the same
   problem `tr.already` below solves in a table. */
.pbtick input:disabled + span { color: var(--muted); cursor: default; }

.tick { width: 34px; }
.thumbcell { width: 120px; padding-right: 0; }

/* Already in the group: ticked, disabled, and legible as *not an option* rather than as broken. */
tr.already { color: var(--muted); }
tr.already .name { font-weight: 500; }

/* The select-all, whose label carries its own count — the whole point of not being a bare tick. */
.pickall { display: flex; align-items: center; gap: var(--gap-2); font-size: var(--t-small);
           color: var(--muted); white-space: nowrap; }

/*
  <b>Stuck to the bottom, because the count of what you picked was below the fold.</b> Measured
  2026-08-19 on the screen the owner asked for by name: with the picker open, `Add N items` sat at
  y=1481 in a 900-pixel viewport and y=1663 in a 720-pixel one. Everything above the fold said how
  many items *exist*; nothing said how many were *chosen*, and the select-all's only feedback was a
  native indeterminate dash. A confirmation you have to scroll to find is a confirmation that does not
  confirm.
*/
.addfoot { position: sticky; bottom: 0; z-index: 2;
           margin-top: var(--gap-5); gap: var(--gap-3); align-items: baseline; flex-wrap: wrap;
           padding: var(--gap-3) 0; background: var(--surface);
           border-top: 1px solid var(--rule); }
.addfoot .hint { flex: 1 1 340px; min-width: 240px; }

/* ---------------------------------------------------------------- the inline picker

   <b>Its own rules, because it was wearing `.setting`.</b> That class is a 620px flex row built for
   a number and a unit: a 116px right-aligned monospace field. A listbox of `folder/service` paths
   inherited the width and the right-alignment, where the meaning is on the left and so is the
   truncation, and the checkbox inherited 116px as well. This is D-46's failure in reverse — not a
   shape rebuilt because the old one was hard to find, but an old shape borrowed for a job it was
   not built for. */
.picker { border: 1px solid var(--rule); border-radius: var(--round-lg);
          padding: var(--gap-3) var(--gap-4); background: var(--surface-2);
          margin: var(--gap-3) 0; max-width: 620px; }
.picker label { display: block; font-size: var(--t-small); color: var(--muted);
                margin-bottom: 4px; }
.picker input[type=search] { width: 100%; margin-bottom: var(--gap-2); }
.picker select { width: 100%; text-align: left; font-family: var(--mono);
                 font-size: var(--t-small); }
.picker .row { display: flex; align-items: center; gap: var(--gap-3);
               margin-top: var(--gap-3); }
.picker .row label { display: flex; align-items: center; gap: 6px; margin: 0; }
.picker .row input[type=checkbox] { width: auto; }
.picker .row > :last-child { margin-left: auto; }

/*
  <b>The Logs screen. ADR-045.</b> Three rules, and no new concepts: the rows are the table
  every other screen uses, the states are this file's own pills, and the only things missing
  were a column that must not wrap and the detail row behind a click.
*/
.nowrap { white-space: nowrap; }

/* A log row is clickable, and it has to look it — a row that reveals something on click and
   looks identical to one that does not is a control nobody finds. */
tr.logrow { cursor: pointer; }
tr.logrow:hover td { background: var(--surface-2); }

/* The detail is monospace and scrolls in its own box rather than widening the table, which
   is the same reasoning `widetable` carries: a long value must not push the page sideways. */
tr.logdetail td { background: var(--surface-2); padding: 0; }
tr.logdetail pre { margin: 0; padding: var(--gap-3) var(--gap-4);
                   font: 12px/1.5 var(--mono); color: var(--muted);
                   max-height: 22rem; overflow: auto; }

/*
  <b>The Logs screen's toolbars wrap, and the reason is that there are six controls in one.</b>
  A `.toolbar` is `display: flex` with no `flex-wrap`, which is right for the two or three
  controls every other screen puts in one — at 1024px this screen's filter row pushed *Only
  failures* off the edge and dragged the whole page, sidebar included, sideways. Scoped to this
  screen rather than changed globally: the other toolbars were measured and fit.
*/
#view-logs .toolbar { flex-wrap: wrap; row-gap: var(--gap-2); }
#view-logs .toolbar input[type=search],
#view-logs .toolbar input[type=text] { flex: 1 1 12rem; }

/* A label beside its control, so the pair wraps as one thing rather than splitting. */
#view-logs #logOwn { display: inline-flex; align-items: center; gap: var(--gap-2); }
#view-logs #logOwn label { color: var(--muted); font-size: var(--t-small); }
#view-logs .toolbar label { white-space: nowrap; }

/*
  <b>The table scrolls in its box below the width its columns need.</b> `widetable` already
  does this; what it lacked was a minimum for these five columns, so the cells squeezed until
  Resource clipped instead of the box scrolling.
*/
#view-logs .widetable table { min-width: 60rem; }

/*
  <b>Which screen a cold load paints, before `console.js` has run.</b> `session.js` — loaded
  from the head without `defer`, in its own file because the console's CSP allows no inline
  script — sets `data-session` from `sessionStorage`, and these two rules are what make that
  decision visible on the first paint instead of after 454 KB of JavaScript.

  <b>Measured with `console.js` delayed 800 ms and a valid token in storage:</b> the sign-in
  panel was never visible, in any of about 1,380 samples per run across three runs. Before
  this, the same test kept it on screen for the whole delay.

  <b>`!important` on exactly these two, and it is load-bearing rather than lazy.</b>
  `console.js` sets `style.display` inline on both elements once it knows the real answer, and
  an inline style beats a class — so without this the first paint would ignore the attribute.
  These rules are the *initial* state only: `start()` overrides them deliberately, in both
  directions, the moment `whoami` has answered.
*/
html[data-session="held"] #signin { display: none !important; }
html[data-session="held"] #app { display: block !important; }

/*
  <b>The symbology editor: the picture beside the controls.</b> Owner request 2026-09-03 — the
  page had a JSON box, a derived document and swatches, which is an editor for somebody who
  already knows MapLibre. What was missing is controls that name what they do and a picture of
  the result, and the picture is the half that decides: a swatch says what colour, not what the
  map looks like at the size anybody sees it.

  <b>Rebuilt to the owner's handoff, direction 1c, 2026-09-04.</b> The 2026-09-03 answer put the
  picture beside a single column of controls and left the rest of the page — the document, the
  ArcGIS projection, the losses — stacked underneath, so the screen was still a scroll. The
  complaint it was answering was *çok karmaşık*, and a shorter scroll is not an answer to that.

  <b>Three columns, and each one is a question.</b> *What kind of drawing is this* on the left,
  *what does it look like* in the middle, *what is in it* on the right. Nothing was removed: the
  document, the losses and the derived `drawingInfo` are tabs of the third column rather than
  three more sections below the fold, and the preview stops being a 336-pixel thumbnail beside a
  form and becomes the thing the page is mostly made of.
*/

/* <b>The editor takes the window, so the shell's page padding is not its padding.</b> Every
   other screen is a document in a 44-pixel margin; this one is an instrument panel, and a
   column of controls that stops 44 pixels short of the edge reads as a panel that failed to
   load. `#app` keeps its padding for every other view. */
#app.symfull { padding: 0; max-width: none; }
#app.symfull #editHead { display: none; }
#app.symfull #edit {
  display: block; min-height: 0; border: 0; border-radius: 0; box-shadow: none;
  background: transparent;
}
#app.symfull #editNav { display: none; }
#app.symfull #editPages { padding: 0; }

/* <b>Full height minus the top bar, so the three columns end where the window does.</b> The
   preview is the middle column and it fills what is left; without a height on the page it would
   be as tall as whichever column had the most in it.
   <b>The 1,180 is the three columns' floor, not the screen's.</b> Below it the columns stack
   rather than being cut off by `#edit`'s rounded corners — see *below the editor's floor*,
   D-221, further down, which lifts this minimum. */
#app.symfull #page-symbology.on {
  display: flex; flex-direction: column;
  height: calc(100vh - 58px);
  min-width: 1180px;
}

/* ------------------------------------------------------------------ the title strip
   <b>One line that says where you are, what you are looking at and what you can do with
   it.</b> The three used to be three rows — a breadcrumb, a tab strip, a row of buttons at the
   bottom of the form — and Store was the one below the fold. */

.symstrip {
  display: flex; align-items: center; gap: var(--gap-2);
  padding: 0 var(--gap-5); min-height: 44px; flex: none;
  background: var(--surface); border-bottom: 1px solid var(--rule);
  font-size: var(--t-small); color: var(--muted);
  position: sticky; top: 0; z-index: 5;
}

.symstrip .crumbs { white-space: nowrap; }
.symstrip .crumbs a { color: var(--muted); }
.symstrip .crumbs a:hover { color: var(--accent); }
.symstrip .crumbs b { color: var(--ink); font-weight: 600; }

.symstrip .symdo { margin-left: auto; display: flex; align-items: center; gap: var(--gap-2); }

/* <b>The state line is the caption the preview used to carry, and it belongs beside Store.</b>
   *Edited — this is what Store would keep* is a sentence about the button next to it. */
.symstate { font-size: 12.5px; color: var(--muted); white-space: nowrap; }
.symstate.symstate-edited { color: var(--warn); font-weight: 600; }

/*
  <b>A segmented control, and this file had three hand-rolled ones before it had this.</b> The
  layer switcher, the item tabs and the inspector's own tabs are the same shape: a row of links
  in a hairline box where the current one is a raised chip. Written once (D-46).
*/
.segmented {
  display: inline-flex; gap: 2px; padding: 3px; flex: none;
  border: 1px solid var(--rule); border-radius: 6px; background: var(--paper);
}
.segmented a {
  padding: 3px 10px; border-radius: 3px; color: var(--muted);
  font-size: 12.5px; white-space: nowrap; text-decoration: none;
}
.segmented a:hover { color: var(--ink); text-decoration: none; }
.segmented a[aria-current] {
  background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: var(--lift);
}

/* The item page's own tabs, which are a level up from the layer switcher beside them: they move
   between subjects, so they are the taller of the two. */
.segmented.tabs { background: var(--surface); margin-left: auto; }
.segmented.tabs a { padding: 5px 12px; font-size: var(--t-small); }

/* ------------------------------------------------------------------------ the banner
   The server's own refusal, said where the thing it refused was typed. Nothing else on the page
   changes: a document that was not stored is still the document on the screen. */
.symbanner {
  display: flex; gap: var(--gap-3); align-items: flex-start; flex: none;
  padding: 10px var(--gap-5);
  background: var(--alert-soft); border-bottom: 1px solid var(--alert-line);
  color: var(--alert); font-size: var(--t-small); line-height: 1.45;
}
.symbanner .mono { font-family: var(--mono); }
.symbanner button { margin-left: auto; flex: none; }

/* --------------------------------------------------------------------- the empty state
   <b>A generated appearance is an answer, so it is stated as one.</b> §5b: no document is
   stored, the colour is deterministic, and both faces report version 0. That used to be a
   sentence above a form already full of controls for a document nobody had written. */
.symempty { flex: 1; display: grid; place-items: center; padding: 40px; }
.symempty > div {
  max-width: 560px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: var(--gap-3);
}
.symempty b { font-size: 19px; letter-spacing: -.01em; }
.symempty p { margin: 0; color: var(--muted); }
.symempty .row { margin: 0; }
.symempty .sw {
  width: 120px; height: 80px; border-radius: 6px; display: grid; place-items: center;
  box-shadow: 0 0 0 1px var(--rule);
  background: repeating-linear-gradient(135deg, #eef2f6 0 6px, #e6ebf1 6px 12px);
}

/* ------------------------------------------------------------------- the three columns */

.symcols {
  flex: 1; min-height: 0;
  display: grid; grid-template-columns: 264px minmax(0, 1fr) 336px;
}

/* <b>Renderer.</b> A column of sections, each one a question with a heading over it. */
.symrail {
  background: var(--surface); border-right: 1px solid var(--rule);
  padding: var(--gap-4) var(--gap-3); overflow: auto;
  display: flex; flex-direction: column; gap: var(--gap-4);
}

.symrail h5, .syminsp h5 {
  margin: 0 0 var(--gap-2);
  font: 600 11.5px/1.3 var(--sans); letter-spacing: .06em; text-transform: uppercase;
  color: var(--faint);
}

/* The rail is 264px wide with 24px of padding, so a label at one edge and a control at the
   other is not an arrangement it can afford. Label over control. */
.symrail .setting { display: block; padding: 0 0 var(--gap-2); max-width: none; }
.symrail .setting .q { display: block; margin-bottom: 3px; font-size: 12.5px; color: var(--muted); }
.symrail .setting select,
.symrail .setting input:not([type=checkbox]):not([type=color]) { width: 100%; }
.symrail .hint { margin: 2px 0 var(--gap-2); font-size: 12px; }

/* ------------------------------------------------------- the renderer, as three cards
   <b>A `<select>` was three answers behind a click.</b> The three are the biggest decision on
   the page and they are visually different from each other — one colour, a colour per value, a
   ramp — so they are shown rather than named. Radios, so a keyboard moves between them with the
   arrow keys and a screen reader is told it is one choice of three. */

.symkinds { display: flex; flex-direction: column; gap: 6px; margin-bottom: var(--gap-3); }

.symkind {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border: 1px solid var(--rule); border-radius: 6px;
  background: var(--surface); color: var(--muted); font-size: var(--t-small);
  cursor: pointer;
}
.symkind:hover { border-color: var(--rule-strong); }
/* <b>The card is the radio's containing block — `position: relative`, added 2026-09-11.</b>
   Without it the hidden radio was positioned against a box outside every scroller on the page,
   so it did not move when the rail scrolled: focusing a card in a scrolled rail left the card, and
   its focus ring, out of sight. Measured at 1,300 below the floor (D-221): card at 300 pixels,
   rail box from 549. It was latent in the wide window only because the rail there overflows by
   fourteen pixels. */
.symkind { position: relative; }
.symkind input { position: absolute; opacity: 0; width: 0; height: 0; }
.symkind:has(input:checked) {
  border: 2px solid var(--accent); padding: 8px 10px;
  background: var(--accent-soft); color: var(--ink); font-weight: 600;
}
.symkind:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

.symkind .ramp { display: flex; gap: 2px; flex: none; }
.symkind .ramp i { width: 7px; height: 5px; border-radius: 2px; display: block; }

/* ------------------------------------------------------------------------ the picture
   <b>The preview is the column, not a thumbnail in it.</b> It is what the reader came to look
   at; the hatched ground behind it is the same one the empty thumbnails on every list use, so a
   picture that has not arrived reads as *not yet* rather than as *nothing*. */

.sympreview {
  position: relative; overflow: hidden; min-width: 0;
  background: repeating-linear-gradient(135deg, #eef2f6 0 6px, #e6ebf1 6px 12px);
}

.sympreview img,
.sympreview .thumb.empty {
  display: block; position: absolute; inset: 0;
  width: 100%; height: 100%; border-radius: 0; background: none; box-shadow: none;
  object-fit: contain;
}

.sympreview .thumb.empty { display: grid; place-items: center; }

/* The overlays. All four are cards on the picture rather than chrome around it, because the
   picture is what has to keep its size when the window changes. */
.symcap {
  position: absolute; left: 14px; top: 12px; max-width: calc(100% - 28px);
  padding: 5px 9px; border-radius: 4px; background: rgba(255, 255, 255, .9);
  color: var(--muted); font: 11.5px/1.4 var(--mono);
  box-shadow: 0 0 0 1px var(--rule);
}

.symground { position: absolute; right: 14px; top: 12px; }

.symlegend {
  position: absolute; left: 14px; bottom: 14px; max-width: calc(100% - 28px);
  padding: 8px 11px; border-radius: var(--round); background: rgba(255, 255, 255, .94);
  font-size: 12px; display: flex; flex-direction: column; gap: 4px;
  box-shadow: 0 0 0 1px var(--rule);
  max-height: 40%; overflow: auto;
}
.symlegend span { display: flex; align-items: center; gap: var(--gap-2); }
.symlegend em { font-style: normal; color: var(--faint); }

/* ---------------------------------------------------------------------- the inspector
   Three tabs over one pane, and the third carries a count when the conversion lost something.
   The losses were a block under the editor that a reader reached by scrolling past everything
   else; a badge on a tab is the same fact where it can be seen without looking for it. */

.syminsp {
  background: var(--surface); border-left: 1px solid var(--rule);
  display: flex; flex-direction: column; overflow: hidden; min-width: 0;
}

.insptabs { display: flex; border-bottom: 1px solid var(--rule); flex: none; }
.insptabs a {
  flex: 1; text-align: center; padding: 11px 0; color: var(--muted);
  font-size: var(--t-small); text-decoration: none;
  display: flex; justify-content: center; align-items: center; gap: 6px;
}
.insptabs a:hover { color: var(--ink); text-decoration: none; }
.insptabs a[aria-current] {
  color: var(--accent); font-weight: 600; box-shadow: inset 0 -2px 0 var(--accent);
}
.insptabs .badge {
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
  background: var(--warn); color: #fff;
  font: 700 10.5px/16px var(--sans); text-align: center;
}

.insppane { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
.insppane[hidden] { display: none; }

.insppane .pad { padding: var(--gap-3) var(--gap-3) 0; }
.insppane .setting { display: flex; padding: 5px 0; max-width: none; }
.insppane .hint { margin: 2px 0 var(--gap-2); font-size: 12px; }

/* <b>The symbol stack is a footer under the list, not a page that replaces it.</b> 2026-09-04's
   answer to *a panel titled after a row nobody can see* was to show one or the other; the
   handoff's is to put them in one 336-pixel column where the selected row and its symbol are
   within a screenful of each other, and to scroll the selection into view whenever it moves.
   Both fix the same fault; this one does not cost a click to see what a class is made of. */
.symstack {
  margin-top: auto; padding: var(--gap-3);
  border-top: 1px solid var(--rule); background: #fafbfc;
  font-size: var(--t-small);
}
.symstack .symstackhead {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: var(--gap-2);
}
.symstack .symstackhead b { font-weight: 600; }
.symstack .symstackhead span { font-size: 12px; color: var(--muted); }

/* The document, as a pane rather than a `<details>`. It is the canonical thing Store sends, and
   a disclosure triangle over it said the opposite. */
.insppane .docpane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.insppane .docpane textarea {
  flex: 1; min-height: 0; margin: 0; border: 0; border-radius: 0; resize: none;
  padding: var(--gap-3); font: 12px/1.6 var(--mono); background: #fafbfc;
}
.insppane .docpane textarea:focus { outline: 2px solid var(--accent); outline-offset: -2px; }
.insppane .docfoot, .insppane .dochead {
  flex: none; padding: 10px var(--gap-3); color: var(--muted);
  font-size: 12.5px; line-height: 1.45;
  display: flex; gap: var(--gap-2); align-items: center;
}
.insppane .dochead { border-bottom: 1px solid var(--rule); }
.insppane .docfoot { border-top: 1px solid var(--rule); }
.insppane .dochead .tag {
  padding: 2px 8px; border-radius: 10px; background: var(--surface-2);
  font: 11.5px var(--mono); color: #33404f;
}

#symDerived { margin: 0 var(--gap-3) var(--gap-3); flex: 1; min-height: 120px; }

/*
  <b>One row per class, and the colour last.</b> The value is what a reader scans for, the label
  is what they write, and the colour is what they adjust — so they sit in that order, and the
  remove button is a ghost at the end where it cannot be hit on the way to anything else.
*/
.setting.symclass { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
.setting.symclass .q { flex: 0 0 auto; }
.setting.symclass .symvalue { flex: 1 1 120px; min-width: 90px; }
.setting.symclass .symlabel { flex: 1 1 120px; min-width: 90px; }

.setting input[type="color"] {
  width: 44px; height: 30px; padding: 2px;
  border: 1px solid var(--rule); border-radius: var(--round);
  background: var(--surface); cursor: pointer;
}

#symAdvanced { margin: 12px 0 0; }
#symAdvanced summary { cursor: pointer; font: var(--t-small)/1.4 var(--sans); color: var(--muted); }
#symAdvanced[open] summary { margin-bottom: 8px; }

/* --------------------------------------------------------- the symbol layer stack
   ADR-052 §3.7. A symbol is a stack, so the editor is a list of rows rather than one
   outline shared by every class. The selected class is marked because the panel below
   belongs to it, and a panel whose subject is not visible is a panel people misread. */

.setting.symclass{cursor:pointer}

.setting.symclass.symchosen{
  background:var(--accent-soft);
  border-radius:4px;
  box-shadow:inset 2px 0 0 var(--accent);
  padding-left:8px;
}

#symStackNote{margin:2px 0 8px}

.setting.symlayer{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  border:1px solid var(--rule);border-radius:4px;
  padding:7px 9px;margin-bottom:6px;
}

.setting.symlayer .symlayerkind{
  min-width:64px;font-weight:600;
}

.setting.symlayer input[type="number"]{width:74px}

/* <b>A resting border.</b> These were ghost buttons that appeared on hover, and reordering is
   the whole idea of a stack — its controls cannot be the ones nobody can see. */
.setting.symlayer .tiny{
  border:1px solid var(--rule);
  background:var(--surface);
  min-width:30px;
}
/* <b>No `margin-left: auto`, because the row wraps.</b> Pushing the reorder and remove buttons
   to the far right aligned them down the stack, which is worth having on a row that fits — and
   this one does not always: measured at 1440, a stroke or marker row breaks before its buttons,
   and `auto` then held them at the right edge of an otherwise empty line, three-quarters of it
   blank. A cluster alone at the end of a blank line reads as a rendering fault rather than as
   the same three buttons that were there before. They follow the controls now, so a second line
   begins where the eye is already going. */
.setting.symlayer .symup{margin-left:var(--gap-2)}

#symStackActions{gap:6px}

/* ------------------------------------------------------------- the symbol library
   ADR-052 §3.8. A grid of shipped symbols, each drawn from the same stack the editor
   below edits. The swatch is an icon; the picture beside the form is the renderer's. */

#symGallery{
  display:grid;gap:8px;
  grid-template-columns:repeat(auto-fill,minmax(84px,1fr));
  margin-bottom:14px;
}

.symcard{
  display:flex;flex-direction:column;align-items:center;gap:4px;
  padding:7px 5px 6px;cursor:pointer;
  background:var(--surface);
  border:1px solid var(--rule);border-radius:5px;
  font:inherit;font-size:11px;line-height:1.25;color:inherit;text-align:center;
}

.symcard:hover{border-color:var(--accent)}
.symcard:focus-visible{outline:2px solid var(--accent);outline-offset:1px}

.symcard svg{width:100%;height:38px;display:block}

/* <b>Two lines rather than an ellipsis.</b> *Road with ca…* and *Dashed boun…* are the two
   entries somebody is looking for, and a name clipped before its distinguishing word is a name
   that does not identify anything. */
.symcard span{
  display:block;max-width:100%;
  line-height:1.2;opacity:.85;
  overflow-wrap:anywhere;
}

#symGalleryNote{margin:2px 0 8px}

/* -------------------------------------------------------- varying with a number
   ADR-052 §3.6. Two stops, side by side, with the colour or the number showing
   according to what is being varied — the other is hidden rather than disabled,
   because a greyed control still reads as something that ought to work. */

.setting.symvarystop{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.setting.symvarystop input[type="number"]{width:96px}
#symVaryNote{margin:6px 0 0}

/* The Symbology link beside the Visualization layer picker. It sits on the toolbar
   baseline rather than the label's, so it lines up with the picker and not its caption. */

#visSymbology{
  align-self:flex-end;margin-bottom:6px;
  text-decoration:none;white-space:nowrap;
}

/* ------------------------------------------------------- the service's Symbology tab
   One row per layer: what the renderer draws, what it is drawn with, and the way in. */

.symrow{
  display:flex;align-items:center;gap:14px;
  padding:10px 4px;border-bottom:1px solid var(--rule);
}
.symrow:last-child{border-bottom:0}

.symrow .thumb{width:84px;height:56px;object-fit:cover;border-radius:4px;flex:0 0 auto}
.symrowtext{flex:1;min-width:0}
.symrowname{font-weight:600}
/* <b>No opacity.</b> `--muted` is 6.18:1 on `--surface` and this row's own three-quarters
   dropped it to 3.53:1 — measured, and AA asks 4.5 for text this size. The token is already
   tuned; dimming a tuned token is undoing the tuning. */
.symrow .rowmeta{color:var(--muted);font-size:12.5px;margin-top:2px}
.symrow .primarylink{flex:0 0 auto}

/* -------------------------------------------------- the symbology form's own spacing
   `.setting .q { flex: 1 }` suits a page of numeric fields, where the unit sits at the far
   right of a fixed row. Here it puts every label at one edge and every control at the other
   with a hand's width of nothing between, so no label is beside the thing it names. */

#page-symbology .setting .q{ flex:0 1 auto; min-width:104px; }
#page-symbology .setting{ gap:var(--gap-2); }

/* A class row is a control, so it looks like one when it has focus. */
.setting.symclass{ border-radius:var(--round); }

/* <b>The row is no longer focusable, so the ring belongs to whatever inside it has focus.</b>
   `:focus-within` keeps the row itself marked while a keyboard user is in one of its boxes,
   which is what says "this is the class you are editing" now that selection follows focus. */
.setting.symclass:focus-within{ outline:2px solid var(--accent); outline-offset:1px; }

#styleState{margin:0 0 10px}
#styleAdvanced summary{cursor:pointer;margin-top:10px}
#styleAdvanced textarea{margin-top:8px}

/* -------------------------------------------- the classify row
   <b>The button does not wrap.</b> "Read the values" fits on one line and "Read the data" broke
   across three in the same fixed-width button, so switching the Draw family visibly changed the
   row's height. Measured by a design review 2026-09-04. */
#symClassifyRow .tiny{ white-space:nowrap; }
#symClassCount{ width:5.5em; }
#symClassifySays{ margin:2px 0 10px; }

/* ------------------------------------------- the class list, when it is long
   <b>Ten rows, and the height is set in script rather than here.</b> A classification may hold
   256 classes; the owner looked at that list and then at twenty and said twenty was still too
   long. Ten is their number. It cannot be written as a length in this file because a row's
   height depends on the font, the theme and the browser's zoom — so `drawSymbologyClasses`
   measures one row and sets the box to ten of them, and the list is ten rows whatever any of
   those are.

   <b>No box at all when it fits.</b> A scroller around eight rows is a frame around nothing. */
#symClasses{
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* <b>This was moved from `--rule` to `--rule-strong` on 2026-09-04 and the comment claimed the
   contrast problem was fixed. It was not.</b> 1.23:1 became 1.45:1, and the rule the comment
   itself cited asks for 3:1 -- the repair was a step in the right direction reported as arrival,
   which is worse than leaving it alone, because a row in the register then says it is done.
   Computed 2026-09-05 rather than eyeballed. `--control-edge` clears it at 3.57:1, and this
   boundary earns the token: it carries the only standing signal that the list continues below. */
#symClasses.symscrolls{
  border: 1px solid var(--control-edge);
  border-radius: var(--round);
  padding: 4px;
}

#symFilter{ flex: 1 1 180px; min-width: 120px; }
#symFilterRow .rowmeta{ flex: 0 0 auto; font-variant-numeric: tabular-nums; }

/* ------------------------------------ a class row keeps to one line
   <b>Measured 2026-09-04: the same row is 46 pixels at 1440 wide and 86 at 1280.</b> The row
   wraps when its label and two boxes stop fitting, which doubles its height — and a list bounded
   at ten rows then wants 860 pixels, taller than the window it is in. The row shrinks instead:
   the value and label boxes give up width down to nothing rather than the row giving up a line.
   A colour swatch and a remove button do not shrink, and should not: they are already small
   enough to be hard to hit. */
.setting.symclass{ flex-wrap: nowrap; }
.setting.symclass .symvalue,
.setting.symclass .symlabel{ flex: 1 1 60px; min-width: 0; }
.setting.symclass .q{ white-space: nowrap; }

/* -------------------------- an opacity box beside a colour, on both surfaces
   <b>Narrow, because it holds at most five characters.</b> A per-cent box the width of the
   number boxes beside it reads as another measurement of the same importance as the colour, and
   it is not: it qualifies the swatch to its left. */
.symalpha,
#symVaryFromAlpha,
#symVaryToAlpha{ width: 4.75em; font-variant-numeric: tabular-nums; }

/* <b>A number and its unit are one flex item, not two.</b> They were two, and a row short of
   width broke between them: measured by a design review at 1280 wide, where the symbol stack's
   panel is 359 px and the *%* of an opacity wrapped onto a line of its own, under its own box.
   Gluing them is the fix that holds at any width, rather than a `nowrap` that turns a wrap into
   an overflow — which is what the first attempt did here, pushing the ↑ ↓ × buttons out past the
   row's own border by 125 px. */
.pair{ display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
.pair > input{ width: 4.75em; }

/* <b>These two rows wrap, and that is the right answer for them.</b> The class list does not:
   it is bounded to ten visible rows against a classification of 256, so a row that doubles its
   height halves what anybody can see. A symbol has two or three layers and the vary form has two
   rows, so a second line costs a reader nothing and an overflowing row costs them the controls.
   The difference is the length of the list, not the shape of the row. */
.setting.symlayer{ flex-wrap: wrap; row-gap: 6px; }
.setting.symvarystop{ row-gap: 6px; }
.setting.symvarystop input[type="number"]{ width: 5.5em; }

/* ══════════════════════════════════════════════ the 1c editor's own components
   Everything above this line was written for the two-column editor and most of it still
   applies; what follows is only what the three-column one adds or has to say differently.
   Kept together and last, so the difference between the two is readable as a block rather
   than as edits scattered through a thousand lines. */

/* --------------------------------------------------------------- a class row, restated
   <b>Swatch, then the two texts stacked, then the way to remove it.</b> The old row put a
   label word, a value box, a label box, a colour and two buttons on one line — six things
   across 620 pixels, which became six things across 306 when the inspector took over. The
   label and the value are the same fact at two levels, so they stack; the colour comes
   first because a list of classes is read as a list of colours. */

.setting.symclass {
  display: grid; grid-template-columns: 26px minmax(0, 1fr) 20px;
  gap: var(--gap-2); align-items: center;
  padding: 6px 8px; margin: 0;
  border-bottom: 1px solid #eef1f5;
}
.setting.symclass:last-child { border-bottom: 0; }

.setting.symclass .symtexts { min-width: 0; display: flex; flex-direction: column; gap: 2px; }

/* <b>Boxes that do not look like boxes until they are used.</b> Two bordered inputs a row,
   ten rows deep, is a wall of rectangles; the full border arrives on hover and focus, which is
   when it is telling somebody something. The text is still selectable and the control is
   still an input — this is a border, not a disguise.

   <b>But *no* affordance at rest was a different thing, and it was
   [D-225](../../../docs/architecture-debt.md).</b> SC 1.4.11 asks 3:1 of the visual information
   that identifies a control, and an outline that appears on hover satisfies it for nobody: a
   pointer is how you find out, and a keyboard user tabbing through, or anyone scanning the
   screen, never hovers. The row's own trigger asked for this to be decided as a design question
   rather than a contrast one — *raising a contrast that is not there is not a repair* — and
   named the two quiet shapes: a baseline rule, or a fill a shade off the row.

   <b>The baseline rule, because it is one line instead of four.</b> It keeps the reason the
   border was dropped — ten rows of rectangles would be the loudest thing on a screen whose
   subject is colour — while saying *you can type here* without being asked. It is the filled
   text field's affordance, and it is `--control-edge`, so it carries D-215's measured 3.57:1 on
   `--surface`, 3.24 on `--surface-2` and 3.01 on `--surface-3` rather than a value chosen here
   by eye. The transparent border on the other three sides is what keeps hover from moving
   anything. */
.setting.symclass .symlabel,
.setting.symclass .symvalue {
  width: 100%; min-width: 0; flex: none;
  padding: 2px 5px; background: none;
  border: 1px solid transparent;
  border-bottom-color: var(--control-edge);
  border-radius: 3px;
}
.setting.symclass .symlabel { font-size: var(--t-small); color: var(--ink); }
.setting.symclass .symvalue {
  font: 11.5px var(--mono); color: var(--muted);
}
/* <b>The arriving border is the one that has to be seen.</b> It was `--rule-strong` at 1.45:1,
   which after 2026-09-05 was *lighter* than the resting border of every other box on the page --
   so hovering one of these made it fainter than a control that had done nothing. Whether they
   should be borderless at rest at all is a separate question and a separate row:
   [D-225](../../../docs/architecture-debt.md). */
.setting.symclass .symlabel:hover, .setting.symclass .symvalue:hover,
.setting.symclass .symlabel:focus, .setting.symclass .symvalue:focus {
  border-color: var(--control-edge); background: var(--surface);
}

.setting.symclass .symname { font-size: var(--t-small); }

.setting.symclass .symdrop {
  border: 0; background: none; color: var(--faint); min-width: 0; padding: 0;
}
.setting.symclass .symdrop:hover { color: var(--alert); background: none; }

/* <b>3px, and it was 2.</b> The row has a hairline under it now, so a 2px edge read as part
   of the grid rather than as the mark on one row of it. The handoff says 3. */
.setting.symclass.symchosen {
  background: var(--accent-soft); border-radius: 0;
  box-shadow: inset 3px 0 0 var(--accent);
  padding-left: 8px;
}

/* -------------------------------------------------------------- the swatch has a shape
   <b>The inner swatch of a colour input is stylable, and that is what makes this cheap.</b>
   Shaping the element itself would shrink the target — a 12-pixel-tall line swatch is a
   12-pixel-tall click — so the box stays 26 by 22 and the padding decides what is drawn
   inside it: a bar for a line, a rounded rectangle for an area, a disc for a point. */

.setting input[type="color"].symfill { width: 26px; height: 22px; flex: none; }

input[type="color"].sw-line { padding: 8px 2px; }
input[type="color"].sw-poly { padding: 3px; }
input[type="color"].sw-point { padding: 3px; border-radius: 50%; }

input[type="color"].sw-line::-webkit-color-swatch { border: 0; border-radius: 1px; }
input[type="color"].sw-poly::-webkit-color-swatch { border-radius: 2px; }
input[type="color"].sw-point::-webkit-color-swatch { border-radius: 50%; }
input[type="color"].sw-line::-moz-color-swatch { border: 0; border-radius: 1px; }
input[type="color"].sw-poly::-moz-color-swatch { border-radius: 2px; }
input[type="color"].sw-point::-moz-color-swatch { border-radius: 50%; }

/* The legend's swatch is the same three shapes, drawn rather than editable. */
.legendsw { display: block; flex: none; background: var(--sw); }
.legendsw.sw-line { width: 20px; height: 3px; border-radius: 1px; }
.legendsw.sw-poly {
  width: 18px; height: 12px; border-radius: 2px; border: 1px solid rgba(16, 24, 38, .25);
}
.legendsw.sw-point {
  width: 12px; height: 12px; border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(16, 24, 38, .3);
}

/* ------------------------------------------------------------------- the class list box
   Inside the inspector the list is a bordered block rather than a run of rows, because the
   symbol stack under it is a block too and two unframed lists would read as one. */
#insp-classes #symClasses {
  border: 1px solid var(--rule); border-radius: 6px; overflow: auto;
}
#insp-classes #symClasses.symscrolls { border-color: var(--control-edge); padding: 0; }
#insp-classes #symClasses .hint { margin: 0; padding: 10px; }

#insp-classes #symFilterRow, #insp-classes #symAllRow { gap: var(--gap-2); }
#insp-classes #symClassActions { margin: var(--gap-2) 0 var(--gap-3); }

/* --------------------------------------------------------- the stack, in 336 pixels
   The handoff's row: kind, colour, the measurement and its unit, then the three buttons.
   One line, because the panel is short and a stack that wraps is taller than the list
   above it. */
.symstack .setting.symlayer {
  display: grid; grid-template-columns: 52px 26px minmax(0, 1fr) auto;
  gap: var(--gap-2); align-items: center; flex-wrap: nowrap;
  padding: 7px 9px; margin-bottom: 6px;
}
.symstack .setting.symlayer .symlayerkind { min-width: 0; font-size: 12.5px; }
.symstack .setting.symlayer .symmeasures {
  display: flex; gap: 6px; align-items: center; min-width: 0;
}
.symstack .setting.symlayer .pair > input { width: 3.6em; }
.symstack .setting.symlayer .tiny { min-width: 24px; padding: 2px 4px; }
.symstack .setting.symlayer .symup { margin-left: 0; }
.symstack .symbuttons { display: flex; gap: 4px; }
.symstack #symStackActions { gap: 6px; margin: var(--gap-2) 0 0; }
.symstack .hint { margin: 0; }

/* --------------------------------------------------------------- the rail's own widths */

#symClassifyRow { display: flex; align-items: center; gap: var(--gap-2); flex-wrap: wrap; }
#symClassifyRow .q { flex: 1 1 100%; }
.symclassify { display: flex; gap: 6px; flex: 1 1 auto; min-width: 0; }
.symclassify #symClassCount { width: 3.4em; flex: none; text-align: center; }
.symclassify #symMethod { flex: 1 1 auto; min-width: 0; }

/* Three across, which is what a 232-pixel content width holds without a name clipping. */
.symrail #symGallery { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin: 0; }
.symrail .symcard { padding: 6px 4px 5px; font-size: 10.5px; }
.symrail .symcard svg { height: 30px; }

.symrail .setting.symvarystop { display: flex; flex-wrap: wrap; gap: 6px; }
.symrail .setting.symvarystop .q { flex: 1 1 100%; margin: 0; }
.symrail .setting.symvarystop input[type="number"] { width: 4.6em; }

/* ------------------------------------------------------------------- what is underneath
   Three grounds for a transparent picture. `none` is the checker every empty thumbnail in
   this console already uses, so *no ground* looks like the same absence everywhere. */
.sympreview.ground-light { background: #f2f5f8; }
.sympreview.ground-dark { background: #131a26; }
.sympreview.ground-none {
  background: repeating-linear-gradient(135deg, #eef2f6 0 6px, #e6ebf1 6px 12px);
}

/* On the dark ground the overlays would be four white cards on a night map; they keep their
   contrast and lose their brightness. */
.sympreview.ground-dark .symcap,
.sympreview.ground-dark .symlegend { background: rgba(19, 26, 38, .88); color: #cfd6e2; }
.sympreview.ground-dark .symcap { box-shadow: 0 0 0 1px rgba(255, 255, 255, .14); }
.sympreview.ground-dark .symlegend { box-shadow: 0 0 0 1px rgba(255, 255, 255, .14); }
.sympreview.ground-dark .symlegend em { color: #8e98a8; }

/* --------------------------------------------------------------------- the banners
   The refusal is red because the server said no; the override is amber because nothing
   failed — what is on the screen is simply not what a map draws. */
.symbanner.note {
  background: var(--warn-soft); border-bottom-color: var(--warn-line); color: var(--warn);
}
.symbanner .tiny { border-color: currentColor; color: inherit; background: none; }

/* <b>Neutral, because nothing is wrong</b> — ADR-075, D-271. Reading somebody else's layer is an
   ordinary state, and the amber above already means *what you see is not what a map draws* on this
   same page; a design review read the ownership band as that warning. The surface the unauthored
   note uses, which is the page saying a plain fact about itself. */
.symbanner.note.ownership {
  background: var(--surface-2); border-bottom-color: var(--rule); color: var(--muted);
}

#symUnauthored {
  flex: none; margin: 0;
  padding: 10px var(--gap-5);
  background: var(--surface-2); border-bottom: 1px solid var(--rule);
}

/* ---------------------------------------------------------- what the rail does not need
   `.setting .q { flex: 1 }` and the 104-pixel minimum above were written for a form 620
   pixels wide. In a 232-pixel column they put every label on its own line anyway and then
   held a control at 104 pixels from the left of nothing. */
#page-symbology .symrail .setting .q,
#page-symbology .insppane .setting .q { min-width: 0; }
#page-symbology .insppane .setting .q { flex: 0 0 auto; }

/* --------------------------------------------------------------- the corrections pass
   Measured on the running fixture set at 1440×900, 2026-09-04. Each of these was visible
   in a screenshot rather than argued about. */

/* <b>The panel's own bottom margin, on a panel that is the window.</b> `.panel` carries 16
   pixels so panels stack; this one is a screen, and 16 pixels below the fold is a scrollbar
   on a page that fits. Measured: `document.scrollHeight` 916 against a 900-pixel window. */
#app.symfull #edit { margin-bottom: 0; }

/* <b>Left, and mono only where a value is mono.</b> `.setting input { text-align: right }` is
   right for a form of numbers with units beside them and wrong for a column of names: every
   class label was pinned to the right edge of its own box, so a list of 256 read as ragged
   text with a gap down the left. */
.setting.symclass input:not([type=color]),
.symrail .setting select,
.symrail .setting input:not([type=checkbox]):not([type=color]),
.insppane .setting input:not([type=checkbox]):not([type=color]) { text-align: left; }

.setting.symclass .symlabel { font-family: var(--sans); }
.symrail .setting select { font-family: var(--sans); }

/* The two rail rows that are a number with a unit keep the right-hand alignment a measurement
   wants; the exception above is for names, not for figures. */
.symclassify #symClassCount, .insppane .pair > input { text-align: right; }

/* <b>Two columns when the renderer is one this form cannot author.</b> A `display: none` grid
   item is not an empty cell, it is no cell — so hiding the rail moved the picture into its
   264 pixels and left the inspector's 336 blank. */
.symcols.noRail { grid-template-columns: minmax(0, 1fr) 336px; }

/* The caption and the ground chips share the top of the picture, so neither may take all of
   it. At 264 pixels — which is what the column is while a Proportional renderer is open —
   they were on top of each other. */
.symcap { max-width: 46%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.symground { max-width: 50%; }

.insppane .docfoot > span { min-width: 0; }
.insppane .docfoot .tiny, .insppane .dochead .tiny { flex: none; margin-left: auto; }

/* The generated swatch on the empty screen is the layer's own colour, not a grey box: the
   sentence beside it says the colour is derived from the layer's identity, and a hatched
   rectangle is the one thing on the screen that could have shown it. */
.symempty .sw i {
  display: block; background: var(--sw, var(--muted));
  box-shadow: 0 0 0 1px rgba(16, 24, 38, .25);
}
.symempty .sw i.sw-line { width: 70px; height: 4px; border-radius: 2px; box-shadow: none; }
.symempty .sw i.sw-poly { width: 62px; height: 40px; border-radius: 3px; }
.symempty .sw i.sw-point { width: 26px; height: 26px; border-radius: 50%; }

/* <b>The swatch fills the box it is in.</b> Chromium wraps a colour input's swatch in its own
   padded element, so a 26×22 control drew a 20×9 chip inside six pixels of nothing and the
   three shapes were indistinguishable from each other. The wrapper's padding goes; the input's
   own padding is what makes a bar, a box or a disc of it. */
input[type="color"].symfill::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"].symfill::-webkit-color-swatch { border: 1px solid rgba(16, 24, 38, .2); }

/* <b>The symbol stack is a footer, not the end of a scroll.</b> Ten class rows at 58 pixels
   each is 580, and the pane is about 700 — so on the fixture this screen was reported against
   the panel naming the class you are editing sat thirty pixels below the fold, which is the
   fault D-217 was about wearing a different shape. The list gives up the room instead.

   <b>One scroller, not two.</b> The list is the flexible child of a column that does not
   scroll, so a short list is its own height and a long one stops where the panel does. Putting
   the scroll on the wrapper instead would nest a ten-row scroller inside a pane scroller, and
   nobody can work out which of two bars moves what. */
#insp-classes { overflow: hidden; }
#insp-classes > .pad {
  flex: 1; min-height: 0; display: flex; flex-direction: column; overflow: hidden;
  padding-bottom: var(--gap-3);
}
#insp-classes #symFilterRow,
#insp-classes #symAllRow,
#insp-classes #symClassActions { flex: none; }
#insp-classes #symClasses { flex: 0 1 auto; min-height: 0; overflow: auto; }
#insp-classes .symstack { flex: none; margin-top: 0; }

/* <b>The strip's right-hand end is not allowed to leave the screen.</b> Measured on a
   three-layer service: the switcher's chips took 420 pixels, the tabs 400, and Store was 130
   pixels past the right edge with nothing to scroll it back. Store is the only control on this
   screen that cannot be reached another way, so it is the one thing that never shrinks; the
   breadcrumb gives up characters first, then the switcher scrolls. */
/* <b>The crumb still abbreviates before the controls do.</b> It shared the strip with a
   segmented layer switcher until the handoff's revision moved that into the rail; what is left
   is context, and context is the thing that can be shortened. `flex-basis: 0` keeps it to the
   room the tabs and the two acts leave over. */
.symstrip .crumbs {
  min-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  flex: 1 1 auto;
}
.symstrip .segmented.tabs { flex: none; }
.symstrip .symdo { flex: none; }

/* <b>Two measurements and three buttons in 306 pixels.</b> A stroke row carries a width, a
   unit, an opacity, a per-cent sign and the three controls that reorder and remove it; laid out
   as one unbroken line the opacity box came out four pixels wide. The measurements wrap inside
   their own cell instead, which keeps the buttons where they are down the stack. */
.symstack .setting.symlayer .symmeasures { flex-wrap: wrap; row-gap: 4px; }
.symstack .setting.symlayer .pair > input { width: 3.9em; }
.symstack .setting.symlayer .tiny { min-width: 22px; padding: 1px 3px; }
.symstack .symbuttons { gap: 2px; }

/* A classify row that is only its button — which is what a unique-value renderer's is — reads
   as a button somebody forgot to label when it is pushed to the right by a label that is not
   there. */
#symClassifyRow #symClassify { margin-right: auto; }

/* <b>Fitting 1,370 pixels of strip into 1,208.</b> Measured at 1440 on a three-layer service.
   The tabs give up horizontal padding first — they are five short words and were spending 410
   pixels on them — and the state line is capped with its whole sentence in a tooltip, because
   it is the one child of the strip that is prose. Store and *Back to generated* do not shrink:
   they are the two acts this screen exists for. */
/* <b>The same size as the item page's, because they are the same five tabs.</b> This strip drew
   them at 13.5px and the page head at 12.5; a reader crossing between the two screens met one
   control that had quietly changed scale. */
.symstrip .segmented.tabs a { padding: 4px 9px; font-size: 12.5px; }
.symstrip .symstate {
  max-width: 190px; overflow: hidden; text-overflow: ellipsis;
}

/* One symbol is not a list, so its pane does not reserve a list's worth of height. */
#insp-classes.onesymbol { overflow: auto; }
#insp-classes.onesymbol > .pad { flex: none; overflow: visible; }

/* ═══════════════════════════════════════════════════ the item page, handoff 2026-09-04 */

/* <b>The service's tabs are a segmented control now, beside its title.</b> `.tabstrip` is still
   right where it is used — a scope strip over a table, a group's sections — but this one moves
   between the five faces of one item, which is the same act the layer editor's strip performs,
   and two shapes for one act is D-46's whole subject. `solid` is the handoff's dark fill: these
   tabs sit on `--paper` rather than inside a panel, so the raised-white chip the other segmented
   controls use has nothing to be raised from. */
.segmented.solid a[aria-current] {
  background: var(--ink); color: var(--surface); box-shadow: none;
}

.pagehead .segmented { align-self: center; }

/* Two cards side by side, and one column when there is not room for two. */
.cardpair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--gap-4); }
.cardpair .panel { margin-bottom: 0; }
.cardpair h4 { margin-top: 0; }
@media (max-width: 1180px) { .cardpair { grid-template-columns: minmax(0, 1fr); } }

.pills { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: var(--gap-3); }

/* <b>Offered and not offered, both shown.</b> A list of what is on says nothing about what is
   off, and *why can this client not edit* is the question somebody has. The greyed pill keeps
   `--faint`, which is 4.54:1 on the palest surface it sits on — a state that cannot be read is
   not a lighter version of one that can. */
.pill.p-on { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.pill.p-off { background: var(--surface-2); border-color: var(--rule); color: var(--faint); }
.pill.p-off::before { opacity: .45; }

/* --------------------------------------------------------- the Overview layers table
   Five columns: the id every address uses, the shape the layer is, its name, whether anybody
   has styled it, and the three ways in. */
#serviceLayerRows .lid {
  width: 30px; font-family: var(--mono); font-size: 12.5px; color: var(--muted);
  text-align: right; vertical-align: middle;
}
#serviceLayerRows .lsw { width: 44px; vertical-align: middle; }
#serviceLayerRows .lstate { width: 160px; vertical-align: middle; }
#serviceLayerRows .lstate .authored { color: var(--accent); font-weight: 600; }

.geoswatch {
  display: grid; place-items: center; height: 30px; border-radius: 4px;
  background: repeating-linear-gradient(135deg, #eef2f6 0 5px, #e6ebf1 5px 10px);
}
.geoswatch::before { content: ""; display: block; background: var(--sw, var(--muted)); }
.geoswatch.sw-line::before { width: 26px; height: 3px; border-radius: 2px; }
.geoswatch.sw-poly::before {
  width: 24px; height: 15px; border-radius: 2px; border: 1px solid rgba(16, 24, 38, .25);
}
.geoswatch.sw-point::before {
  width: 12px; height: 12px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(16, 24, 38, .3);
}

/* <b>Three buttons, not two links run together.</b> Measured before the change: *Symbology* and
   *document* sat side by side with a space between them, which reads as one target of 96 pixels
   holding two. A resting border is what makes three of them three. */
#serviceLayerRows .acts { white-space: nowrap; }
#serviceLayerRows .acts .tiny {
  border: 1px solid var(--rule); background: var(--surface); border-radius: 4px;
  padding: 3px 9px; margin-left: 6px; color: var(--ink); font-size: 12.5px;
}
#serviceLayerRows .acts .tiny:hover { border-color: var(--accent); color: var(--accent); }

/* <b>A blank cover explains itself.</b> The picture arrived and is empty, which is not the same
   as a picture that failed — and a white rectangle where a map should be reads as the second. */
.thumb.empty.drewnothing {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; padding: 0 14px; text-align: center;
  font-size: 12px; line-height: 1.5; color: var(--faint);
}
.thumb.empty.drewnothing b { color: #33404f; font-size: 13px; }

/* The Visualization toolbar's own row: a label, the layer strip, what that layer is, and the
   way to its symbology. `.toolbar` already lays these out; these only stop the strip from
   stretching and give the meta a resting colour. */
.toolbar .segmented { flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.toolbar .segmented::-webkit-scrollbar { height: 0; }
#visMeta { color: var(--muted); font-size: var(--t-small); white-space: nowrap; }

/* <b>A hover ring, so the picture reads as the target it is.</b> The thumbnail in a content row
   opens the map; without a resting affordance it is a picture that happens to be clickable,
   which is a thing people find by accident or not at all. */
.thumblink { display: block; border-radius: 5px; }
.thumblink:hover .thumb, .thumblink:focus-visible .thumb {
  box-shadow: 0 0 0 2px var(--accent);
}
.thumblink:focus-visible { outline: none; }

/* ------------------------------------------------------ summary tiles, handoff 2026-09-04
   <b>A count first, then what it counts.</b> Used by the anonymous view's three outcomes: a
   report whose usual answer is *nothing is wrong* has to be able to say that as a number, or
   every run reads as one that found nothing rather than as one that found nothing wrong.

   <b>A zero is quiet, and a zero is not always good.</b> Nought layers answering anyway is the
   result somebody wants; drawing it in alert red would train the reader to ignore the colour.
   The tone only fills in when the count is above nought. */
.tilerow {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--gap-3); margin-bottom: var(--gap-4);
}
.tile {
  background: var(--surface); border: 1px solid var(--rule); border-radius: var(--round-lg);
  box-shadow: var(--lift); padding: var(--gap-3) var(--gap-4);
  display: flex; flex-direction: column; gap: 2px;
}
.tile b {
  font-size: 26px; font-weight: 650; letter-spacing: -.02em; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.tile span { font-size: var(--t-small); color: var(--muted); }
.tile.t-ok b { color: var(--ok); }
.tile.t-alert { border-color: var(--alert-line); background: var(--alert-soft); }
.tile.t-alert b { color: var(--alert); }
.tile.t-warn { border-color: var(--warn-line); background: var(--warn-soft); }
.tile.t-warn b { color: var(--warn); }
.tile.quiet { border-color: var(--rule); background: var(--surface); }
.tile.quiet b { color: var(--faint); }

/* A row the report disagrees with, tinted where the eye lands rather than only in its last
   cell. `--alert-soft` is the same ground the alert pill uses, so the two read as one signal. */
tr.rowbad > td { background: var(--alert-soft); }

/* ------------------------------------------------------- operations, handoff 2026-09-04
   Three cards across rather than three sections down, then the two short blocks side by side.
   The route table stays full width below both: its patterns do not wrap and it needed 1,407
   pixels the last time anybody measured it. */
/* Four cards since D-237 added Datastore, and the basis came down from 300px so the fourth
   fits beside the others at 1440 rather than sitting alone under the first with the rest of
   its row empty — design review 2026-09-11. 1100 and 400 still give two and one. */
.opscards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--gap-4); margin-bottom: var(--gap-5);
  align-items: start;
}
.opscard { margin-bottom: 0; display: flex; flex-direction: column; }
.opscard h4 {
  margin: 0 0 var(--gap-3); display: flex; align-items: center; gap: var(--gap-2);
  padding-bottom: var(--gap-2); border-bottom: 1px solid var(--rule);
}
.opscard .metrics { margin: 0; }
.opscard .hint { margin: var(--gap-3) 0 0; }
.opscard .hint:empty { display: none; }

.opspair {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: var(--gap-5); align-items: start; margin-bottom: var(--gap-5);
}
.opspair .label:first-child { margin-top: 0; }
.opspair .dot { margin-right: 6px; }

@media (max-width: 1100px) { .opspair { grid-template-columns: minmax(0, 1fr); } }

/* <b>A dot with a class, because `#healthDot` was the only one and it was styled by id.</b> The
   operations cards need the same indicator and copying four rules under a second id is how a
   product ends up with two greens. The dot tokens are the indicator pair the token block
   defines — `--ok` is the text colour and `--ok-dot` is the dot, and the reason is written
   there: dark enough to read and dark reads as muddy on an arc. */
.dot {
  display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--faint); vertical-align: middle;
}
.dot.ok { background: var(--ok-dot); }
.dot.degraded { background: var(--warn-dot); }
.dot.unusable { background: var(--alert-dot); }
.dot.unknown { background: var(--rule-strong); }

/* <b>A label and its figure on one line, which a bare `dl` does not do.</b> `dl.metrics` had no
   rules at all: the browser's default put every value on its own line indented 40 pixels, so a
   card of five figures was ten lines with a staircase down the middle. Measured on the
   operations cards, 2026-09-04. */
.metrics { display: flex; flex-direction: column; gap: 7px; margin: 0; }

/* <b>The grid is on the row, not on the list.</b> `metric()` wraps each pair in a `div`, so a
   grid on the `dl` would lay out the wrappers and leave every `dt`/`dd` stacked inside one. */
.metric {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px var(--gap-3); align-items: baseline;
}
.metric dt { color: var(--muted); font-size: var(--t-small); min-width: 0; }
.metric dd {
  margin: 0; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The note is a second `dd`, and it belongs under both columns rather than beside the figure. */
.metric dd.val {
  grid-column: 1 / -1; text-align: left; font-weight: 400; white-space: normal;
  color: var(--faint);
}
.metric dd small { font-weight: 400; color: var(--muted); font-size: 11.5px; margin-left: 2px; }

/* <b>The override is a footer of the symbology panel, not a panel of its own.</b> It is the
   exception to every row above it — unless one of these is stored, none of them is what the tile
   face draws — and an exception is read under the rule it qualifies. */
.styleoverride {
  margin: var(--gap-4) calc(var(--gap-5) * -1) calc(var(--gap-4) * -1);
  padding: var(--gap-4) var(--gap-5);
  border-top: 1px solid var(--rule); background: #fafbfc;
  border-radius: 0 0 var(--round-lg) var(--round-lg);
}
.styleoverridehead {
  display: flex; align-items: flex-start; gap: var(--gap-4); flex-wrap: wrap;
}
.styleoverridehead .row { margin: 0 0 0 auto; flex: none; }
.styleoverride .hint { margin: 2px 0 0; }
.styleoverride #styleAdvanced { margin-top: var(--gap-3); }

/* ------------------------------------------------------- a member's initials, 2026-09-04
   <b>No image and no request.</b> Two letters on a tinted square, the tint derived from the
   name — the same determinism the generated symbology uses, so the square somebody learns is
   the same square tomorrow. Ink stays `--ink` at every hue because only the hue moves. */
.whorow { display: flex; align-items: center; gap: var(--gap-3); min-width: 0; }
.avatar {
  width: 28px; height: 28px; flex: none; border-radius: 6px;
  display: grid; place-items: center;
  background: var(--avatar, var(--surface-3));
  box-shadow: inset 0 0 0 1px rgba(16, 24, 38, .08);
  font: 600 11.5px/1 var(--sans); color: #33404f; letter-spacing: .02em;
}

/* <b>The role select wears the pill's hue.</b> The handoff draws a pill — admin teal, publisher
   violet, other grey — and a pill cannot be changed; this is the control that changes what
   somebody may do, so it keeps being a control and takes the colour. */
.rolepick { font-weight: 500; }
.rolepick[data-role-is="administrator"] {
  color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft);
}
.rolepick[data-role-is="publisher"] {
  color: var(--violet); border-color: #d9cdf3; background: var(--violet-soft);
}

/* -------------------------------------------------------- the sharing scope, as cards
   <b>Three answers with a sentence each.</b> A select can carry a word, so the sentences were
   three paragraphs under it — which is where an explanation goes when the control has no room
   for one, and it means the reader chooses before reading. The chosen card takes a 2px accent
   border and the pale accent ground, which is the same *this one* the class list uses. */
.scopecards { display: flex; flex-direction: column; gap: var(--gap-2); max-width: 620px; }

.scopecard {
  display: flex; gap: var(--gap-3); align-items: flex-start;
  padding: 10px 12px; border: 1px solid var(--rule); border-radius: 6px;
  background: var(--surface); cursor: pointer;
}
.scopecard:hover { border-color: var(--rule-strong); }
.scopecard:has(input:checked) {
  border: 2px solid var(--accent); padding: 9px 11px; background: var(--accent-soft);
}
.scopecard:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.scopecard input { margin: 3px 0 0; flex: none; accent-color: var(--accent); }
.scopecard > span { min-width: 0; }
.scopecard b { font-weight: 600; }
.scopecard .said { display: block; color: var(--muted); font-size: 12.5px; margin-top: 1px; }

/* <b>The stack's colour box is 26 pixels because its grid column is.</b> `.setting
   input[type="color"]` is 44 wide, which suits a form row and not a 306-pixel panel: squeezed
   into the column it drew a grey bar with no colour visible in it. And the width boxes take one
   more character, because a stroke width is `0.75` and `0.` is not a number. */
.symstack .setting.symlayer input[type="color"] { width: 26px; height: 22px; padding: 2px; }
.symstack .setting.symlayer input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.symstack .setting.symlayer .pair > input { width: 4.9em; }

/* <b>The Map shortcut costs 42 pixels the services table did not have, and it still does.</b>
   Measured at 1440: without the shortcut the table is 816 wide in an 816-pixel column beside the
   folder rail — exactly full — and with it, 858. Tightening the row's own padding is what this
   block does and it buys back none of the difference, because the columns were already at their
   content width; the honest statement is that this table now scrolls by 42 pixels at 1440 and
   that `.widetable` is the mechanism that lets it. The alternative was hiding the shortcut in
   the overflow menu, which is where the handoff explicitly did not put it. */
#services .acts { white-space: nowrap; }
#services .acts .tiny, #services .acts .menu > summary { padding: 3px 7px; }
#services .acts .tiny + .tiny, #services .acts .tiny + .menu { margin-left: 4px; }
#services .acts .ico { margin-right: 3px; }

/* ═════════════════════════════════ the rail, handoff revision 2026-09-04
   Three changes, all of them about the same thing: a 264-pixel column was reading as a wall of
   controls. Which layer moves into it from the title strip, because it is the editor's first
   question and not a place; the two blocks a reader has usually not asked for fold shut and say
   what they hold on their own row; and the service-wide override becomes a footnote at the
   bottom rather than a panel on a page of its own. */

/* --------------------------------------------------------------- which layer, as cards
   Each entry carries the geometry as a swatch and whether anybody has styled it, so choosing
   between three layers does not mean opening three of them. Drawn even at one layer: a heading
   that says *this is what you are editing* still has an answer there, and a segmented control
   of one did not. */
#symLayerPick { display: flex; flex-direction: column; gap: 4px; }

.symlayerpick {
  display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 10px; align-items: center;
  padding: 7px 9px; border: 1px solid var(--rule); border-radius: 6px;
  background: var(--surface); color: var(--ink); text-decoration: none;
}
.symlayerpick:hover { border-color: var(--rule-strong); text-decoration: none; }
.symlayerpick.on {
  border: 2px solid var(--accent); padding: 6px 8px; background: var(--accent-soft);
}
.symlayerpick:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.symlayerpicktext { min-width: 0; }
.symlayerpickname {
  display: block; font-size: var(--t-small);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.symlayerpick.on .symlayerpickname { font-weight: 600; }
.symlayerpick .rowmeta { display: block; font-size: 12px; color: var(--muted); }
.symlayerpick .rowmeta.authored { color: var(--accent); font-weight: 600; }
.symlayerpick .geoswatch { height: 26px; }

/* ------------------------------------------------------------- a fold, and its summary
   <b>A disclosure owes a summary.</b> A row that says only *Vary with a number* asks the reader
   to open it to find out whether anything is varying, which is exactly the cost folding was
   meant to remove. */
.symfold { display: block; }

.symfoldhead {
  display: flex; align-items: center; gap: var(--gap-2); width: 100%;
  padding: 9px 11px; border: 1px solid var(--rule); border-radius: 6px;
  background: var(--surface); color: #33404f;
  font: inherit; font-size: 13px; text-align: left; cursor: pointer;
}
.symfoldhead:hover { border-color: var(--accent); }
.symfoldhead .caret { color: var(--faint); flex: none; }
.symfoldsays {
  margin-left: auto; font-size: 12px; color: var(--ink); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; max-width: 55%;
}
.symfoldsays.quiet { color: var(--faint); }

.symfoldbody { margin-top: var(--gap-2); }
.symfoldbody[hidden] { display: none; }

/* ------------------------------------------------- the service's own style, as a footnote
   It is a MapLibre document for the whole service — an expert control that most services do not
   have — so it sits under everything about this one layer, at the size a footnote is, and its
   editor opens only when somebody asks for it. */
.symoverride {
  margin-top: auto; padding-top: var(--gap-4);
  border-top: 1px solid var(--rule);
  font-size: 12.5px; color: var(--faint); line-height: 1.5;
}
.symoverride > b { display: block; color: #33404f; font-size: 13px; margin-bottom: 3px; }
.symoverride .hint { margin: 0 0 6px; font-size: 12.5px; }
.symoverride .row { margin: 0 0 var(--gap-2); gap: 6px; flex-wrap: wrap; }
.symoverride textarea { width: 100%; font: 11.5px/1.5 var(--mono); }

/* The scope pill is its own child of the strip, so the breadcrumb's ellipsis cannot eat it. */
.symstrip #symScope { flex: none; display: inline-flex; }
.symstrip #symScope:empty { display: none; }

/* <b>The generated screen covers the picture and the inspector, not the rail.</b> The handoff's
   revision moved the layer list into the rail, and a screen about *this* layer's absent document
   must not hide the way to the service's other layers. It takes their two columns and says so
   across both. */
.symcols.generated > .sympreview, .symcols.generated > .syminsp { display: none; }
.symcols.generated > .symempty { grid-column: 2 / -1; }
.symcols > .symempty { border-left: 1px solid var(--rule); background: var(--paper); }

/* ══════════════════════════════════ the preview column is a map, 2026-09-04
   Owner, pointing at two ArcGIS Online Map Viewer videos: the map should open the way theirs
   does, and what the symbology controls change should show on it. The still frame could answer
   *what colour* and never *what does this look like where I am looking* — which is the question
   ADR-051 says an appearance is chosen by.

   <b>The ground pans, the picture is drawn by the server.</b> Two elements: an OpenLayers map
   underneath, and the image the preview endpoint returns for that exact viewport over it. The
   image takes no pointer events, so every gesture reaches the map. */
#symMap { position: absolute; inset: 0; }
#symMap .ol-viewport { border-radius: 0; }

.sympreview > img,
.sympreview > .thumb.empty { pointer-events: none; z-index: 1; }

/* <b>The ground chips choose what OpenLayers draws, not what is behind a transparent PNG.</b>
   With a map underneath, *None* is the honest name for no basemap — the hatched checker that
   used to mean it is what an absent picture looks like, not an absent ground. */
.sympreview.ground-none #symMap { opacity: 0; }
.sympreview.ground-dark #symMap { filter: invert(1) hue-rotate(180deg) saturate(.7) brightness(.85); }

/* The overlays sit above both. */
.symcap, .symground, .symlegend { z-index: 2; }

/* The zoom buttons take the console's own chrome rather than OpenLayers' blue. */
.sympreview .ol-zoom { top: auto; left: 14px; bottom: 14px; z-index: 2; }
.sympreview .ol-zoom button {
  background: var(--surface); color: var(--ink); border-radius: 4px;
  box-shadow: 0 0 0 1px var(--rule); font: 600 15px/1 var(--sans); margin: 0 0 2px;
  width: 26px; height: 26px;
}
.sympreview .ol-zoom button:hover { background: var(--surface-2); color: var(--accent); }
/* The legend keeps the bottom-left corner, so it moves up out of the buttons' way. */
.symlegend { bottom: 74px; }

/* ══════════════════════════════════ below the editor's floor, D-221 — owner decision 2026-09-11
   <b>The columns stack and the picture stays on the screen.</b> The owner's answer —
   *"Yeniden dizilsin"*, re-arrange it — and the two alternatives they turned down: a sentence
   saying this screen needs more width, and waiting for somebody to complain. ADR-053 set a floor of 1,180 pixels and nothing answered below it.

   <b>What happened below it was worse than the register said.</b> D-221 recorded that the whole
   editor scrolled sideways. It did not: `#edit` carries `overflow: hidden` for its rounded
   corners, so the three columns were cut off at the window's edge with no scrollbar at all.
   Measured 2026-09-11 with the sidebar open: at 1,300 the inspector and Store were past the edge
   and unreachable; at 1,024 the picture was cut to 528 of its 580 pixels as well; at 768 a
   reader saw 272 pixels of picture and nothing to the right of it.

   <b>Measured against the editor, not the window.</b> The floor is the editor's own width, and the
   window is that plus a sidebar the reader chooses — 232 pixels open, 64 collapsed. A window
   query would have to carry a copy of the sidebar's width inside each breakpoint and a copy of
   each rule per sidebar state, and the sidebar has changed width once already (248 to 232, see
   `#shell`). The idiom is otherwise the one `.pickpair` and `.cardpair` use: a grid gives up a
   column under a width, nothing is hidden, nothing new is drawn. So the numbers here are the
   editor's: three columns from 1,180, which is a 1,412 window with the sidebar open and 1,244
   with it collapsed — exactly where they were, so the wide layout is unchanged wherever it fit.

   <b>Layout containment, and only while the editor is showing.</b> A size container contains its
   own layout, which makes it the reference box for anything fixed-position inside it. Nothing on
   this page is — the row menus that position themselves against the window live on other pages,
   and the dialogs are in the top layer — and `symfull` is set for this page alone. */
#app.symfull #editPages { container: symeditor / inline-size; }

/* <b>Under 1,180: the picture on top of the renderer, the inspector beside both.</b> The picture
   keeps the full width of the column it leaves and the height of most of the screen; the
   inspector keeps exactly the column it had — 336 pixels, window-tall — because it is the one
   whose inside was tuned against a height: the class list gives up room so the symbol stack
   below it stays on the screen (ADR-053 §5), and a short inspector would bring that fault back.
   The rail is the column that moves. It was a scroller before and it still is, now under the
   picture rather than beside it. */
@container symeditor (max-width: 1179px) {
  #app.symfull #page-symbology.on { min-width: 0; }

  .symcols {
    grid-template-columns: minmax(0, 1fr) 336px;
    grid-template-rows: minmax(240px, 56%) minmax(0, 1fr);
    grid-template-areas: "preview insp" "rail insp";
  }
  .symcols > .sympreview { grid-area: preview; }
  .symcols > .symrail { grid-area: rail; border-right: 0; border-top: 1px solid var(--rule); }
  .symcols > .syminsp { grid-area: insp; }

  /* <b>The rail's form keeps a form's width, and the three renderer cards go side by side.</b>
     Design review 2026-09-11, measured at 1,300: under the picture the rail is 732 pixels wide
     and 350 tall, holding 736 of content — the cards and the Field list stretched across all of
     it with an icon and a label at one end, and half the rail was below the box. Side by side the
     cards give back 75 pixels of that height, which at 1,300 brings the Renderer section to
     within eight pixels of the box's bottom; the cap stops every control being as wide as a
     picture. 560 is three cards at 180 and a list at a width somebody can read across. What is
     still below the box there is the two folds and the service-wide override, which the handoff
     folded for being the parts a reader has usually not asked for, and the rail scrolls to them —
     with its own scrollbar, as it always has. At 1,024 by 768 the box is 274 pixels and the
     Field list and *Read the values* are below it too; the picture keeps its height regardless,
     because it is what the page is for. */
  .symcols > .symrail > * { max-width: 560px; }
  .symcols > .symrail .symkinds { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }

  /* No rail, so no second row: the picture takes the height the rail would have had. */
  .symcols.noRail { grid-template-rows: minmax(0, 1fr); grid-template-areas: "preview insp"; }

  /* The generated screen keeps its shape — the layer list, then the sentence across the rest —
     because the columns it covers are the ones that moved. */
  .symcols.generated {
    grid-template-columns: 264px minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
    grid-template-areas: "rail empty";
  }
  .symcols.generated > .symrail { border-top: 0; border-right: 1px solid var(--rule); }
  .symcols.generated > .symempty { grid-area: empty; }

  /* <b>The strip wraps rather than pushing Store out.</b> Its children want about 970 pixels on
     one line; below that the two acts go to a second line, still at the right-hand end. The
     breadcrumb's basis is zero so that it is the thing that gives way on the first line, which is
     the order `.symstrip .crumbs` already sets for the wide strip. No vertical padding: Store is
     44 pixels and the strip's own minimum, so on one line it touches both edges in the wide
     window too — padding added here made the one-line strip 12 pixels taller than the wide one. */
  .symstrip { flex-wrap: wrap; row-gap: 6px; }
  .symstrip .crumbs { flex: 1 1 0; }
}

/* <b>Under 760: one column, and the picture is pinned to the top of it.</b> The picture beside a
   column of controls would be under 424 pixels here, which is back to ADR-051's thumbnail. So the
   editor becomes one scrolling column — picture, renderer, inspector, in the order they read
   across a wide window — and the picture is `sticky`, so that scrolling down to a control does
   not scroll away the thing the control changes. The inspector is sized to fill what the picture
   leaves, which puts the class list and the symbol stack on the screen together when it is
   scrolled to, as they are in the wide window. */
@container symeditor (max-width: 759px) {
  /* <b>`scroll-padding-top`, because a pinned picture is a pinned header.</b> Anything scrolled
     to from below lands at the top of the scrollport, which is under the picture. The page does
     that itself — *Add a class* calls `scrollIntoView({ block: "nearest" })` on the new row —
     and measured at 768 by 900 a card brought into view that way sat at 177 pixels under a
     picture ending at 480; with the padding it sits at 481. Chrome centres a focused control,
     which happens to clear the picture; a browser that aligns focus to the edge would not. */
  .symcols {
    display: flex; flex-direction: column; overflow-y: auto;
    scroll-padding-top: max(220px, 42%);
  }
  .symcols > * { flex: none; }

  .symcols > .sympreview {
    order: -1; position: sticky; top: 0; z-index: 3;
    height: 42%; min-height: 220px; border-bottom: 1px solid var(--rule);
  }
  .symcols > .symrail { overflow: visible; border-top: 0; border-right: 0; }
  .symcols > .syminsp { height: 58%; min-height: 360px; border-left: 0; border-top: 1px solid var(--rule); }

  /* The generated screen reads first, and the layer list follows it. */
  .symcols > .symempty { order: -1; border-left: 0; border-bottom: 1px solid var(--rule); }
  .symcols.generated > .symrail { border-right: 0; }

  /* The state sentence is the one child of the strip that may shorten; Store and *Back to
     generated* do not. */
  .symstrip .symdo { flex: 0 1 auto; min-width: 0; }
  .symstrip .symstate { flex: 0 1 auto; min-width: 0; }
}

/*
  <b>Visibly out of use rather than gone.</b> When the advanced connection string is filled in
  it is what gets sent, and the fields above it are ignored — a design review found that
  happening silently on 2026-09-06, with a Test failing against a host nowhere on screen. The
  fields are disabled and dimmed instead of hidden, because somebody who typed a host into them
  wants to see it still there when they empty the advanced box again.
*/
label.overridden { opacity: .5; }
label.overridden input { background: var(--surface-2); cursor: not-allowed; }

/* ------------------------------------------------------------------- the composer */
/*
  <b>Three columns, and the middle one is not a map.</b> ADR-057's study drew a preview
  between Contents and Databases; nothing on this server turns an unpublished composition
  into a picture, so what sits there is the service that will exist rather than a control
  for a feature that does not — ADR-034's rule applied to our own design.
*/
/*
  <b>Bounded, since 2026-09-06, and the unbounded version was the whole complaint.</b> The three
  panes were `min-height: 420px` with no ceiling, so the tallest one set the height of the row —
  and the tallest is Databases, which lists every table in every registered database. Sixty
  tables made the page four screens long, the contents pane a strip at the top of an empty
  column, and the preview a 900-pixel drawing adrift in seventeen hundred pixels of nothing. The
  owner's words were *arayüz çok kötü durumda*, and this is what they were looking at.

  <b>The row is a viewport height minus what is above it, and each pane scrolls its own
  contents.</b> A list that can be any length has to scroll inside something; the alternative is
  the page growing to fit the largest database anybody registers, which is not a layout decision
  so much as the absence of one.
*/
.composer {
  display: grid; grid-template-columns: 320px minmax(0, 1fr) 340px; gap: 12px;
  align-items: stretch;
  height: calc(100vh - 232px); min-height: 460px;
}
@media (max-height: 780px) { .composer { height: auto; } }

.composerpane { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 0; }
.composerhead {
  display: flex; align-items: baseline; gap: 8px; padding: 9px 12px;
  border-bottom: 1px solid var(--rule); background: var(--surface-2);
}
.composerhead b {
  font-size: var(--t-tiny); letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
}
.composerbody { flex: 1; overflow: auto; padding: 6px 0 12px; }

.pubempty {
  margin: 18px 12px; padding: 26px 14px; text-align: center; color: var(--faint);
  border: 1px dashed var(--rule-strong); border-radius: 4px; font-size: var(--t-small);
}
#pubContents.dropping .pubempty { border-color: var(--accent); color: var(--accent); }

.pubnode { margin: 0 8px; border-radius: 3px; }
.pubnode + .pubnode { margin-top: 1px; }
.pubnode.dragging { opacity: .45; }
.pubnode.over { box-shadow: inset 0 2px 0 var(--blue); }
.pubnode.into .pubrow { box-shadow: inset 0 0 0 2px var(--accent); }
.pubnode.sel > .pubrow { background: var(--blue-soft); box-shadow: inset 0 0 0 1px var(--blue-line); }
.pubrow {
  display: flex; align-items: center; gap: 7px; padding: 5px 8px; border-radius: 3px;
  cursor: grab; user-select: none; font-size: var(--t-small);
}
.pubrow:hover { background: var(--surface-2); }
/* <b>The name takes the room and the badge gives it up.</b> Every child of the row had its
   natural width, so `tm30native_ee39df8f` was clipped to `tm30native_ee39…` beside three
   hundred pixels of empty column — the ellipsis was doing its job on a box nothing had sized.
   The title attribute stays for the case where the column really is narrow. */
.pubrow .pubname { flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pubrow .pubsr { margin-left: auto; flex: 0 0 auto;
  font: var(--t-tiny)/1 var(--mono); color: var(--faint); }
.pubrow .pubkill { border: 0; background: transparent; color: var(--faint); padding: 0 4px;
  cursor: pointer; font-size: 13px; }
.pubrow .pubkill:hover { color: var(--alert-dot); }
.pubnode.grouped .pubrow { margin-left: 20px; }
.pubgroupname { font-weight: 600; }
.pubcount {
  font: var(--t-tiny)/1 var(--mono); color: var(--faint); background: var(--surface-3);
  border-radius: 8px; padding: 2px 6px;
}

.pubdb, .pubschema, .pubtable {
  display: flex; align-items: center; gap: 7px; padding: 5px 8px; border-radius: 3px;
  font-size: var(--t-small); user-select: none;
}
.pubdb { font-weight: 600; cursor: pointer; margin: 0 8px; }
.pubschema { padding-left: 24px; color: var(--muted); cursor: pointer; margin: 0 8px; }
.pubtable { padding-left: 40px; cursor: grab; margin: 0 8px; }
.pubdb:hover, .pubschema:hover, .pubtable:hover { background: var(--surface-2); }
/* <b>`.used` went with the rule it drew.</b> A table served by another service is an ordinary
   table here since migration 40 — owner decision 2026-09-06. What is left is the one thing that
   really cannot become a layer: no geometry column, or no integer this server can use as an
   object id. */
.pubtable.no { cursor: not-allowed; color: var(--faint); }
/* <b>The name gives way, not the pane.</b> `ci_earlyalert_reports_08d1a14c` beside a reference
   badge is wider than a 340-pixel column, and with nothing allowed to shrink the row pushed a
   horizontal scrollbar across the whole list — so reading the table names meant scrolling
   sideways through sixty rows. The name ellipsises and keeps its full text in the title. */
.pubtable > span:nth-child(2) { flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pubtable .pubsrid { margin-left: auto; flex: 0 0 auto;
  font: var(--t-tiny)/1 var(--mono); color: var(--faint); }
/* <b>Point, line or area, beside every table.</b> Owner instruction 2026-09-06. The same three
   shapes the contents tree draws for a layer's symbol, at the same size, so the pane and the
   tree read as one screen — but in the pane's own grey, because here the shape is the fact and
   a colour would imply a symbol nobody has chosen. A table with no geometry gets none of the
   three: it is not a feature class, and the row beneath it says so in words. */
.pubgeom { flex: none; width: 11px; height: 11px; }
.pubgeom.dot { width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); opacity: .75; }
.pubgeom.line { height: 0; width: 12px; border-top: 2px solid var(--accent); opacity: .75;
  transform: rotate(-20deg); }
.pubgeom.fill { border-radius: 2px; background: var(--accent-soft);
  border: 1.5px solid var(--accent); opacity: .8; }

/* Not publishable at all: the shape stays, so the row still says what the table holds, and the
   colour goes, so it does not read as an offer. */
.pubgeom.no.dot, .pubgeom.no.fill { background: var(--rule); border-color: var(--rule-strong); }
.pubgeom.no.line { border-top-color: var(--rule-strong); }
.pubwhy { padding: 0 8px 6px 54px; font-size: var(--t-tiny); color: var(--warn); line-height: 1.45; }


@media (max-width: 1180px) {
  .composer { grid-template-columns: 1fr; }
}

/* ADR-063 — the layer's Fields page. The label box fills its cell so a long label is readable
   while it is typed; the orphaned overrides are a plain list with their Remove beside them. */
#page-fields td input[type=text] { width: 100%; min-width: 8ch; text-overflow: ellipsis; }

/* Fixed widths for the three columns whose content is short, so Label keeps its share down to
   ~980px instead of being squeezed by a long column name — design review 2026-09-11. A long
   name wraps inside its cell rather than widening the table. */
#page-fields .fieldsgrid { table-layout: fixed; width: 100%; }
#page-fields .fieldsgrid col.c-name  { width: 19%; }
/* "SmallInteger" is the longest type name and does not wrap (below); 11% holds it at 1280. */
#page-fields .fieldsgrid col.c-type  { width: 11%; }
/* ADR-065: the Values button says what the column allows in a few words ("List of 12", or a date
   range, which wraps to two lines rather than being cut — design review 2026-09-12 measured every
   summary truncated at 1280 and the date range at 1440 when this was 13% and never wrapped). */
#page-fields .fieldsgrid col.c-values { width: 15%; }
/* ADR-064: what a column records about edits. A native select cuts its value with no ellipsis,
   so this is sized for the longest option, "Created on", at ~1100px — design review 2026-09-11,
   which found the first labels cut at 1440. Label keeps 24%. */
#page-fields .fieldsgrid col.c-records { width: 18%; }
#page-fields .fieldsgrid col.c-hide  { width: 14%; }
/* The label column keeps the rest, 23%. The name column went to 17% for a first draft of this and
   "material" broke mid-word at 1024 — the rule above is that only a long name may break. */
#page-fields .fieldsgrid select { width: 100%; }
/* Only a long column name may break; a type name broken mid-word ("Stri ng") reads as two. */
#page-fields .fieldsgrid td code { overflow-wrap: anywhere; }
#page-fields .fieldsgrid td:nth-child(2) { white-space: nowrap; }
/* The reason under a locked box: the page's own muted prose, not the monospace of a value. */
#page-fields .lockwhy { display: block; color: var(--muted); font-size: var(--t-small);
  line-height: 1.3; margin-top: 2px; }
#fieldsInert ul { list-style: none; padding: 0; margin: 6px 0 0; }
#fieldsInert li { display: flex; gap: 8px; align-items: baseline; padding: 4px 0; }

/* ADR-065 — values and subtypes on the Fields page. The editor and a subtype's detail are panels
   under what opened them, bounded in width so a two-column list does not stretch across a wide
   window; a Values button that has something set reads in ink rather than muted. */
#page-fields .valuesbtn { max-width: 100%; white-space: normal; text-align: left; overflow-wrap: anywhere;
  line-height: 1.3; }
/* The editor row spans the table and sits flush under the row that opened it. */
#page-fields tr.valuesrow > td { padding-top: 0; }
#page-fields .valuesedit .setting input { width: 180px; }
/* A name is prose: left-aligned, proportional, as wide as the panel allows — not the right-aligned
   monospace box `.setting` gives a number with a unit, which is what it inherited and why the
   review found "Pipe material" cut in a 116px box. */
#page-fields .valuesname { display: flex; flex-direction: column; gap: 4px; margin: 0 0 var(--gap-2);
  max-width: 420px; }
#page-fields .valuesname input { width: 100%; }
#page-fields .valuesnote { margin: var(--gap-2) 0 var(--gap-2); }
#page-fields .valuesbtn.set { color: var(--ink); }
#page-fields .valuesedit,
#page-fields .subtypedetail { border: 1px solid var(--rule); border-radius: var(--round);
  padding: var(--gap-3); margin: var(--gap-3) 0; max-width: 720px; background: var(--surface); }
#page-fields .valueskind { border: 0; padding: 0; margin: 0 0 var(--gap-2); display: flex;
  flex-wrap: wrap; gap: var(--gap-3); }
#page-fields .valueskind label { display: inline-flex; align-items: center; gap: 6px; }
#page-fields .valuesgrid,
#page-fields .subtypesgrid { width: 100%; max-width: 720px; }
#page-fields .valuesgrid input,
#page-fields .subtypesgrid input[type=text],
#page-fields .subtypesgrid input[type=number] { width: 100%; min-width: 6ch; }
#page-fields .subtypesgrid td.actions { white-space: nowrap; }
#page-fields .subtypesgrid tr.open td { background: var(--surface-2); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* ADR-066: the kind of source, chosen before any field, and the files a probe could not offer. */
#dbconn .dckind { border: 0; padding: 0; margin: 0 0 var(--gap-3); display: flex; flex-wrap: wrap;
  gap: var(--gap-2) var(--gap-4); }
#dbconn .dckind legend { font-weight: 600; margin-bottom: var(--gap-1); padding: 0; }
#dbconn .dckind label { display: inline-flex; align-items: center; gap: 6px; }
#dbconn .dckind .hint { flex-basis: 100%; margin: 0; }
/* ADR-067: a checkbox sits beside its words, not under an uppercase caption like a text box. */
#dbconn label.field.check { flex-direction: row; align-items: center; gap: 6px; align-self: flex-end;
  padding-bottom: 8px; text-transform: none; letter-spacing: 0; font-size: var(--t-body); color: inherit;
  font-weight: 400; }
#dbconn #dcS3 { margin-top: var(--gap-2); }

/* ADR-067: an s3:// prefix is long and has no spaces, and it widened the Sources table past the page
   until it broke somewhere. Measured 2026-09-13 with Overture's prefix: 1,377 pixels in a 1,280 window. */
#sources td.val { overflow-wrap: anywhere; }
#dbconn #dcS3 summary { cursor: pointer; }
.dcskipped { margin: var(--gap-2) 0 0; padding-left: 1.2em; }
.dcskipped li { margin: 2px 0; }

/* Server warnings (V-33): no caller and no address, so those two columns are not drawn, and the message
   is one line with the whole of it on hover and in the detail row. */
table.logs-server th:nth-child(3), table.logs-server th:nth-child(4),
table.logs-server tr.logrow td:nth-child(3), table.logs-server tr.logrow td:nth-child(4) { display: none; }
td.logmessage span[title] { display: inline-block; max-width: 60ch; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: bottom; }

/*
  <b>The History page — ADR-078.</b> The changes, and under or beside them the chosen feature's versions.

  <b>Side by side only when the page itself is wide enough, measured on the page and not the window.</b>
  The first version switched at a 1100-pixel viewport; the review measured the page area at about 730
  pixels in a 1280 window, once the shell and the editor's own navigation are taken, and the list spilled
  under the versions and hid who made each change. A container query asks the page.
*/
#page-history { container-type: inline-size; }
.histsplit { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--gap-5);
             align-items: start; margin-top: var(--gap-3); }
@container (min-width: 1000px) {
  .histsplit { grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); }
}
.histlist { overflow-x: auto; }
.histgrid td:first-child { white-space: nowrap; }
.histlegend { margin-top: var(--gap-2); }
.histfeature h5 { margin: 0 0 var(--gap-2); font-size: var(--t-body); }
.histversion { border: 1px solid var(--rule); border-radius: var(--round); padding: var(--gap-3);
               margin-bottom: var(--gap-2); background: var(--surface); }
.histversion .vhead { font-size: var(--t-small); display: flex; flex-wrap: wrap; gap: 2px var(--gap-3);
                      align-items: baseline; }
.histversion .vhead b, .histversion .vhead .until { white-space: nowrap; }
.histversion button { margin-top: var(--gap-2); }
.histattrs { font-size: var(--t-small); width: auto; }
.histattrs th { text-align: left; font-weight: 500; color: var(--muted); width: 1%; white-space: nowrap;
                padding: 2px var(--gap-3) 2px 0; vertical-align: top; text-transform: none;
                letter-spacing: normal; background: none; border-radius: 0; font-size: var(--t-small); }
.histattrs td { overflow-wrap: anywhere; padding: 2px 0; }
.histattrs del { color: var(--muted); }
#page-history .tag { display: inline-block; font-size: 11.5px; line-height: 1.5; padding: 0 6px;
                     border: 1px solid var(--rule-strong); border-radius: 999px; color: var(--muted);
                     vertical-align: 1px; white-space: nowrap; }

/* <b>The Settings screen's card — V-70.</b> A setting row stops at its own width, so a card as wide as
   the page left 424 pixels between a label and its box (design review 2026-09-24). */
.settingcard { max-width: 760px; }

/* A label over its box at phone width, rather than a label broken over two lines beside it. */
@media (max-width: 560px) {
  .setting { flex-wrap: wrap; }
  .setting .q { flex-basis: 100%; }
}
