/* The single stylesheet for the whole template: plain CSS, no framework, no
   build step, no external fonts, so a project built on this has nothing to
   install. Colours and spacing are custom properties on :root, redefined for
   dark mode, so the whole site restyles from the block at the top. */

/* -----------------------------------------------------------------------------
   1. Custom properties
   -------------------------------------------------------------------------- */

:root {
    /* Neutral surfaces */
    --colour-page-background:      #f5f6f8;
    --colour-card-background:      #ffffff;
    --colour-header-background:    #ffffff;
    --colour-input-background:     #ffffff;
    --colour-input-text:           #1c2230;
    --colour-input-border:         #b9c0cb;
    --colour-border:               #d8dce3;
    --colour-border-strong:        #b9c0cb;

    /* Text */
    --colour-text-primary:         #1c2230;
    --colour-text-secondary:       #5b6472;
    --colour-text-inverse:         #ffffff;
    --colour-heading-text:         #1c2230;

    /* The wordmark has colours of its own rather than borrowing the header and
       quiet-text ones. It is the one piece of text that routinely sits on top
       of a picture, so it needs to be legible against a banner without forcing
       every subtitle and help note on the site to the same colour. */
    --colour-site-name:            #1c2230;
    --colour-site-byline:          #5b6472;

    /* A SURFACE AND THE TEXT ON IT ARE ONE DECISION, so they are declared as a
       pair and edited as a pair. Styling mode will not let a background be
       changed without checking the text on it is still readable, which is only
       possible because it knows which text belongs to which surface. */
    --colour-module-background:    #f5f6f8;
    --colour-module-text:          #1c2230;
    --colour-card-text:            #1c2230;
    --colour-table-row-background: #ffffff;
    --colour-table-row-text:       #1c2230;
    --colour-header-text:          #1c2230;

    /* THE MENU BUTTON'S OWN SURFACE, which is what groups the module's name and
       the visitor's picture into one control rather than two things that happen
       to be side by side. Offerable in the styling manager, so an administrator
       can hand it to visitors: see the 'headerMenuButton' target in
       includes/style_service.php.

       A NEUTRAL TRANSLUCENT WASH, FOR THE SAME REASON THE HOVER SURFACE IS ONE.
       The header's colour is not knowable from here -- the presets alone put it
       at cream, white, deep green, slate and two different near blacks -- so any
       opaque default would be right on one of them and wrong on the rest, and
       every preset would have to remember a colour whose effect it cannot see.
       Half way between black and white lightens a dark header and darkens a
       light one with one value, needs no dark mode counterpart, and shows
       through as a tint of whatever is behind it.

       AN ADMINISTRATOR OR VISITOR WHO SETS ONE GETS AN OPAQUE COLOUR, because
       the picker is an <input type="color"> and the save path takes six digit
       hex only. That is the intended trade: the default belongs to the header
       it is drawn on, and a chosen colour belongs to whoever chose it. */
    --colour-header-menu-button-background: rgba(127, 127, 127, 0.14);

    /* THE DROPDOWN IS A PANEL, NOT A PIECE OF THE HEADER BAR, and it took the
       header's text colour for a long time after it stopped being one. The rows
       used to sit in the bar itself, where --colour-header-text is the one
       colour certain to be readable; they are on a card-coloured panel hanging
       below it now, and on any theme with a dark header over light cards that
       made them very nearly invisible -- Old Way printed #f2fbf6 on #ffffff,
       which is a contrast ratio of 1.06 to 1.

       THEY FOLLOW THE CARD UNTIL SOMEBODY SETS THEM. A panel that looks like
       every other panel on the site is the right default, and it is also the
       only default that cannot be wrong: whatever a preset does to cards, this
       does too. */
    --colour-header-menu-background: var(--colour-card-background);
    --colour-header-menu-text:       var(--colour-card-text);

    /* Follows the header's text until somebody sets it, so changing the text on
       the header still moves the text on the button. It is separately styleable
       because the button now has a surface of its own: the editor pairs the two
       and moves this to black or white if a chosen background would leave the
       name unreadable, and it must be able to do that without repainting the
       site name beside it. */
    --colour-header-menu-button-text: var(--colour-header-text);

    /* Accent used by primary buttons and links.
       The two derived from it are given fixed fallbacks here and re-derived
       below, so a visitor who picks a new accent in styling mode gets matching
       hover colours rather than the template's blue turning up under their
       green. */
    --colour-accent:               #2f5bd7;
    --colour-accent-hover:         #24499f;

    /* The colour a link goes on hover. Its own property rather than the accent
       hover, so recolouring links does not drag every button's hover with it. */
    --colour-link-hover:           #24499f;
    --colour-accent-soft:          #e8eefc;
    --colour-link:                 #2f5bd7;
    --colour-button-text:          #ffffff;

    /* Status colours */
    --colour-success:              #17663f;
    --colour-success-soft:         #e2f4ea;
    --colour-error:                #a3232b;
    --colour-error-soft:           #fbe7e8;
    --colour-info:                 #1f5673;
    --colour-info-soft:            #e5f1f7;

    /* Spacing scale */
    --space-extra-small:  0.25rem;
    --space-small:        0.5rem;
    --space-medium:       1rem;
    --space-large:        1.5rem;
    --space-extra-large:  2.5rem;

    /* Shape and depth */
    --radius-small:  6px;
    --radius-medium: 10px;

    /* ONE INSET FOR EVERY CONTAINER. A card, a product tile and the page
       heading panel sitting beside each other with three different insets do
       not read as a set, so they take this one number and change together.

       Slim strips take half of it rather than a second number of their own, so
       the relationship between them stays visible when it changes. Every inset
       on the site is one of these two, which is what makes the page read as a
       grid rather than as a pile of separately decided boxes. */
    --container-padding: 24px;
    --container-padding-tight: calc(var(--container-padding) / 2);

    /* THE THREE INSETS OF THE HEADER'S MENU, nested one inside the next: the
       space around the button, the space inside it, and the space either side of
       the name within that. All three are set in site edit mode -- they are on
       the Menu target in includes/layout_targets.php -- so they are declared
       here rather than on .site-header. A chosen value arrives on :root:root and
       is inherited; a declaration further down would beat it for the whole
       header and the setting would do nothing.

       ZERO IS A REAL ANSWER FOR TWO OF THEM. The button sits against the bar's
       own inset and the name against the button's, which is what the header
       looked like before any of this was settable, so nobody who never opens the
       setting sees a change. */
    --site-header-menu-padding:        0px;
    --site-header-menu-button-padding: 5px;
    --site-header-menu-name-padding:   0px;

    /* AND THE THING ALL THREE ARE MEASURED AROUND. The picture on the menu
       button is the innermost box in the header, and every height above it is
       now worked out from it: the button is this plus its own inset, the menu is
       the button plus its own, the bar is the menu plus its own, and the header
       is the bar. Set it here and the whole strip resizes in proportion.

       IT USED TO BE THE OTHER WAY UP, with a declared button height and the
       picture taking whatever was left inside it. That was already an inversion
       of an earlier arrangement, and it stopped making sense the moment the
       picture itself became something to choose: a setting for the size of a
       thing that is a leftover is a setting for nothing. */
    --site-header-avatar-size: 54px;

    /* HOW WIDE THE BUTTON IS, FULL STOP. It used to be as wide as its own label
       with a cap that only applied on a narrow window, which meant the button
       changed width as the visitor moved between modules -- "Main" and
       "Administration" are not the same number of letters, and the branding
       beside it jumped every time. One declared width holds the bar still.

       IT IS ON THE BUTTON, NOT THE BOX AROUND IT. .site-header__menu carries an
       inset of its own, so a width there would be the button plus that inset and
       the number would not mean what it says. */
    --site-header-menu-button-width: 250px;
    --shadow-card:   0 1px 2px rgba(16, 24, 40, 0.06),
                     0 8px 24px rgba(16, 24, 40, 0.06);

    /* Typography */
    --font-family-base: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo,
                        Consolas, monospace;
    --font-family-heading: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;

    /* The two sizes the style editor can change. Everything else is relative
       to them, so moving either rescales the page as a whole. */
    --font-size-base:    16px;

    /* Buttons and card padding, sized on their own so that making the buttons
       bigger does not mean making all the text bigger. */
    --font-size-button:  16px;
    --font-family-input: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    --font-size-input:   16px;
    --font-size-heading: 30px;

    /* Subtitles, help text and anything marked secondary. Most of the prose on
       the site is this rather than the base size, so it gets its own control in
       styling mode instead of being tied to the body size. */
    --font-size-quiet:   14px;

    /* The wordmark's own font and sizes. The stack is written out to match the
       'System default' entry in AVAILABLE_FONT_STACKS exactly, so the style
       editor's font menu opens on the right option instead of on nothing. */
    --font-family-wordmark:   system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    --font-size-site-name:    18px;
    --font-size-site-byline:  14px;

    --layout-maximum-width: 62rem;


    /* The header pictures and their sizes. Site edit mode overrides these
       through the nonced block in the head; the values here are what a site
       that has uploaded nothing uses, and they must exist for the header to
       have anything to resolve. The two lengths match what
       includes/site_settings_service.php works out from its default
       percentages, which is 10% and 30% of the 512 pixel maximum. */
    --site-logo-image:   none;
    --site-banner-image: none;
    --site-logo-size:    51px;
    --site-banner-height: 154px;
    --site-banner-width: 100%;

    /* Which part of a cropped picture stays in view. Dragging the picture in
       site edit mode is what moves these. */
    --site-logo-position:   50% 50%;
    --site-banner-position: 50% 50%;
}

/* The same palette, adjusted for visitors whose system asks for dark mode. */
@media (prefers-color-scheme: dark) {
    :root {
        --colour-page-background:  #14161c;
        --colour-card-background:  #1c1f27;
        --colour-header-background: #1c1f27;
        --colour-input-background: #23272f;

        /* THE TEXT IN AN INPUT AND ITS BORDER, which this block used to change
           the background of and nothing else. The light values stayed, so a
           near black #1c2230 was drawn on a near black #23272f: 1.06:1, which
           is not low contrast so much as no contrast. A surface and the text
           on it are one decision and have to move together. */
        --colour-input-text:       #eef0f4;
        --colour-input-border:     #4a5160;

        --colour-border:           #333844;
        --colour-border-strong:    #4a5160;

        --colour-text-primary:     #eef0f4;
        --colour-text-secondary:   #a5adbb;
        --colour-text-inverse:     #10131a;
        --colour-heading-text:     #ffffff;
        --colour-header-text:      #eef0f4;
        --colour-site-name:        #eef0f4;
        --colour-site-byline:      #a5adbb;
        --colour-module-background: #14161c;
        --colour-module-text:       #eef0f4;
        --colour-card-text:         #eef0f4;
        --colour-table-row-background: #1c1f27;
        --colour-table-row-text:    #eef0f4;

        --colour-accent:           #7f9dfa;
        --colour-accent-hover:     #a2b8ff;
        --colour-link-hover:       #a2b8ff;
        --colour-accent-soft:      #232c44;
        --colour-link:             #7f9dfa;
        --colour-button-text:      #10131a;

        --colour-success:          #6fd6a3;
        --colour-success-soft:     #17301f;
        --colour-error:            #ff9ba1;
        --colour-error-soft:       #3a1f21;
        --colour-info:             #8ecbe8;
        --colour-info-soft:        #162b36;

        --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4),
                       0 8px 24px rgba(0, 0, 0, 0.35);
    }
}

/* -----------------------------------------------------------------------------
   1b. Hover colours, derived from the accent
      Everywhere in this file, a hover is one of two things: a filled surface
      that darkens, or a quiet surface that picks up a wash of the accent. Both
      are worked out from --colour-accent rather than being separate colours, so
      changing the accent changes the hovers with it.

      color-mix is a few years old, so the fixed values above stand in for it on
      an older browser: the hovers there are simply the template's own blue,
      which is the behaviour this file had before.
   -------------------------------------------------------------------------- */

@supports (color: color-mix(in srgb, red 50%, blue)) {
    :root {
        /* A darker accent for text and filled surfaces. */
        --colour-accent-hover: color-mix(in srgb, var(--colour-accent) 82%, #000000);

        /* A wash of the accent. Mixing with transparent rather than white means
           it works on a white card and a dark one alike. */
        --colour-accent-soft: color-mix(in srgb, var(--colour-accent) 14%, transparent);
    }

    /* Dark mode lightens on hover instead of darkening, because a dark surface
       has nowhere darker to go. */
    @media (prefers-color-scheme: dark) {
        :root {
            --colour-accent-hover: color-mix(in srgb, var(--colour-accent) 78%, #ffffff);
            --colour-accent-soft: color-mix(in srgb, var(--colour-accent) 22%, transparent);
        }
    }
}

/* -----------------------------------------------------------------------------
   1b-ii. The header hovers on its own colours, not the page's
      THE HEADER IS THE ONE STRIP THAT NEED NOT MATCH THE PAGE. A preset is free
      to give it a dark background while the page stays light -- Old Way sets a
      deep green header over a pale page -- and the moment it does, the accent
      derived hovers above stop working there. They are built for a light
      surface: --colour-accent-hover darkens the accent, and outside dark mode
      nothing tells it the surface under it is already dark.

      Old Way made this exact shape. Its accent is #047857, so a hovered header
      link asked for #036347 text on a surface that composited to #066348: the
      same colour twice, at a contrast ratio of about 1 to 1. The link did not
      dim on hover, it disappeared.

      SO THE HEADER DERIVES ITS HOVER FROM --colour-header-text INSTEAD, which is
      the one colour guaranteed to contrast with the header background, because
      it is what the header already prints on it. Dark header, light wash; light
      header, dark wash; and no preset has to remember to set a fourth variable
      it cannot see the effect of.

      A FLAT GREY WASH RATHER THAN A MIX OF THE TEXT COLOUR. Deriving it with
      color-mix was tried first and withdrawn: mixing towards `transparent`
      computed to a fully transparent colour here rather than to the text colour
      at sixteen percent, so the wash disappeared and the hover had no surface
      at all. A neutral grey needs no mixing, no @supports guard and no browser
      new enough for either, and being half way between black and white it
      lightens a dark header and darkens a light one with the same value.
   -------------------------------------------------------------------------- */

/* THE HEADER STACKS ABOVE THE PAGE, STICKY OR NOT.

   The menu's dropdown hangs off the header and carried a z-index of 900, which
   sounds like plenty and was not: it only ranks against its siblings inside the
   header, and the header itself only had a z-index while it was sticky -- 800,
   set for a different reason. So the whole menu entered the page's stacking
   order at 800 at best and at the header's document position at worst, and
   anything on the page that made its own stacking context could be painted over
   the top of an open menu.

   WHERE 1050 SITS, and why not higher. The order this file keeps is:

       page content          below 800
       a dragged category    1000   (category_tree.js, on the body)
       the header and menu   1050   <- here
       the saved toast       1100
       tooltips and modals   1200

   Above everything the menu can be opened over, and below the three things that
   are meant to cover it. A modal covering the header is correct: it is the only
   thing on screen while it is open.

   position: relative IS WHAT MAKES z-index MEAN ANYTHING. Static elements ignore
   it, which is the other half of why 900 on the dropdown was doing so little. */
.site-header {
    --colour-header-hover-surface: rgba(127, 127, 127, 0.18);

    position: relative;
    z-index: 1050;
}

/* -----------------------------------------------------------------------------
   1c. How hovering works, everywhere
      Every hover in this file is one of three things, and nothing invents a
      fourth:

        FILLED     a button or a link styled as one. It darkens: the accent goes
                   to --colour-accent-hover, and a surface in another colour uses
                   filter: brightness() so it follows its own base colour.

        QUIET      a link, a menu item, a tile, a table row, a small control. It
                   takes a wash of --colour-accent-soft, and if it has a border,
                   that border becomes --colour-accent.

        TEXT ONLY  a link in a run of prose. It moves to --colour-accent-hover
                   and underlines. Nothing else changes, because a background
                   appearing mid-sentence is noise.

      A quiet element takes whichever half of the treatment suits it. A tile or a
      menu item takes both the wash and the border; a text input takes the border
      only, because a wash behind text somebody is about to type is a distraction
      rather than a hint; a table row takes the wash only, having no border to
      change.

      All three follow --colour-accent, so a visitor who restyles the site in
      styling mode restyles its hovers too.

      THE TRANSITION below is deliberately short and applies to colour only.
      Anything with a movement or a fade of its own is left out of it: the
      tooltip layer, the collapsed menu and its items, and the development
      banner all animate on their own terms and would fight a second rule.
   -------------------------------------------------------------------------- */

a,
.button,
.site-header__link,
.site-header__branding,
.site-header__menu-button,
.admin-navigation__link,
.module-toolbar__screen,
.form-field__input,
.checkbox-grid__item,
.avatar-picker__option,
.data-table tbody tr {
    transition: background-color 120ms ease-out,
                border-color 120ms ease-out,
                color 120ms ease-out;
}

/* The one exception among them: the menu button already animates its corners
   when the menu opens, so its own transition is extended rather than replaced.
   Its ring is an inset shadow rather than a border, so that is what fades. */
.site-header__menu-button {
    transition: background-color 120ms ease-out,
                background-image 120ms ease-out,
                box-shadow 120ms ease-out,
                color 120ms ease-out,
                border-radius 110ms ease-out;
}

/* -----------------------------------------------------------------------------
   2. Base elements and page skeleton
   -------------------------------------------------------------------------- */

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

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

    /* NOTHING ON THE PAGE IS FOR HIGHLIGHTING. Dragging across a heading or a
       row selects it in blue, which on a site made of clickable rows and
       draggable tiles is almost always a gesture that missed rather than one
       anybody meant. The exceptions are listed immediately below, and they are
       the whole of the exceptions.

       INHERITED, NOT APPLIED TO EVERYTHING. user-select inherits, so one
       declaration here reaches the whole document and a child can take it back
       for itself. A `*` selector would set it on every element directly and the
       list below would have to fight the same specificity to undo it.

       THIS IS A COMFORT, NOT A PROTECTION. Text nobody can drag over is still
       in the markup, still readable by any reader or extension, and still
       selectable from the keyboard in several browsers. Anybody who wants the
       words has them; this is only about the blue smear. */
    -webkit-user-select: none;
    user-select: none;
}

/* WHAT YOU CAN STILL SELECT, AND WHY EACH ONE HAS TO BE HERE.

   FIELDS, BECAUSE THEY HOLD YOUR OWN WORDS. Typing survives user-select: none,
   but correcting it does not: no double click to take a word, no drag to
   replace a line, and no way to copy back out something you had just entered.
   A confirmation code somebody wants to check against their email is the plain
   case.

   CODE, BECAUSE IT EXISTS TO BE COPIED. Every .code-block on the site is a
   sample -- the fenced blocks the wiki renders, the manifest examples on the
   Modules screen, and the deployment lines on the Appearance screen, which sit
   under the words "Paste these into config/config.php". A block of configuration
   somebody cannot copy is a block of configuration they have to retype.

   AND INLINE CODE, WHICH IS MOST OF WHAT ANYBODY ACTUALLY COPIES. The wiki names
   functions and files in running prose far more often than it prints a whole
   block of them, so getDataStoreDirectoryPath() or config/config.php in the
   middle of a sentence is usually the thing somebody is reaching for. Bare
   `code` covers the inline ones and the ones inside a block together, which is
   why the block no longer needs a selector for its own child. */
input,
textarea,
select,
[contenteditable="true"],
.code-block,
code {
    -webkit-user-select: text;
    user-select: text;
}

/* THE PAGE ITSELF DOES NOT SCROLL. The window is divided once, into a header,
   a middle and a footer, and only the middle scrolls. That is what keeps the
   header and footer still while reading, keeps them at the full width of the
   window rather than losing a slice to a scrollbar, and keeps the module
   content between them instead of running underneath.

   100dvh rather than 100vh because on a phone the browser's own toolbars
   appear and disappear as you scroll, and 100vh measures the window as if they
   were never there, which puts the footer permanently just off the bottom of
   the screen. The vh line before it is for the few browsers that do not know
   dvh, where being a toolbar too tall is the lesser fault. */
body {
    margin: 0;
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    display: flex;
    flex-direction: column;

    font-family: var(--font-family-base);
    font-size: var(--font-size-base);
    line-height: 1.55;
    color: var(--colour-text-primary);
    background-color: var(--colour-page-background);
}

a {
    color: var(--colour-link);
    text-decoration: none;
}

/* Headings share their own colour, font and size, so the style editor can
   change them without touching body text. */
h1, h2, h3, h4,
.page-title,
.card__title {
    color: var(--colour-heading-text);
    font-family: var(--font-family-heading);
}

a:hover,
a:focus-visible {
    color: var(--colour-link-hover);
    text-decoration: underline;
}

/* One visible focus ring for every interactive element, for keyboard users. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
    outline: 3px solid var(--colour-accent);
    outline-offset: 2px;
}

/* THE ONE SCROLLING BOX ON THE PAGE.

   It is full width rather than a centred max-width box, so its scrollbar sits
   against the edge of the window where people expect to find it, and the
   content is centred with padding instead. The padding is the same sum the
   header uses for its gutter, so the two line up: half the space left over once
   a box of the maximum width is centred, plus the padding itself.

   min-height: 0 is not decoration. A flex item will not shrink below the size
   of its own content without it, so the box would grow to fit the page and
   nothing would ever scroll.

   scrollbar-gutter reserves the scrollbar's width whether or not there is
   anything to scroll, which stops the content shifting sideways by a few pixels
   as you move between a short page and a long one. */
.page-main {
    --content-maximum-width: var(--layout-maximum-width);
    /* THE LARGER OF WHAT AN ADMINISTRATOR ASKED FOR AND WHAT CENTRING LEAVES.
       On a wide screen the column is centred and the second term wins; on a
       phone there is nothing to centre and the first is the whole gutter.

       --content-gutter-minimum is set in site edit mode. The fallback is what
       this was before it was settable, so a site nobody has edited is unchanged
       and a browser that never receives the token still lays out. */
    --content-gutter: max(
        var(--content-gutter-minimum, var(--space-medium)),
        calc((100% - var(--content-maximum-width)) / 2 + var(--container-padding))
    );

    flex: 1 1 auto;
    min-height: 0;
    width: 100%;

    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-gutter: stable;

    padding: var(--space-extra-large) var(--content-gutter);

    /* Its own colour rather than the page's, so a module's content area can be
       set off from the window behind it. They match by default, which is why
       this changed nothing until somebody chose otherwise. */
    background-color: var(--colour-module-background);
    color: var(--colour-module-text);
}

/* Centres a narrow column, used by the sign in, registration and PIN pages.
   It works either as a modifier on .page-main or on a plain div inside it. */
.page-main--narrow {
    max-width: 30rem;
    margin-left: auto;
    margin-right: auto;
}

/* On .page-main itself it cannot narrow the box, because that box is the
   scrolling viewport and narrowing it would move the scrollbar into the middle
   of the window. It narrows the content inside it instead, which is what was
   meant either way. */
.page-main.page-main--narrow {
    max-width: none;
    --content-maximum-width: 30rem;
}

/* CONTENT GUTTERS OFF. The same idea as .site-header--edge-to-edge and set the
   same way in site edit mode, and it works by the same arithmetic: with no
   maximum width to centre, the first term of the gutter sum collapses and the
   padding is all that is left, so the content runs to the window edge with only
   its own breathing room beside it.

   Setting the maximum width rather than the padding is what keeps this in step
   with the header. Both are computed from the same custom property, so a header
   with its gutters on still lines up with content that has them off, which is a
   combination somebody will choose and should not have to think about. */
.page-main--edge-to-edge {
    --content-maximum-width: 100%;
}

/* None of these scrolls, and none gives up any of its width to a scrollbar,
   which is the whole point of the arrangement above. */
.site-header,
.site-footer,
.module-toolbar {
    flex-shrink: 0;
}

/* A STRIP BELOW THE HEADER for a module that asked for one. Exactly forty
   pixels: enough for a control and a number, not enough to become a second
   content area. Anything taller is a card and belongs in the content.

   height rather than min-height, deliberately. min-height would let a module
   quietly grow the bar by putting something tall in it, and the fixed size is
   what makes it safe to give away this much of every screen. */
.module-toolbar {
    height: 40px;

    display: flex;
    align-items: center;
    gap: var(--space-small);

    padding: 0 var(--content-gutter);

    background-color: var(--colour-card-background);
    border-bottom: 1px solid var(--colour-border);
}

/* The same gutter arithmetic as .page-main, so what sits at each end of the
   toolbar lines up with the content below it. */
/* -----------------------------------------------------------------------------
   THE SCREENS OF WHATEVER YOU ARE LOOKING AT.

   These used to sit inside the content area, which made a row about moving
   between screens look like part of the screen it was above, and scrolled it
   away the moment you started reading. Here they are outside the scrolling
   area, so they stay put and stay reachable.
   -------------------------------------------------------------------------- */

.module-toolbar__screens {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);
    min-width: 0;

    /* SCROLLS RATHER THAN WRAPS. The strip is one row tall, and seven
       administration screens do not fit across a phone. Scrolling sideways
       keeps the height honest; wrapping would push the page content down by an
       amount nobody can predict. */
    overflow-x: auto;
    scrollbar-width: none;
}

.module-toolbar__screens::-webkit-scrollbar {
    display: none;
}

.module-toolbar__screen {
    flex-shrink: 0;

    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-small);

    font-weight: 600;
    font-size: 0.9rem;
    white-space: nowrap;
    color: var(--colour-text-secondary);
}

.module-toolbar__screen:hover {
    background-color: var(--colour-accent-soft);
    color: var(--colour-accent-hover);
    text-decoration: none;
}

.module-toolbar__screen--current {
    background-color: var(--colour-accent-soft);
    color: var(--colour-accent);
}

/* Whatever the module itself puts in the bar sits at the far end, away from the
   screen links, so the two never read as one list. */
/* WHAT A MODULE PUTS IN THE STRIP, and where it sits.

   IT ONLY GOES TO THE RIGHT WHEN THERE IS SOMETHING ON THE LEFT. The strip was
   designed around two halves -- the screens of the area on the left, whatever
   the module wants to say on the right, a cart and its total being the case it
   was written for. margin-left: auto was unconditional, so a module with only
   one screen has nothing on the left and its contribution was pushed across an
   empty bar to the far edge. The forum's board links arrived hard right of a
   page whose every other thing starts at the left gutter.

   The adjacent sibling selector says it exactly: pushed over only when it
   follows a row of screens. Alone in the bar, it starts where the content
   below it starts. */
.module-toolbar__module {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    flex-shrink: 0;
}

.module-toolbar__screens + .module-toolbar__module {
    margin-left: auto;
}

/* A WRAPPER THAT HOLDS A ROW OF SCREENS MAY SHRINK; ONE THAT HOLDS CONTROLS MAY
   NOT. flex-shrink: 0 above is right for the usual case -- a module puts a
   button and a count in the bar and neither should be squeezed.

   IT IS WRONG WHEN THE MODULE PUTS A SCREENS NAV IN THERE. That nav already has
   min-width: 0 and overflow-x: auto and is entirely ready to scroll, but a child
   only scrolls once something makes it narrower than its contents -- and a
   parent that never shrinks never does. So the nav stayed at its full width, the
   scrolling never engaged, and the strip pushed the whole page sideways on a
   phone. The forum does exactly this with its board list.

   :has() RATHER THAN A SECOND CLASS, so a module gets the right behaviour from
   the markup it already writes instead of having to know to ask for it. */
.module-toolbar__module:has(> .module-toolbar__screens) {
    flex-shrink: 1;
    min-width: 0;
}

.module-toolbar {
    --content-maximum-width: var(--layout-maximum-width);
    /* THE LARGER OF WHAT AN ADMINISTRATOR ASKED FOR AND WHAT CENTRING LEAVES.
       On a wide screen the column is centred and the second term wins; on a
       phone there is nothing to centre and the first is the whole gutter.

       --content-gutter-minimum is set in site edit mode. The fallback is what
       this was before it was settable, so a site nobody has edited is unchanged
       and a browser that never receives the token still lays out. */
    --content-gutter: max(
        var(--content-gutter-minimum, var(--space-medium)),
        calc((100% - var(--content-maximum-width)) / 2 + var(--container-padding))
    );
}

/* Pushes whatever follows it to the far end of the bar. */
.module-toolbar__spacer {
    flex: 1 1 auto;
}

/* -----------------------------------------------------------------------------
   3. Site header and footer
   -------------------------------------------------------------------------- */

.site-header {
    background-color: var(--colour-header-background);
    border-bottom: 1px solid var(--colour-border);
}

/* ONE ELEMENT ANSWERED TWO QUESTIONS AND NOW TWO DO.

   THE CONTAINER ANSWERS HOW FAR IN FROM THE EDGE THE HEADER STARTS, which is
   the gutter choice an administrator makes once for the whole site.

   THE BAR ANSWERS WHAT IS IN THE HEADER AND IN WHAT ORDER, which has nothing to
   do with the gutter. While they were one element every change to either had to
   be made without disturbing the other, and the padding in particular is read
   by the gutter calculation further up. */
/* THE HEADER LINES UP WITH THE CARDS, BY DOING THE SAME SUM THEY DO.

   A MAX-WIDTH BOX AND A CENTRING MARGIN GOT CLOSE AND NO CLOSER. It matched the
   second half of the content area's gutter -- half the space left over once the
   column is centred, plus an inset -- but not the first: on a window narrower
   than the maximum width there is no leftover space, the content area falls back
   to its minimum gutter, and the header did not. The two edges agreed on a
   desktop and disagreed on a phone.

   SO THE SUM IS COPIED RATHER THAN APPROXIMATED. This is the same expression as
   .page-main's --content-gutter, and it has to be evaluated somewhere full
   width or the percentage would measure against the header's own column instead
   of the window. .site-header__overlay and .site-header__stage are both plain
   blocks, so this is that width.

   THE BAR'S OWN PADDING IS SUBTRACTED, because it sits inside this one: the two
   together have to come to the gutter, not the gutter plus five pixels. */
.site-header__container {
    --site-header-content-maximum-width: var(--layout-maximum-width);

    --site-header-content-gutter: max(
        var(--content-gutter-minimum, var(--space-medium)),
        calc((100% - var(--site-header-content-maximum-width)) / 2 + var(--container-padding))
    );

    width: 100%;
    padding-inline: calc(
        var(--site-header-content-gutter) - var(--site-header-bar-padding)
    );
}

/* THE BAR IS ONE ROW OF ONE HEIGHT, AT EVERY WIDTH.

   THE HEIGHT COMES FROM THE BUTTON, AND FROM NOTHING ELSE. The button is a
   declared height and the bar is that plus its own inset, so the header cannot
   be stretched by a long site name or an oversized logo -- both of which used to
   change how tall the site's header was.

   NOTHING WRAPS. Below a tablet the two halves used to stack into a pair of
   centred rows, which doubled the header's height on exactly the screens with
   the least room to spare. The branding takes whatever is left after the menu
   and shortens its text to fit; see the ellipsis rules below.

   FULL HEIGHT MEANS THE HEIGHT INSIDE THE PADDING. The bar keeps a few pixels
   on every side, and anything in here asked to fill it fills the content box --
   not the border box, and not past the inset to the edge of the header. A thing
   that reaches the edge of its container by ignoring the container's padding is
   not full height, it is overflowing tidily.

   THE INSET IS SMALL BECAUSE IT IS NOT THE ONE THAT MATTERS. The space that puts
   the header's contents on the same line as the cards below them belongs to
   .site-header__container, which works it out from the content area's own
   gutter; all this one does is keep the button off the edge of the bar. */
.site-header {
    /* THE SIZE THE PICTURE IS ACTUALLY DRAWN AT, which is the chosen one until
       the header is compact. Kept apart from the setting itself so that compact
       can scale what an administrator asked for rather than replace it: writing
       a fixed size here would make the setting do nothing on a compact header,
       and writing the scale into the setting would be a variable that referred
       to itself. The logo does the same thing a few hundred lines down, for the
       same reason. */
    --site-header-avatar-drawn: var(--site-header-avatar-size);

    /* THE BUTTON IS THE PICTURE PLUS ITS OWN INSET, and everything above it
       follows in turn. Nothing in the header declares a height of its own any
       more; there is one number, and it is the size of the picture. */
    --site-header-button-height: calc(
        var(--site-header-avatar-drawn)
        + (var(--site-header-menu-button-padding) * 2)
    );

    /* Just enough that the button is not touching the edge of the bar. The inset
       that lines the header up with the page is on the container, not here. */
    --site-header-bar-padding: 5px;


}

/* COMPACT HALVES THE PICTURE, AND THE HEIGHTS ABOVE IT FOLLOW. The setting's own
   help says compact halves the header, and this is now the only place that has
   to be true: everything else is derived. It used to name a button height of
   forty pixels, which was close to half of the sixty four it replaced and had to
   be kept in step with it by hand. */
.site-header--compact {
    --site-header-avatar-drawn: calc(var(--site-header-avatar-size) / 2);
}

.site-header__bar {
    /* NAMED, BECAUSE THE SPACER AT THE END HAS TO CANCEL IT. A gap applies
       between the spacer and the thing before it as much as between anything
       else, and the spacer is supposed to be invisible; it takes this back off
       as a negative margin. Written as a number in two places it went wrong the
       first time the menu moved to the left, where the gap is larger. */
    --site-header-bar-gap: var(--space-medium);

    padding: var(--site-header-bar-padding);

    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--site-header-bar-gap);
    flex-wrap: nowrap;
}

/* THE LAST FIFTEEN PIXELS, WHICH BELONG TO A SCROLLBAR IN A DIFFERENT ELEMENT.

   .page-main reserves room for its scrollbar whether or not the page scrolls,
   so its cards are centred inside a box narrower than the window by that much.
   The header spans the whole window. Both now work their gutter out from the
   same expression, which lines their left edges up exactly -- and leaves their
   right edges apart by the width of that scrollbar.

   NOTHING IN CSS CAN SAY HOW WIDE A SCROLLBAR IS, so this asks for one instead.
   An empty box that scrolls, with the same scrollbar-gutter setting .page-main
   has, sized to fit its contents, comes out exactly as wide as the gutter it
   reserves: fifteen pixels or so where scrollbars take up room, and nothing at
   all where they are drawn over the content. It sits at the end of the bar and
   pushes the menu in by that much.

   IT IS AN ELEMENT RATHER THAN A ::after ON THE BAR, which is the same rule and
   no markup at all. It was written that way first. Where scrollbars are drawn
   over the content and reserve nothing, the pseudo-element still came out a
   scrollbar wide and pushed the menu in by fifteen pixels it had not taken; the
   same declarations on a real element came out at nothing, which is right. The
   markup is the price of being correct on both.

   MEASURING IT IN JAVASCRIPT WAS TRIED FIRST AND CANNOT WORK HERE. The width is
   easy to read, but publishing it means writing a custom property onto an
   element, and the Content Security Policy blocks inline styles: the attribute
   appears in the DOM, the console logs a violation, and the layout never moves.

   THE NEGATIVE MARGIN CANCELS THE BAR'S GAP, which would otherwise put a space
   between the menu and a spacer that is supposed to be invisible. It cancels
   exactly, so where a scrollbar reserves nothing the spacer costs nothing -- and
   it reads the gap from the same custom property the bar sets it from, because
   the two are not always the same: the menu on the left is given a wider one. */
.site-header__scrollbar-gutter {
    flex: 0 0 auto;
    align-self: flex-start;

    height: 0;
    width: fit-content;

    overflow-y: auto;
    scrollbar-gutter: stable;

    margin-inline-start: calc(var(--site-header-bar-gap) * -1);
}

/* WHERE THE FINGER IS THE POINTER, NOTHING IS RESERVED AND NOTHING STANDS IN.
   A touch platform draws its scrollbars over the content, so .page-main gives up
   no width and there is no difference left for this to make up. Asking the box
   what a scrollbar measures is not reliable there: a phone emulated in a desktop
   browser answers with the desktop's fifteen pixels while the content area
   reserves none of them, and the header ends up inset by a scrollbar that does
   not exist. The input is the better question, because the two travel together:
   a device driven by touch has overlay scrollbars, and one driven by a mouse --
   including a laptop whose screen also happens to be touchable -- does not. */
@media (pointer: coarse) {
    .site-header__scrollbar-gutter {
        display: none;
    }
}
/* THE BRANDING TAKES WHAT IS LEFT AND KEEPS IT. min-width: 0 is the whole of it:
   without it a flex item refuses to shrink below its content, so a long site
   name pushes the menu off the end of the bar instead of being cut short. */
.site-header__branding {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}

.site-header__wordmark {
    min-width: 0;
}

/* CUT SHORT RATHER THAN ALLOWED TO PUSH. A name or a byline longer than the room
   for it ends in an ellipsis; neither is important enough to change the shape of
   the header for.

   THE BAR ITSELF IS NOT CLIPPED, and must not be: the menu's dropdown is
   positioned against an element inside it, so an overflow rule there would cut
   the menu off at the bottom edge of the header. */
.site-header__name,
.site-header__byline {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* THE NAME AND BYLINE ARE NOT BESIDE THE LOGO, THEY ARE OVER THE BANNER. They
   sit in .site-header__overlay, a layer of their own above the pictures, so
   nothing here is laid out against the logo and nothing here can push it or be
   pushed by it.

   The offset below is the one link between them, and it is deliberate: the logo
   is drawn at the left gutter, so text starting at that same gutter would be
   printed across it. Stepping the text past the logo's width, plus a gap, puts
   it clear of the picture and nowhere else. Without a logo there is nothing to
   step past and the rule does not apply. */
/* NOTHING STEPS PAST THE LOGO ANY MORE. The logo used to be pinned to the
   gutter behind the row and the branding was padded by the logo's width to
   clear it -- two rules holding one arrangement, either of which could be
   changed without the other. The logo is an item in the branding row now, so
   the row spaces it like anything else. */

.site-header__branding {
    display: inline-flex;
    align-items: center;

    font-size: 1.15rem;
    font-weight: 650;
    color: var(--colour-header-text);
    letter-spacing: -0.01em;
}

/* The old banner rule expected an <img>. The pictures are backgrounds now, so
   that they can tile; see section 22. */

.site-header__branding:hover {
    text-decoration: none;
    color: var(--colour-header-text);
    background-color: var(--colour-header-hover-surface);
}

.site-header__navigation {
    display: flex;
    align-items: center;
    gap: var(--space-medium);
    flex-wrap: wrap;
}

.site-footer {
    flex-shrink: 0;
    border-top: 1px solid var(--colour-border);
    padding: var(--container-padding-tight) var(--container-padding);
    text-align: center;
    font-size: 0.875rem;
    color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   4. Cards and typography
   -------------------------------------------------------------------------- */

.card {
    background-color: var(--colour-card-background);
    color: var(--colour-card-text);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
    box-shadow: var(--shadow-card);
    padding: var(--container-padding);
}

/* EVERY CARD AFTER THE FIRST, not every card directly after another. A hidden
   element between two cards -- a panel waiting to be moved into a modal, a
   block some condition switched off -- is still a node in the tree, and with
   the adjacent combinator it took the gap with it. */
.card ~ .card {
    margin-top: var(--space-large);
}

/* EXCEPT INSIDE A GRID, WHERE THE GAP ALREADY DID IT.

   Not merely redundant -- harmful. A top margin on a grid item is taken out of
   that item's own grid area, so every card after the first came up short of the
   row and sank to the bottom of it. A row of three cards was three different
   heights with their tops on two different lines, and the gap between columns
   was the row gap plus a margin.

   Found on the front page, but it was true of every feature grid on the site. */
.feature-grid > .card ~ .card {
    margin-top: 0;
}

.card__title {
    margin: 0 0 var(--space-extra-small);
    font-size: 1.35rem;
    letter-spacing: -0.01em;
}

.card__subtitle {
    margin: 0 0 var(--space-large);
    line-height: 1.5;
}

/* -----------------------------------------------------------------------------
   An expandable card

   A CARD THAT OPENS, for the ones that hold a lot and are wanted rarely. Shut,
   it is its title and subtitle and nothing else, so a screen carrying one of
   them still reads as a list of what is on it. Open, it is an ordinary card.

   A <details> RATHER THAN A BUTTON AND A CLASS. The browser already knows how to
   open one, reach it from the keyboard and announce whether it is open, and a
   card that only collapsed once a script had loaded would be a worse card than
   one that never collapsed. It is the same choice the wiki tree makes.

   The card keeps its own padding, so a shut one is not a thin strip: it is a
   panel with a heading in it, which is what a card is.
   -------------------------------------------------------------------------- */

.card--expandable {
    /* Its own padding lives on .card. What changes is that the summary is the
       whole of the card until it is opened. */
    padding: var(--container-padding);
}

.card__summary {
    /* NOT A FLEX ROW WITH THE MARKER IN IT. The chevron used to sit to the left
       of the title, which pushed every word on the card in by its width and
       left an expandable card's text out of line with the plain cards above
       and below it. The heading has to start where every other heading starts,
       so the marker went underneath instead. */
    display: block;

    cursor: pointer;

    /* The default marker is a filled triangle that nothing else on the site
       uses, and it cannot be moved or coloured. Ours is drawn below. */
    list-style: none;
}

.card__summary::-webkit-details-marker {
    display: none;
}

/* The subtitle's own bottom margin is already dropped by the existing
   .card__subtitle:last-child rule, because inside .card__summary-text it always
   is the last child. The gap to the content comes from the body's padding-top
   instead, which is the same either way round. */

.card__summary .card__title,
.card__summary .card__subtitle {
    /* The summary is the clickable thing; its children should not swallow the
       click or draw a focus ring of their own. */
    pointer-events: none;
}

.card__summary-text {
    display: block;
}

/* Drawn from two borders rather than a character, so it needs no font to be
   present and turns with the card rather than being swapped for a glyph. */
/* ACROSS THE BOTTOM, CENTRED. A full width strip is a larger thing to aim at
   than a chevron beside a heading, and it reads as the edge of the card being
   the handle, which is what it is. */
.card__summary-marker {
    display: block;
    margin: var(--space-small) auto 0;

    width: 0.5rem;
    height: 0.5rem;

    border-right: 2px solid var(--colour-text-secondary);
    border-bottom: 2px solid var(--colour-text-secondary);

    /* Pointing down when shut, up when open, so it says which way the card is
       about to move rather than which state it is in. */
    transform: rotate(45deg);
    transform-origin: center;
    transition: transform 0.15s ease;
}

.card--expandable[open] .card__summary-marker {
    transform: rotate(-135deg);
}

.card__summary:hover .card__summary-marker {
    border-right-color: var(--colour-text-primary);
    border-bottom-color: var(--colour-text-primary);
}

.card__summary:focus-visible {
    outline: 2px solid var(--colour-accent);
    outline-offset: 3px;
    border-radius: var(--radius-small);
}

/* Everything below the summary, which is what the toggle actually shows and
   hides. One container, so a card contributed by a module does not have to know
   it is going to be inside a collapsible one. */
.card__expandable-body {
    padding-top: var(--space-small);
}

@media (prefers-reduced-motion: reduce) {
    .card__summary-marker {
        transition: none;
    }
}

/* THE PAGE TITLE SITS ON A PANEL, like every other piece of text on the site.
   It used to sit bare on the content area, which meant it was readable only
   while the content background and the heading colour happened to differ. A
   visitor who darkened the content area in styling mode lost the title, and
   nothing in the styling manager could give it back: the two colours fighting
   were the content area's and the heading's, and neither belonged to the title.

   It uses the card surface rather than a surface of its own, so it is already
   covered by the Panels and cards class and there is nothing new to learn.

   The title and the subtitle are one panel, not two: the title keeps its bottom
   corners square when a subtitle follows, and the subtitle sits flush against
   it. */
.page-title {
    margin: 0;
    /* The same inset as a card, so the heading panel tiles with everything
       under it. Its bottom is closed by the subtitle when there is one. */
    padding: var(--container-padding) var(--container-padding)
             var(--container-padding-tight);

    font-size: var(--font-size-heading);
    letter-spacing: -0.02em;

    /* THE SAME EDGE EVERY OTHER PANEL HAS. The heading sits on a panel like the
       cards below it, but it had the background without the border, so on a
       screen made of cards it was the one surface with no edge to it and read
       as unfinished rather than as deliberate.

       NO BOTTOM BORDER: the subtitle continues this panel and closes it. Drawn
       here it would put a line across the middle of one shape. The rule below
       puts it back for a heading with no subtitle under it. */
    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    border-bottom: 0;
    border-radius: var(--radius-medium) var(--radius-medium) 0 0;
    box-shadow: var(--shadow-card);
}

/* Nothing follows it, so this heading is the whole panel and closes itself. */
.page-title:not(:has(+ .page-subtitle)) {
    margin-bottom: var(--space-large);
    padding-bottom: var(--container-padding);
    border-bottom: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
}

.page-subtitle {
    margin: 0 0 var(--space-large);
    padding: 0 var(--container-padding) var(--container-padding);

    color: var(--colour-text-secondary);
    font-size: 1.05rem;

    /* The bottom half of the heading panel: no top border, because the heading
       above it is the same shape and a line between them would divide it. */
    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    border-top: 0;
    border-radius: 0 0 var(--radius-medium) var(--radius-medium);
    box-shadow: var(--shadow-card);
}

/* One size and one colour for every quiet run of text, so the two sliders in
   styling mode reach all of it rather than some of it. */
.text-secondary,
.card__subtitle,
.form-field__help,
.stat-tile__label {
    font-size: var(--font-size-quiet);
    color: var(--colour-text-secondary);
}

.detail-list {
    margin: 0;
    display: grid;
    grid-template-columns: minmax(8rem, auto) 1fr;
    gap: var(--space-small) var(--space-medium);
}

.detail-list dt {
    color: var(--colour-text-secondary);
    font-size: 0.9rem;
}

.detail-list dd {
    margin: 0;
    word-break: break-word;
}

/* -----------------------------------------------------------------------------
   5. Forms, inputs and validation messages
   -------------------------------------------------------------------------- */

.form-field {
    margin-bottom: var(--space-medium);
}

.form-field__label {
    display: block;
    margin-bottom: var(--space-extra-small);
    font-weight: 600;
    font-size: 0.925rem;
}

.form-field__input,
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="url"],
textarea,
select {
    display: block;
    width: 100%;

    /* TRIMMED FROM 0.625rem, WHICH WAS TALLER THAN IT NEEDED TO BE. Four pixels
       a control is nothing on a form with three fields and most of a screen on
       the avatar builder, which is nine menus and four rows of colour in a
       modal. Every input takes the same cut rather than only the menus, because
       a select that is shorter than the text box beside it looks like a mistake.

       THIS IS THE FLOOR. Below about this the box stops being a comfortable
       target on a touch screen -- these are already under the 44 pixel guidance
       and further down is the wrong direction. */
    padding: 0.5rem 0.7rem;

    /* THEIR OWN FONT AND COLOURS, not the card's. What somebody types is read
       against the box it is in, and a box that inherits whatever surface it
       happens to sit on is a box nobody can style out of being unreadable. */
    font-family: var(--font-family-input);
    font-size: var(--font-size-input);
    color: var(--colour-input-text);
    background-color: var(--colour-input-background);

    border: 1px solid var(--colour-input-border);
    border-radius: var(--radius-small);

    /* Only the shadow is named here; the colours are handled by the shared
       transition in section 1c, along with every other interactive element. */
    transition: box-shadow 120ms ease-out;
}

.form-field__input:hover {
    border-color: var(--colour-accent);
}

.form-field__input:focus {
    outline: none;
    border-color: var(--colour-accent);
    box-shadow: 0 0 0 3px var(--colour-accent-soft);
}

.form-field__input:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.form-field__help {
    margin: var(--space-extra-small) 0 0;
    line-height: 1.45;
}

/* Error text is empty and hidden until JavaScript fills it in. */
.form-field__error {
    margin: var(--space-extra-small) 0 0;
    font-size: 0.85rem;
    color: var(--colour-error);
    min-height: 0;
}

.form-field__error:empty {
    display: none;
}

/* Applied to the input alongside an error message. */
.form-field__input--invalid,
.form-field__input[aria-invalid="true"] {
    border-color: var(--colour-error);
}

.form-field__input--invalid:focus,
.form-field__input[aria-invalid="true"]:focus {
    box-shadow: 0 0 0 3px var(--colour-error-soft);
}

.form-footnote {
    margin-top: var(--space-large);
    text-align: center;
    font-size: 0.925rem;
    color: var(--colour-text-secondary);
}

.form-actions {
    display: flex;
    align-items: center;
    gap: var(--space-medium);
    flex-wrap: wrap;
    margin-top: var(--space-large);
}

/* -----------------------------------------------------------------------------
   6. Buttons
   -------------------------------------------------------------------------- */

.button {
    display: inline-block;
    padding: 0.625rem 1.1rem;

    font-family: inherit;
    font-size: var(--font-size-button);
    font-weight: 600;
    line-height: 1.3;
    text-align: center;
    color: var(--colour-button-text);
    background-color: var(--colour-accent);

    border: 1px solid transparent;
    border-radius: var(--radius-small);
    cursor: pointer;

    /* Colours come from the shared transition in section 1c. */
    transition: opacity 120ms ease-out;
}

.button:hover {
    background-color: var(--colour-accent-hover);
    color: var(--colour-button-text);
    text-decoration: none;
}

.button:disabled,
.button[aria-busy="true"] {
    opacity: 0.6;
    cursor: not-allowed;
}

.button--full-width {
    display: block;
    width: 100%;
}

.button--small {
    padding: 0.4rem 0.8rem;
    font-size: 0.9rem;
}

/* A button that reads as a link, for secondary actions. */
.button--quiet {
    background-color: transparent;
    color: var(--colour-accent);
    border-color: transparent;
    padding-left: 0;
    padding-right: 0;
}

.button--quiet:hover {
    background-color: transparent;
    color: var(--colour-accent-hover);
    text-decoration: underline;
}

.button--outline {
    background-color: transparent;
    color: var(--colour-accent);
    border-color: var(--colour-border-strong);
}

.button--outline:hover {
    background-color: var(--colour-accent-soft);
    border-color: var(--colour-accent);
    color: var(--colour-accent-hover);
}

/* -----------------------------------------------------------------------------
   7. Alerts and the PIN entry field
   -------------------------------------------------------------------------- */

.alert {
    padding: var(--container-padding-tight);
    margin-bottom: var(--space-large);
    border: 1px solid transparent;
    border-radius: var(--radius-small);
    font-size: 0.95rem;
}

.alert:empty {
    display: none;
}

.alert--info {
    color: var(--colour-info);
    background-color: var(--colour-info-soft);
    border-color: currentColor;
}

.alert--success {
    color: var(--colour-success);
    background-color: var(--colour-success-soft);
    border-color: currentColor;
}

/* -----------------------------------------------------------------------------
   THE SAVED NOTICE

   A setting that saves itself still has to say so, or there is no telling a
   saved change from a lost one. Saying it in a banner above the form was the
   problem: the banner is part of the layout, so it appeared, pushed everything
   below it down, and moved the thing somebody had just clicked out from under
   their cursor.

   Out of the layout entirely, then. Fixed to the corner of the screen rather
   than the document, because the forms that use it are long and a notice that
   scrolled away would be missed by anybody working near the bottom of one.
   -------------------------------------------------------------------------- */

.saved-toast {
    position: fixed;
    left: var(--container-padding);
    bottom: var(--container-padding);
    z-index: 1100;

    padding: var(--container-padding-tight) var(--container-padding);

    font-weight: 600;
    color: var(--colour-success);

    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-success);
    border-radius: var(--radius-medium);
    box-shadow: var(--shadow-card);

    /* It starts invisible and out of reach: the class below brings it in, and
       nothing can be clicked through it on the way past. */
    opacity: 0;
    transform: translateY(6px);
    pointer-events: none;

    transition: opacity 120ms ease-out, transform 120ms ease-out;
}

.saved-toast--showing {
    opacity: 1;
    transform: translateY(0);
}

/* Leaving is slower than arriving. A notice that appears sharply reads as a
   response to what you did; one that vanishes just as sharply reads as a
   glitch. */
.saved-toast--leaving {
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 300ms ease-in, transform 300ms ease-in;
}

@media (max-width: 40rem) {
    /* On a phone the corner is nearly the whole width, so it spans it rather
       than leaving a stub of card against one edge. */
    .saved-toast {
        left: var(--container-padding);
        right: var(--container-padding);
        text-align: center;
    }
}

@media (prefers-reduced-motion: reduce) {
    .saved-toast,
    .saved-toast--leaving {
        transition: opacity 120ms linear;
        transform: none;
    }
}

.alert--error {
    color: var(--colour-error);
    background-color: var(--colour-error-soft);
    border-color: currentColor;
}

/* The six digit code field: wide, monospaced and generously letter spaced so
   the digits are easy to read back from an email. */
.pin-input {
    font-family: var(--font-family-mono);
    font-size: 1.75rem;
    letter-spacing: 0.6em;
    text-align: center;
    text-indent: 0.6em; /* keeps the digits optically centred despite tracking */
    padding: 0.6rem 0.5rem;
}

.pin-input::placeholder {
    letter-spacing: 0.35em;
    font-size: 1.35rem;
    opacity: 0.45;
}

/* Inline row holding the resend button and its countdown. */
.resend-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-small);
    flex-wrap: wrap;
    margin-top: var(--space-medium);
    font-size: 0.9rem;
    color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   8. Utilities and responsive adjustments
   -------------------------------------------------------------------------- */

/* Visible only to assistive technology. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.stack-small  > * + * { margin-top: var(--space-small); }
.stack-medium > * + * { margin-top: var(--space-medium); }
.stack-large  > * + * { margin-top: var(--space-large); }

.text-centre { text-align: center; }

.feature-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-medium);
}

@media (max-width: 40rem) {
    /* Only the vertical padding changes. The horizontal padding is the gutter
       that keeps the content lined up with the header, and setting all four
       here would quietly undo it. */
    .page-main {
        padding-top: var(--space-large);
        padding-bottom: var(--space-large);
    }

    .page-title {
        font-size: 1.55rem;
    }

    .pin-input {
        font-size: 1.4rem;
        letter-spacing: 0.4em;
        text-indent: 0.4em;
    }
}

/* Respect a request for less motion. */
@media (prefers-reduced-motion: reduce) {
    * {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
    }
}

/* Extra breathing room above a grid that follows a call to action. The Content
   Security Policy in includes/page_layout.php forbids inline style attributes,
   so spacing like this belongs in a class rather than on the element. */
.feature-grid--spaced {
    margin-top: var(--space-extra-large);
}

/* The sign in, registration, confirmation and reset pages set this class on
   <body>. Nothing depends on it yet; it is the hook to reach for when a project
   wants those pages to look different from the rest of the site, for example a
   different background or a centred logo. */
.page-authentication .page-main {
    padding-top: var(--space-extra-large);
}

/* -----------------------------------------------------------------------------
   9. Development banner
      Rendered above every page by renderDevelopmentBanner() while the site is
      in development, and by nothing at all in production.
   -------------------------------------------------------------------------- */

/* A THIN STRIP, NOT A PARAGRAPH. It says one thing: this is not production.
   The diagnostics that used to sit here in full are on the label's title
   attribute, one hover away and none of them taking a line of every page.

   EXACTLY THIRTY PIXELS, fixed rather than minimum, so that nothing put in it
   later can quietly grow the strip every page carries. */
.development-banner {
    flex-shrink: 0;
    height: 30px;

    display: flex;
    align-items: center;
    gap: var(--space-small);

    padding: 0 var(--space-small);

    font-size: 0.72rem;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
}

.development-banner--offline {
    color: #26160a;
    background-color: #f0b357;
}

.development-banner--database {
    color: #0d2137;
    background-color: #8ecbe8;
}

.development-banner__label {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;

    /* There is more here on hover, and the cursor is what says so. */
    cursor: help;
}

/* THE SIGN IN LINKS SIT AT THE RIGHT HAND END, pushed there by the label's
   auto margin rather than by being positioned, so they cannot land on top of a
   long label on a narrow window. */
.development-banner__actions {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    margin-left: auto;
}

/* Buttons that read as links, because that is what they do: each one posts a
   sign in rather than following an address, but nothing about that is the
   reader's problem. */
.development-banner__link {
    padding: 0;

    font: inherit;
    font-weight: 600;
    color: inherit;
    text-decoration: underline;

    background: none;
    border: 0;
    cursor: pointer;
}

.development-banner__link:hover {
    text-decoration: none;
}

.development-banner__close {
    padding: 0 0.3rem;

    font-size: 0.9rem;
    line-height: 1;
    cursor: pointer;

    color: inherit;
    background: transparent;
    border: 0;
    opacity: 0.7;
}

.development-banner__close:hover {
    opacity: 1;
}

/* Only so the close button has something to be positioned against. */
.development-banner {
    position: relative;
}

/* -----------------------------------------------------------------------------
   10. Admin navigation and badges
   -------------------------------------------------------------------------- */

/* THE ADMINISTRATION TABS SIT ON A PANEL TOO, and for the same reason: a row of
   links on the bare content area is a row of links that disappears when that
   area is recoloured. */
.admin-navigation {
    display: flex;
    gap: var(--space-small);
    flex-wrap: wrap;

    margin-bottom: var(--space-large);

    /* Half the container inset. A row of links does not need a card's worth of
       space around it, and half is a relationship somebody can see rather than
       a second number to keep in step. */
    padding: var(--container-padding-tight);

    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
}

.admin-navigation__link {
    padding: 0.35rem 0.75rem;
    border-radius: var(--radius-small);
    font-weight: 600;
    font-size: 0.95rem;
    color: var(--colour-text-secondary);
}

.admin-navigation__link:hover {
    background-color: var(--colour-accent-soft);
    color: var(--colour-accent-hover);
    text-decoration: none;
}

.admin-navigation__link--active {
    background-color: var(--colour-accent-soft);
    color: var(--colour-accent);
}

.badge {
    display: inline-block;
    margin-right: var(--space-extra-small);
    padding: 0.1rem 0.45rem;

    font-size: 0.75rem;
    font-weight: 650;
    letter-spacing: 0.02em;
    white-space: nowrap;
    vertical-align: middle;

    border: 1px solid currentColor;
    border-radius: 999px;
}

.badge--neutral { color: var(--colour-text-secondary); }
.badge--success { color: var(--colour-success); }
.badge--warning { color: #8a5a00; }
.badge--error   { color: var(--colour-error); }
.badge--accent  { color: var(--colour-accent); }

@media (prefers-color-scheme: dark) {
    .badge--warning { color: #e9b665; }
}

/* -----------------------------------------------------------------------------
   11. Admin tables, search and paging
   -------------------------------------------------------------------------- */

/* Wide tables scroll inside their card rather than pushing the page sideways. */
/* A table reaches the edges of the card it is in by pulling out of the card's
   inset and putting it back as padding, so a scrolled table has something to
   scroll under. The two must cancel exactly: if the pull-out and the card's
   inset are different numbers, the table overhangs the card or stops short of
   it, which is what happened while this was a fixed 1.5rem and the card's inset
   was not. */
/* POSITIONED SO THAT WHAT IS POSITIONED INSIDE IT IS CLIPPED WITH EVERYTHING
   ELSE, and this was a real bug rather than tidiness.

   overflow-x: auto clips a normal child. It does not clip an absolutely
   positioned one, because that child is laid out against the nearest positioned
   ancestor -- and without this rule there was none, so it resolved against the
   page. Any wide table with a visually-hidden label in a heading -- which is how
   this template labels an actions column for a screen reader -- put a 1px span
   six hundred pixels to the right of a phone screen, and the whole document
   scrolled sideways to reach it. Invisible, one pixel wide, and it moved the
   page.

   The measurement, on the forum at 375px: the span sat at x=537 and the document
   scrolled to 537. With this, it is clipped by the scroller it lives in and the
   page is exactly as wide as the window. */
.table-scroll {
    position: relative;
    overflow-x: auto;
    margin: 0 calc(var(--container-padding) * -1);
    padding: 0 var(--container-padding);
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.925rem;
}

.data-table th,
.data-table td {
    padding: 0.6rem 0.5rem;
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--colour-border);
}

.data-table th {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--colour-text-secondary);
    white-space: nowrap;
}

/* EVERY ROW OF EVERY TABLE, which is the plainest example of a group made by
   a loop: one class, however many rows, styled once. */
.data-table tbody tr {
    background-color: var(--colour-table-row-background);
    color: var(--colour-table-row-text);
}

.data-table tbody tr:hover {
    background-color: var(--colour-accent-soft);
}

.data-table__actions {
    white-space: nowrap;
    text-align: right;
}

.data-table__actions > * + * {
    margin-left: var(--space-extra-small);
}

.search-row {
    display: flex;
    gap: var(--space-small);
    align-items: center;
    flex-wrap: wrap;
}

.search-row .form-field__input {
    flex: 1 1 16rem;
    width: auto;
}

.pagination-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-medium);
    flex-wrap: wrap;
    margin-top: var(--space-medium);
}

/* -----------------------------------------------------------------------------
   12. Permission and group editors
   -------------------------------------------------------------------------- */

.checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: var(--space-small);
}

.checkbox-grid__item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-small);

    padding: var(--container-padding-tight);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
    cursor: pointer;
}

.checkbox-grid__item:hover {
    border-color: var(--colour-accent);
    background-color: var(--colour-accent-soft);
}

.checkbox-grid__item input {
    margin-top: 0.25rem;
    flex-shrink: 0;
}

/* The label text of a checkbox stacks its title above its explanation. */
.checkbox-grid__item > span {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    font-size: 0.9rem;
}

.checkbox-grid__item .text-secondary {
    line-height: 1.4;
}

/* -----------------------------------------------------------------------------
   A SECTION INSIDE A CARD.

   THE THREE LEVELS, AND WHAT EACH ONE OWNS:

     .page-main   the content area. Owns the gutter at the window edge and the
                  width of the column, both set in site edit mode.
     .card        the background. Owns the inset between its edge and its words,
                  which is --container-padding, also set in site edit mode.
     .card__section  a related group inside a card. Owns nothing: it takes the
                  card's inset and only says where one group ends and the next
                  begins.

   A SECTION DRAWS NO BOX. It is a division of a card, not a card of its own, and
   giving it a background would put a panel inside a panel -- the same mistake the
   heading row and the modal both avoid. A rule between siblings is the whole of
   it, and the first one needs none because the card's title is already above it.
   -------------------------------------------------------------------------- */

.card__section ~ .card__section {
    margin-top: var(--space-large);
    padding-top: var(--space-large);
    border-top: 1px solid var(--colour-border);
}

/* A SUB HEADING INSIDE A CARD. Every card on this site is a section.card with
   an h2.card__title, and anything needing to be divided further uses this for
   its h3. It was called .permission-category, which was true on the groups
   screen and a small lie on the six other screens that had borrowed it for the
   same job. The specific name is kept as an alias for the screen it describes. */
.card__section-title,
.permission-category {
    margin: var(--space-large) 0 var(--space-small);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--colour-text-secondary);
}

/* The first sub heading in a card already has the card's title above it, so it
   does not need the full gap that separates one section from the next. */
.card__title + .card__section-title,
.card__subtitle + .card__section-title {
    margin-top: var(--space-medium);
}

.permission-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-medium);
    flex-wrap: wrap;

    padding: var(--space-small) 0;
    border-bottom: 1px solid var(--colour-border);
}

.permission-row__label {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    flex: 1 1 18rem;
    font-size: 0.9rem;
}

.permission-row__label .text-secondary {
    line-height: 1.4;
}

.permission-row__select {
    flex: 0 0 13rem;
    width: 13rem;
}

.permission-summary {
    margin: 0;
    padding-left: 1.1rem;
    font-size: 0.925rem;
}

.permission-summary code {
    color: var(--colour-text-secondary);
    font-family: var(--font-family-mono);
    font-size: 0.8rem;
}

/* A destructive action, coloured so it is never pressed absent-mindedly. */
.button--danger {
    background-color: var(--colour-error);
    color: #ffffff;
}

/* A filled surface that is not the accent darkens itself rather than naming a
   second colour, so it follows whatever its base colour is. */
.button--danger:hover {
    background-color: var(--colour-error);
    color: #ffffff;
    filter: brightness(0.9);
}

@media (max-width: 40rem) {
    .permission-row__select {
        flex: 1 1 100%;
        width: 100%;
    }

    .table-scroll {
        margin: 0 calc(var(--space-medium) * -1);
        padding: 0 var(--space-medium);
    }
}

/* -----------------------------------------------------------------------------
   13. The tooltip overlay
      One floating layer shared by styling mode and tutorial mode. It is
      positioned by assets/js/tooltip_overlay.js, which writes only left and top;
      everything about how it looks is here.
   -------------------------------------------------------------------------- */

.tooltip-overlay {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 1200;

    /* Hidden until it is shown, and out of the way of the mouse until it is
       pinned, so a following tooltip can never block what it describes. */
    display: none;
    pointer-events: none;

    max-width: min(20rem, calc(100vw - 2rem));
    max-height: calc(100vh - 2rem);
    overflow-y: auto;

    padding: 0.6rem 0.75rem;

    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--colour-text-primary);
    background-color: var(--colour-card-background);

    border: 1px solid var(--colour-border-strong);
    border-radius: var(--radius-small);
    box-shadow: 0 8px 28px rgba(16, 24, 40, 0.26);
}

.tooltip-overlay--visible {
    display: block;
}

/* Pinned: it stays where it was put and takes the mouse, so the controls
   inside it can be reached. */
.tooltip-overlay--locked {
    pointer-events: auto;
    cursor: auto;
    border-color: var(--colour-accent);
    box-shadow: 0 10px 34px rgba(16, 24, 40, 0.34);
}

/* The wrapper the tooltip layer replaces on every setContent(). Nothing is
   styled on it directly; it exists so the layer has one node to empty. */
.tooltip-overlay__content {
    display: block;
}

.tooltip-overlay__title {
    margin: 0 0 0.35rem;
    font-size: 0.8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--colour-text-secondary);
}

.tooltip-overlay__text {
    margin: 0;
}

.tooltip-overlay__hint {
    margin: 0.5rem 0 0;
    font-size: 0.775rem;
    color: var(--colour-text-secondary);
}

.tooltip-overlay__hint:empty {
    display: none;
}

/* Tutorial tooltips are only ever read, so they stay narrow and quiet. */
.tooltip-overlay--tutorial {
    max-width: min(18rem, calc(100vw - 2rem));
}

/* -----------------------------------------------------------------------------
   14. Controls inside a styling tooltip
   -------------------------------------------------------------------------- */

.tooltip-overlay__field ~ .tooltip-overlay__field {
    margin-top: 0.5rem;
}

.tooltip-overlay__label {
    display: block;
    margin-bottom: 0.15rem;
    font-size: 0.8rem;
    font-weight: 600;
}

.tooltip-overlay__colour {
    width: 100%;
    height: 2rem;
    padding: 2px;
    background-color: var(--colour-input-background);
    border: 1px solid var(--colour-border-strong);
    border-radius: var(--radius-small);
    cursor: pointer;
}

.tooltip-overlay .form-field__input {
    padding: 0.35rem 0.5rem;
    font-size: 0.85rem;
}

.tooltip-overlay__size-row {
    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.tooltip-overlay__size-row input[type="range"] {
    flex: 1 1 auto;
    min-width: 0;
}

.tooltip-overlay__size-value {
    flex: 0 0 3rem;
    text-align: right;
    font-family: var(--font-family-mono);
    font-size: 0.8rem;
    color: var(--colour-text-secondary);
}

.tooltip-overlay__actions {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    flex-wrap: wrap;
    margin-top: 0.6rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--colour-border);
}

/* -----------------------------------------------------------------------------
   The controls site edit mode puts in the tooltip

   THE SAME TOOLTIP THE STYLING MANAGER USES, holding a site setting rather than
   a colour. A dropdown, a line of text and a file picker are all that the
   settings need on top of the sliders and pickers styling mode already built,
   so this is the whole of the addition.
   -------------------------------------------------------------------------- */

.tooltip-overlay__select,
.tooltip-overlay__file {
    display: block;
    width: 100%;
    padding: 0.3rem 0.4rem;

    font: inherit;
    font-size: 0.82rem;
    color: var(--colour-text-primary);
    background-color: var(--colour-input-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* A file input draws its own button, and the border around it doubles it up. */
.tooltip-overlay__file {
    padding: 0.25rem 0;
    background: none;
    border: 0;
}

.tooltip-overlay__field-stack {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    flex-wrap: wrap;
}

.tooltip-overlay__field-help {
    margin: 0.2rem 0 0;
    font-size: 0.74rem;
    line-height: 1.4;
    color: var(--colour-text-secondary);
}

/* A refused save has to look different from a saved one, or the line that says
   "Saved" and the line that says why it was not both read the same. */
.tooltip-overlay__hint--problem {
    color: var(--colour-danger, #b3261e);
    font-weight: 600;
}

/* While site edit mode is on, the cursor says a right click will offer
   something. Left click is left alone, because it still navigates. */
.site-edit-mode-active {
    cursor: context-menu;
}

/* -----------------------------------------------------------------------------
   The Site identity card

   THE LOGO, BANNER, NAME AND BYLINE ARE SET HERE, not by right clicking them.
   Any of the four can be switched off or left empty, and something that is not
   on the screen cannot be right clicked -- which is the moment somebody goes
   looking for the control.
   -------------------------------------------------------------------------- */

.site-identity__preview {
    display: block;
    max-width: 12rem;
    max-height: 6rem;
    margin-bottom: var(--space-extra-small);

    background-color: var(--colour-page-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

.site-identity__slider {
    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.site-identity__slider input[type="range"] {
    flex: 1 1 auto;
    min-width: 0;
}

.site-identity__slider-value {
    flex: 0 0 3rem;
    text-align: right;
    font-family: var(--font-family-mono);
    font-size: 0.8rem;
    color: var(--colour-text-secondary);
}

.site-identity__remove {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);

    margin-top: var(--space-extra-small);
    font-size: var(--font-size-quiet);
    color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   15. Styling mode and tutorial mode
   -------------------------------------------------------------------------- */

/* While styling mode is on, the cursor says that a click picks rather than acts. */
.style-mode-active,
.style-mode-active a,
.style-mode-active button {
    cursor: crosshair;
}

/* The part of the page currently under the cursor or pinned for editing. */
.style-mode-selected {
    outline: 3px dashed var(--colour-accent);
    outline-offset: 2px;
}

/* The strip across the top of the window while styling mode is on. */
/* ONE STRIP, WHICHEVER MODE IS ON. Tutorial mode, styling mode and the styling
   manager all put their line here; only the colour and the words differ. See
   assets/js/mode_banner.js, which also enforces one mode at a time.

   A ROW OF THE PAGE RATHER THAN A BAR OVER IT. The body is a fixed height flex
   column and the content area is the only thing that scrolls, so a fixed strip
   would cover the top of the header for as long as the mode was on. As a row it
   takes its own space and everything below simply gets less. */
.mode-banner {
    flex-shrink: 0;

    display: flex;
    align-items: center;
    gap: var(--space-small) var(--space-medium);
    flex-wrap: wrap;

    padding: 0.5rem var(--space-medium);
    cursor: auto;

    font-size: 0.85rem;
    color: var(--colour-text-inverse);
    background-color: var(--colour-info);
}

/* Each mode keeps its own colour, so which one is on is legible at a glance
   rather than only readable. */
.mode-banner--styling {
    background-color: var(--colour-info);
}

.mode-banner--styling-manager {
    background-color: var(--colour-accent);
}

.mode-banner--tutorial {
    background-color: var(--colour-success);
}

.mode-banner__label {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.mode-banner__message {
    flex: 1 1 12rem;
    opacity: 0.95;
}

.mode-banner__actions {
    display: flex;
    align-items: center;
    gap: var(--space-small);

    /* Pushed to the right hand end, however long the message is. */
    margin-left: auto;
}

/* The buttons sit on the banner's own colour rather than the page's, so they
   are outlined in what they sit on instead of carrying a second background. */
.mode-banner .button {
    color: inherit;
    background-color: transparent;
    border-color: currentColor;
}

.mode-banner .button:hover {
    background-color: rgba(255, 255, 255, 0.18);
}

/* While tutorial mode is on, the cursor says the page is explaining itself. */
.tutorial-mode-active [data-tutorial] {
    cursor: help;
}

/* A toggle that is currently on. */
.button--active {
    background-color: var(--colour-accent-soft);
    color: var(--colour-accent);
}

.button--quiet.button--active {
    text-decoration: underline;
}

@media (max-width: 40rem) {
    .mode-banner {
        font-size: 0.8rem;
    }
}

/* -----------------------------------------------------------------------------
   16. Styling mode indicators
      Two strengths of outline: a solid one on the element under the cursor, and
      a faint one on everything else the same change would reach. Seeing the
      whole set before committing is the point.
   -------------------------------------------------------------------------- */

.style-mode-affected {
    outline: 2px dotted var(--colour-accent);
    outline-offset: 1px;
}

/* The element actually under the cursor wins, so it is always distinguishable
   from the others it belongs with. */
.style-mode-selected {
    outline: 3px dashed var(--colour-accent);
    outline-offset: 2px;
}

.tooltip-overlay__scope {
    margin: 0 0 0.35rem;
    font-size: 0.78rem;
    line-height: 1.4;
    color: var(--colour-text-secondary);
}

/* THE TRAIL OF STYLING CLASSES a click was inside, most specific first. It is
   not a list of the element's classes: most classes are modifiers, utilities
   and script hooks, and offering those would mean reading CSS to find the one
   that means anything. These are the registered styling classes, named in
   words. */
.tooltip-overlay__trail {
    display: flex;
    flex-wrap: wrap;
    gap: 0.2rem;

    margin-bottom: 0.5rem;
}

.tooltip-overlay__trail-step {
    padding: 0.1rem 0.4rem;

    font-size: 0.7rem;
    font-family: inherit;
    cursor: pointer;

    color: var(--colour-text-secondary);
    background-color: transparent;
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

.tooltip-overlay__trail-step:hover {
    border-color: var(--colour-accent);
    color: var(--colour-text-primary);
}

/* The one being edited. Marked by weight and colour rather than by colour
   alone, so it is still obvious to somebody who cannot tell the two apart. */
.tooltip-overlay__trail-step--chosen {
    font-weight: 700;
    color: var(--colour-text-inverse);
    background-color: var(--colour-accent);
    border-color: var(--colour-accent);
}

/* HOW READABLE THE TEXT ON THIS SURFACE IS, shown whether or not it is a
   problem. A number that only appears when something is wrong is a number
   nobody learns to read. */
.tooltip-overlay__contrast {
    margin: 0.35rem 0 0;
    padding-top: 0.35rem;
    border-top: 1px solid var(--colour-border);

    font-size: 0.7rem;
    color: var(--colour-text-secondary);
}

.tooltip-overlay__contrast--poor {
    font-weight: 700;
    color: var(--colour-error);
}

.tooltip-overlay__count {
    margin: 0 0 0.5rem;
    padding-bottom: 0.4rem;
    border-bottom: 1px solid var(--colour-border);

    font-size: 0.75rem;
    font-weight: 600;
    color: var(--colour-accent);
}

/* TUTORIAL MODE MARKS WHAT IT CAN EXPLAIN, for the same reason styling mode
   marks what it can restyle: without it the only way to find an explanation is
   to sweep the cursor across the page and hope. The outline is a dotted one to
   match .style-mode-affected, because it means the same thing in both modes,
   "there is something here", and having two modes draw that idea two different
   ways would only be confusing.

   Nested annotated elements both show, which is correct: a card can explain
   itself while a button inside it explains something more specific. */
.tutorial-mode-active [data-tutorial],
.tutorial-mode-active [data-tutorial-custom] {
    outline: 2px dotted var(--colour-accent);
    outline-offset: 1px;
}

/* The element an administrator is writing about, marked the way styling mode
   marks the element it has selected. */
.tutorial-mode-active .tutorial-mode-editing {
    outline: 3px dashed var(--colour-accent);
    outline-offset: 2px;
}

/* While an administrator is choosing an element, the cursor says that clicking
   will pick something rather than follow it. */
.tutorial-mode-active[data-tutorial-editing="yes"] {
    cursor: help;
}

/* -----------------------------------------------------------------------------
   16b. The tutorial tooltip editor
      Only an administrator ever sees this. It is a form inside a tooltip, so
      the controls are small and stacked rather than laid out in a grid.
   -------------------------------------------------------------------------- */

.tooltip-editor__row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-small);

    margin-bottom: var(--space-small);
    padding-bottom: var(--space-small);
    border-bottom: 1px solid var(--colour-border);
}

/* The text takes whatever room is left once the Remove button has its own. */
.tooltip-editor__row .tooltip-overlay__text {
    flex: 1 1 auto;
    margin: 0;
}

.tooltip-editor__row .button {
    flex-shrink: 0;
}

.tooltip-editor__form {
    display: flex;
    flex-direction: column;
    gap: var(--space-extra-small);

    margin-bottom: var(--space-small);
}

.tooltip-editor__form textarea {
    resize: vertical;
    min-height: 3.5rem;
}

/* A value with a control beside it, such as the email address and the button
   that opens the form for changing it. The button keeps its own size and the
   value takes what is left, so a long address wraps rather than pushing the
   button off the row. */
.detail-list__value-with-action {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-small);
}

/* A block of code or configuration meant to be read and copied. Scrolls
   sideways rather than wrapping, because a wrapped line of code is a line that
   reads as two and pastes as one. */
.code-block {
    margin: var(--space-small) 0;
    padding: var(--container-padding-tight);
    overflow-x: auto;

    font-family: var(--font-family-mono);
    font-size: 0.8rem;
    line-height: 1.5;

    color: var(--colour-text-primary);
    background-color: var(--colour-page-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

.code-block code {
    font-family: inherit;
    white-space: pre;
}

/* Hides an element outright: no space, and out of the tab order, which matters
   because a form somebody cannot see is a form they should not be able to tab
   into. Used for the parts of a screen that only apply part of the time. */
/* WHATEVER ELSE AN ELEMENT IS, HIDDEN WINS. .avatar-builder is a grid and
   also starts hidden, and two single class selectors are decided by which was
   written last -- which is a fragile thing for "do not draw this" to depend on. */
.is-hidden {
    display: none !important;
}

/* THE RUNNING VERDICT UNDER A FIELD, used by every field that reports on
   itself: the username, the email address, the password and its confirmation.
   One appearance for all of them, because they are one idea.

   Colour alone does not carry the message. The sentence says "is available" or
   names the problem outright, so it reads the same to somebody who cannot tell
   the two colours apart, and the same again to a screen reader through the
   aria-live on the element.

   The reserved height stops the fields below shifting down the moment a verdict
   appears, which on a form somebody is typing into is enough to make them click
   the wrong thing. */
.form-field__notice {
    min-height: 1.2em;
    margin: calc(var(--space-extra-small) * -1) 0 var(--space-small);

    font-size: var(--font-size-quiet);
    line-height: 1.4;
    color: var(--colour-text-secondary);
}

.form-field__notice--good {
    color: var(--colour-success);
    font-weight: 600;
}

.form-field__notice--problem {
    color: var(--colour-error);
    font-weight: 600;
}

/* While an answer is being waited for. Deliberately quiet: it is not news. */
.form-field__notice--checking {
    color: var(--colour-text-secondary);
    font-style: italic;
}

/* -----------------------------------------------------------------------------
   16c. The location fields
      Country, state, county and postal code. Which of them appear depends on
      the country chosen, and assets/js/location_fields.js decides; all this
      does is provide the off state.
   -------------------------------------------------------------------------- */

/* display: none rather than visibility or opacity, deliberately. A hidden field
   should take no space and, more importantly, should be out of the tab order:
   tabbing into a field nobody can see is a small nightmare for anybody using a
   keyboard or a screen reader. The value is cleared by the script as well, so a
   hidden field never contributes to what is submitted. */
/* TWO FIELDS ABREAST WHERE THERE IS ROOM. A country menu and a postal code are
   one thought and both are short, so stacking them left a column of two lonely
   controls down the side of the card. auto-fit rather than a fixed two, so the
   pair drops to one column on a narrow screen without a breakpoint of its own --
   and so that a field returning later needs no change here. */
.location-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--space-medium);
    align-items: start;
}

.location-fields__part--hidden {
    display: none;
}

/* -----------------------------------------------------------------------------
   17. Avatars and profiles
   -------------------------------------------------------------------------- */

/* AN AVATAR IS A TILE, NOT A DISC, and its corners are the site's corners.
   A circle was one shape on the site that ignored the corner rounding setting,
   so a squared off site still had round pictures in its header; taking the
   radius from the same token every card and button uses means "Square" is
   square everywhere and "Very rounded" is soft everywhere. */
.avatar {
    display: inline-block;
    flex-shrink: 0;
    vertical-align: middle;

    border-radius: var(--radius-small);
    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    object-fit: cover;

    /* The built in avatars are 16x16 pixel art, so they are scaled up with
       hard edges rather than blurred into mush. */
    image-rendering: pixelated;
}

.avatar--small  { width: 2rem;   height: 2rem; }
.avatar--medium { width: 3rem;   height: 3rem; }
.avatar--large  { width: 4.5rem; height: 4.5rem; }

/* Somebody who has chosen no picture gets their initial on a coloured tile. */
.avatar--initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;

    font-weight: 700;
    line-height: 1;
    color: #ffffff;
    border-color: transparent;
    image-rendering: auto;
}

.avatar--small.avatar--initial  { font-size: 0.9rem; }
.avatar--medium.avatar--initial { font-size: 1.3rem; }
.avatar--large.avatar--initial  { font-size: 2rem; }

/* The tile colours, chosen from the address so a person keeps the same one.
   They are classes rather than inline styles because the Content Security
   Policy forbids a style attribute. */
.avatar--colour-1 { background-color: #2f5bd7; }
.avatar--colour-2 { background-color: #17663f; }
.avatar--colour-3 { background-color: #a3232b; }
.avatar--colour-4 { background-color: #7a4bbd; }
.avatar--colour-5 { background-color: #1f5673; }
.avatar--colour-6 { background-color: #8a5a00; }
.avatar--colour-7 { background-color: #b03a6e; }
.avatar--colour-8 { background-color: #3f6b45; }

/* THE HEADER AVATAR IS NOT A LINK OF ITS OWN ANY MORE. It was a separate item
   at one end of the bar, next to a menu button that already carried the same
   picture; there are not two of them now. It lives inside the button, and the
   rules that give it the height of the bar are with the menu in section 22. */

/* A heading with the avatar beside it. */
.page-heading-row {
    display: flex;
    align-items: center;
    gap: var(--space-medium);
    flex-wrap: wrap;
    margin-bottom: var(--space-large);
}

/* THE WORDS TAKE WHAT THE AVATAR LEAVES, AND SHRINK RATHER THAN WRAP. A basis
   of auto measures the text first, so on a phone the block was wider than the
   room left beside the picture and dropped below it -- which put the avatar
   above the heading instead of beside it. A basis of zero lets it take whatever
   is left and wrap its own lines inside that. */
.page-heading-row__text {
    flex: 1 1 0;
    min-width: 0;
}

/* THE CARD IS THE PANEL NOW, so the heading and its subtitle drop their own.
   They carry a background, a border and a shadow of their own everywhere else
   -- on an admin screen the heading IS the panel -- and keeping that here would
   draw a second bordered box inside the first one.

   Only the padding of the card is left doing the work, which is what makes the
   avatar and the words share one inset rather than each having their own. */
.page-heading-row .page-title,
.page-heading-row .page-subtitle {
    margin-bottom: 0;
    padding: 0;

    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* -----------------------------------------------------------------------------
   18. The avatar chooser
   -------------------------------------------------------------------------- */

.avatar-picker {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
    gap: var(--space-small);
    margin-top: var(--space-extra-small);
}

.avatar-picker__option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-extra-small);

    padding: var(--space-small) var(--space-extra-small);
    text-align: center;
    cursor: pointer;

    border: 2px solid transparent;
    border-radius: var(--radius-small);
}

.avatar-picker__option:hover {
    border-color: var(--colour-accent);
    background-color: var(--colour-accent-soft);
}

/* The picture is the label, so the radio itself is small and quiet. It stays in
   the document rather than being hidden, so it can still be tabbed to. */
.avatar-picker__option input[type="radio"] {
    margin: 0;
}

.avatar-picker__option:focus-within {
    border-color: var(--colour-accent);
}

/* The chosen one. :has() is not relied on: browsers without it simply show the
   checked radio, which is still unambiguous. */
.avatar-picker__option:has(input:checked) {
    border-color: var(--colour-accent);
    background-color: var(--colour-accent-soft);
}

.avatar-picker__caption {
    font-size: 0.75rem;
    line-height: 1.3;
    color: var(--colour-text-secondary);
    word-break: break-word;
}

/* -----------------------------------------------------------------------------
   19. Mode toggles
      Styling mode and tutorial mode sit together on the account screen and are
      presented alike: a name, what it does, and the switch. Styling mode has a
      second button because it has something to undo; tutorial mode has nothing
      to undo, so it has one.
   -------------------------------------------------------------------------- */

.mode-toggle {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-medium);
    flex-wrap: wrap;

    padding: var(--space-medium) 0;
    border-top: 1px solid var(--colour-border);
}

.card__title + .style-presets,
.card__title + .mode-toggle,
.card__subtitle + .style-presets,
.card__subtitle + .mode-toggle {
    border-top: 0;
    padding-top: var(--space-small);
}

/* A subtitle with its control beside it, for a card whose whole content is one
   switch. Without this the switch sits under a second heading that says again
   what the subtitle already said. It wraps below the text on a narrow window,
   because a button squeezed beside a paragraph is worse than one under it. */
.card__subtitle-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-medium);
    flex-wrap: wrap;
}

.card__subtitle-row .card__subtitle {
    /* The row is the last thing in the card, so the trailing margin would be
       space under it rather than between it and anything. */
    flex: 1 1 20rem;
    margin-bottom: 0;
}

.card__subtitle-row .button {
    flex: 0 0 auto;
}

.mode-toggle__description {
    display: flex;
    flex-direction: column;
    gap: var(--space-extra-small);
    flex: 1 1 18rem;
}

.mode-toggle__description .text-secondary {
    line-height: 1.45;
}

.mode-toggle__actions {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    flex-wrap: wrap;

    /* NOT SHRUNK, BUT NOT ALLOWED PAST THE EDGE EITHER. flex-shrink: 0 keeps
       the buttons their proper size next to a long description instead of being
       squeezed into two narrow slivers. On its own, though, it also sized this
       box to whatever its buttons added up to, so on a phone the second button
       hung off the side of the screen: the wrap above never engaged, because a
       box that is allowed to be as wide as its contents never needs to wrap.

       The cap is what makes the wrap work. Wide enough, the buttons sit in a
       row; too narrow, they stack. */
    flex-shrink: 0;
    max-width: 100%;
}

/* -----------------------------------------------------------------------------
   20. Header navigation
      Every item in the header is a link, so they are all styled as one thing.
      The one that stands out is still a link; it is not a button wearing a
      link's job.
   -------------------------------------------------------------------------- */

.site-header__link {
    padding: 0.35rem 0.6rem;
    border-radius: var(--radius-small);

    font-size: 0.95rem;
    font-weight: 600;

    /* THE PANEL'S COLOUR, NOT THE BAR'S. Every one of these is a row inside the
       dropdown; none of them is in the header bar any more. See the token. */
    color: var(--colour-header-menu-text);
    white-space: nowrap;
}

.site-header__link:hover,
.site-header__link:focus-visible {
    color: var(--colour-header-menu-text);

    /* Still the neutral wash, which needs no help: being half way between black
       and white it darkens a light panel and lightens a dark one, so it works
       over whatever colour the menu has been given. */
    background-color: var(--colour-header-hover-surface);
    text-decoration: none;
}

/* The call to action. Still a link: it navigates, so it behaves like one. */
.site-header__link--primary {
    color: var(--colour-button-text);
    background-color: var(--colour-accent);
}

.site-header__link--primary:hover,
.site-header__link--primary:focus-visible {
    color: var(--colour-button-text);
    background-color: var(--colour-accent-hover);
}

/* -----------------------------------------------------------------------------
   21. Site header: the media layer and the overlay
      The header is two layers. .site-header__media holds the pictures, drawn as
      backgrounds so they can be tiled as well as filled or stretched;
      .site-header__overlay holds the name, byline and menu, laid over the top.

      Keeping them apart is what lets a banner be any height and a logo any size
      without either pushing the navigation around. Every size arrives as a
      custom property resolved in includes/site_settings_service.php against one
      512 pixel maximum, so no combination of settings can produce a header that
      swallows the page.
   -------------------------------------------------------------------------- */

.site-header__stage {
    position: relative;
}

/* With pictures behind it, the header is at least as tall as the banner. */
.site-header__stage--with-media {
    min-height: var(--site-banner-height);
}

.site-header__media {
    position: absolute;
    inset: 0;
    overflow: hidden;

    /* Decoration. Clicks belong to the layer above. */
    pointer-events: none;
}

/* Where the gutter falls, worked out once and used by everything in the header
   that has to line up with the page content: the logo, both edges of the
   banner, and the step that carries the name and byline past the logo.
   
   It is the same sum the browser does for .site-header__container: half the space
   left over once the max-width box is centred, plus that box's padding. max()
   covers a window narrower than the maximum width, where there is no leftover
   space and the padding is the whole of it.
   
   The 100% inside it resolves against whichever element uses the property, and
   the pictures are positioned in the same containing block, so they all get the
   same answer. */
.site-header {
    --site-header-gutter: max(
        var(--space-medium),
        calc((100% - var(--layout-maximum-width)) / 2 + var(--container-padding))
    );

    /* Declared here, beside the gutter it follows, so that the edge-to-edge
       rule below can override it. Declared on .site-header__banner-area
       instead it would win over anything inherited and the override would
       never arrive. */
    --site-banner-inset: var(--site-header-gutter);
}

/* Gutters off means the content runs to the edge of the window, so the logo
   goes with it and only the padding separates it from the edge. */
.site-header--edge-to-edge {
    --site-header-gutter: var(--container-padding);

    /* The banner is the exception. "Run to the edge of the screen" is the whole
       point of the setting, and a picture stopped 16 pixels short of the edge
       is not at the edge: it is a picture with a white stripe beside it. So the
       banner takes the window edge itself while the logo keeps its padding. */
    --site-banner-inset: 0px;
}

/* WHERE THE BANNER SITS. It is its own container, spanning gutter to gutter, and
   nothing about the logo enters into that. The logo is drawn on top of it at
   the same left gutter, so the two start at the same place and the banner
   simply carries on underneath; the alternative, starting the banner where the
   logo ends, made the logo's size decide the banner's width, which meant
   resizing one picture silently resized the other.

   With gutters on, both edges land on the page content. With them off, the
   override above takes the banner to the window edge. */
.site-header__banner-area {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--site-banner-inset);
    right: var(--site-banner-inset);
}

/* The banner width setting is a percentage of that area, so at 100 it fills the
   full width between the gutters. */
.site-header__banner {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: var(--site-banner-width);

    background-image: var(--site-banner-image);
    background-position: var(--site-banner-position);
    background-repeat: no-repeat;
    background-size: cover;
}

/* Square, vertically centred in whatever height the header has, and lined up
   with the left gutter.

   The media layer spans the whole header, but the content above it does not:
   with gutters switched on, .site-header__container is a max-width box centred in
   the header with its own padding. So the logo cannot simply sit a fixed
   distance from the header's edge, or it would drift out of line with the site
   name beside it as the window widens.

   The calculation is the same one the browser does for that centred box: half
   the leftover space, plus the padding. max() covers the case where the window
   is narrower than the maximum width, when there is no leftover space and the
   padding is the whole of it. Setting --layout-maximum-width to 100% in site
   edit mode collapses the first term to zero, which lands on the same answer. */
/* The box the logo is drawn in, sized by the setting and holding its place in
   the row whatever shape the picture inside it turns out to be. */
/* NEVER TALLER THAN THE BAR. The logo size is a setting that goes well past
   the header's height, and with the height fixed an oversized one would hang out
   of it. Capped, it stays square and stays inside. */
.site-header__logo-area {
    --site-logo-drawn: min(var(--site-logo-size), var(--site-header-button-height));

    flex: 0 0 auto;
    display: block;

    width: var(--site-logo-drawn);
    height: var(--site-logo-drawn);
}

.site-header__logo {
    display: block;
    width: 100%;
    height: 100%;

    background-image: var(--site-logo-image);
    background-repeat: no-repeat;
    background-size: contain;

    /* ANCHORED LEFT, NOT CENTRED. The box above is square and lands exactly on
       the gutter, but contain fits the picture inside that box by its longer
       side, so a picture taller than it is wide comes out narrower than the box
       and the leftover space is split evenly. Centred, half of it sits between
       the gutter and the first pixel of the logo, and reads as padding nobody
       asked for. Anchored left, the logo starts at the gutter and any leftover
       air falls on the right, where there is nothing to line up against.

       The crop focus is deliberately not used here. It says which part of a
       picture to keep when the box has to cut some away, and contain never cuts
       anything away, so honouring it would only move a picture that fits. */
    background-position: left center;
}

/* Fill crops to cover the box; stretch distorts to fit it exactly; tile repeats
   the picture at its own size. The logo defaults to contain rather than cover,
   because a logo cropped at the edges is usually worse than one with air
   around it. */
.site-header--logo-fit-fill .site-header__logo {
    background-size: cover;

    /* Fill is the one mode that crops, so it is the one mode where the focus
       chosen by dragging the picture in site edit mode means anything. */
    background-position: var(--site-logo-position);
}

.site-header--logo-fit-stretch .site-header__logo {
    background-size: 100% 100%;
}

.site-header--logo-fit-tile .site-header__logo {
    background-repeat: repeat;
    background-size: auto;

    /* Tiling from the corner puts a whole tile against the gutter. Tiling from
       the centre puts a sliver of one there instead, which looks like a seam. */
    background-position: left top;
}

.site-header--banner-fit-fill .site-header__banner {
    background-size: cover;
}

.site-header--banner-fit-stretch .site-header__banner {
    background-size: 100% 100%;
}

.site-header--banner-fit-tile .site-header__banner {
    background-repeat: repeat;
    background-size: auto;

    /* Same reasoning as the logo: a whole tile against the left edge rather
       than a sliver of one. */
    background-position: left top;
}

/* The overlay sits above the pictures and holds everything interactive. */
.site-header__overlay {
    position: relative;
    z-index: 1;
    height: 100%;
}

.site-header__stage--with-media .site-header__overlay {
    display: flex;
    align-items: center;
    min-height: var(--site-banner-height);
}

/* The wordmark takes no notice of the logo.

   The two are on different layers by design: the pictures are a backdrop and
   the name, byline and menu float above them. Padding the wordmark clear of the
   logo tied one layer to the other, so resizing the logo shunted the text
   sideways, which is exactly what a separate container is supposed to prevent.

   The wordmark therefore starts at the gutter, like every other piece of
   content on the page, whatever size the logo happens to be. For a logo that
   already carries the name, switch "Show the site name" off in site edit mode
   rather than trying to arrange the two around each other. */

/* THE WORDMARK IS ITS OWN THING TO STYLE. Everything in here draws on tokens
   that belong to the site name and byline alone, so a visitor can make them
   legible over a banner picture without dragging the rest of the site with
   them. Before this, the byline took --colour-text-secondary, which meant the
   only way to lift it off a dark banner was to recolour every subtitle, help
   note and table caption on the site at the same time.

   The font is set here rather than on each line so the two read as one mark. */
.site-header__wordmark {
    display: flex;
    flex-direction: column;
    line-height: 1.2;

    font-family: var(--font-family-wordmark);
}

.site-header__name {
    font-size: var(--font-size-site-name);
    font-weight: inherit;
    color: var(--colour-site-name);
}

/* The brand is a link home, and it dims on hover like any other. The name now
   carries its own colour, so it no longer inherits that hover from the anchor
   and has to be told about it directly, or the affordance quietly disappears.
   The byline is left alone, which is what it did before this change too. */
.site-header__branding:hover .site-header__name {
    color: var(--colour-site-name);
}

.site-header__byline {
    font-size: var(--font-size-site-byline);
    font-weight: 400;
    color: var(--colour-site-byline);
}

/* Compact: half the height, no banner, and a logo scaled to fit the bar. */
/* A smaller logo for the compact header. This sets the size property rather
   than the logo's width and height directly, so everything measured from that
   property, the brand's offset included, follows it down. */
.site-header--compact {
    --site-logo-size: 2rem;
}

.site-header--compact .site-header__stage--with-media,
.site-header--compact .site-header__stage--with-media .site-header__overlay {
    min-height: 0;
}

/* Hidden: gone for everybody, but shown again, faded and labelled, to whoever
   is in site edit mode. Otherwise hiding the header would hide the controls
   for unhiding it. */
.site-header--hidden,
.site-footer--hidden {
    display: none;
}

.site-edit-mode-active .site-header--hidden,
.site-edit-mode-active .site-footer--hidden {
    display: block;
    opacity: 0.75;
    outline: 2px dashed var(--colour-text-secondary);
    outline-offset: -2px;
}

.site-edit-mode-active .site-header--hidden::before,
.site-edit-mode-active .site-footer--hidden::before {
    content: 'Hidden. Only you can see this.';
    display: block;
    padding: 0.15rem var(--space-medium);
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--colour-text-inverse);
    background-color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   22. Where the menu sits
      The button and the menu it opens are one wrapper, and this moves the
      wrapper. Nothing here positions the dropdown: it hangs off the button by
      construction, so it follows the alignment for free.
   -------------------------------------------------------------------------- */

/* THE BRANDING IS A ROW: the logo, then the name and byline beside it. */
.site-header__branding {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    min-width: 0;
}

/* THE MENU HOLDS AN END OF THE BAR, AND IS THE HEIGHT OF THE BUTTON IN IT.

   IT USED TO CLIMB OUT OF THE BAR'S PADDING on both axes, with negative margins
   the exact size of the inset, so the picture met the edge of the header itself.
   That was written when the bar carried the inset that lines the header up with
   the page, and reaching the header's edge meant reaching the content column's.
   The bar's inset is a few pixels now and the alignment is the container's job,
   so there is nothing left worth escaping: the menu sits inside the padding like
   everything else, which is what full height was asked to mean. */
.site-header__menu {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-small);

    /* FITS ITS CONTENTS. The branding takes whatever is left and shrinks into
       it, which is what its own min-width: 0 is for. On a wide window there is
       room for a long name and no reason to cut one short; the cap that stops it
       taking the bar is below, at the widths where the bar is short of room. */
    flex: 0 0 auto;
    width: max-content;

    /* THE BAR LESS THE GAP, NOT THE WHOLE BAR. The branding shrinks to nothing
       before the button gives way, but the gap between them stays -- a gap
       applies between two flex items however wide they are. A plain 100% here
       therefore let the button start one gap in and end one gap past the bar's
       inset, sitting flush against the edge of the screen. Only reachable with
       the width setting near its maximum on a very narrow phone, which is
       exactly when somebody is least able to spare the pixels. */
    max-width: calc(100% - var(--site-header-bar-gap));

    /* THE GAP BETWEEN THE BUTTON AND THE BAR AROUND IT. At nothing, which is the
       default, this box is the button and the header is what it always was.
       Given a number it holds the button in from every side, and the header
       grows by two of them.

       THE HEIGHT ADDS IT BACK RATHER THAN ABSORBING IT. Every box on the site is
       border-box, so a bare height here would take the padding out of the middle
       and leave the button -- which is a declared sixty four pixels -- hanging
       out of the bottom of its own wrapper. Adding it means the button keeps its
       height and the wrapper grows around it, which is what the setting says it
       does. */
    padding: var(--site-header-menu-padding);

    height: calc(
        var(--site-header-button-height)
        + (var(--site-header-menu-padding) * 2)
    );
}

/* ORDER, NOT FLOAT, DECIDES THE SIDE. The menu position an administrator chose
   in site edit mode is already on the header as a class, and this puts the whole
   block -- button, name and picture -- ahead of the branding so it holds the
   other end of the bar. The centre setting leaves it on the right, because a bar
   with something at both ends and nothing in the middle is what reads. */
.site-header--nav-left .site-header__menu {
    order: -1;
}

/* THE SQUARE THE WHOLE HEADER IS BUILT FROM. It was a subtraction here -- the
   button's declared height less its inset -- and it is a plain number now,
   because the subtraction runs the other way: the button adds its inset to this.

   IT IS A NUMBER RATHER THAN A RATIO, WHICH HAS NOT CHANGED. An aspect-ratio
   would be waiting on a height that flexbox settles after the widths, which has
   already gone wrong twice on this element. A percentage height would be no
   better, resolving against the same stretch that is settled too late. */
.site-header__menu-avatar {
    flex: 0 0 auto;
    display: block;

    width: var(--site-header-avatar-drawn);
    height: var(--site-header-avatar-drawn);
}

/* ALL FOUR CORNERS THE SAME. The button used to clip this to its own curve,
   which left the two outer corners rounded to one radius and the two inner ones
   to another -- a square with two shapes. It carries the site's own small radius
   now, on every corner, and the button matches it so nothing of the picture
   sits outside the button's edge. */
.site-header__menu-avatar .avatar {
    width: 100%;
    height: 100%;
    border-radius: var(--radius-small);
}

/* Auto margins beat justify-content, so these three are the whole mechanism. */
.site-header--nav-right .site-header__menu {
    margin-left: auto;
}

.site-header--nav-centre .site-header__menu {
    margin-left: auto;
    margin-right: auto;
}

.site-header--nav-left .site-header__menu {
    margin-right: auto;
}

/* Left aligned, the menu sits next to the site name rather than against it. */
.site-header--nav-left .site-header__bar {
    --site-header-bar-gap: var(--space-large);
}

/* THE CAP THAT USED TO BE HERE HAS GONE. It stopped a content sized button from
   taking the bar on a narrow window, and applied only below 64rem so that a long
   name still had room on a desktop. The button is a declared width now and is
   the same on every screen, so there is nothing left for a cap to do -- and a
   maximum that only bit on small screens would have made the width setting look
   broken on the desktop somebody sets it from. */

/* THE HEADER DOES NOT CHANGE SHAPE WITH THE WINDOW. Below a tablet the two
   halves used to stack into a pair of centred rows, on the reasoning that a
   long name and the menu were competing for a width neither had. They were,
   but the answer doubled the header's height on the screens with the least
   room, and it made the menu position setting meaningless at those widths --
   all three choices centred. The menu has a declared width now and the name
   shortens into what is left, so one row fits at any width and the setting
   means the same thing everywhere. */

/* -----------------------------------------------------------------------------
   23. The footer
   -------------------------------------------------------------------------- */

.site-footer--align-left   { text-align: left; }
.site-footer--align-centre { text-align: center; }
.site-footer--align-right  { text-align: right; }

.site-footer p {
    margin: 0;
}

.site-footer__text {
    margin-top: var(--space-extra-small);
}

.site-footer--compact {
    padding-top: var(--space-small);
    padding-bottom: var(--space-small);
    font-size: 0.8rem;
}

/* -----------------------------------------------------------------------------
   24. Site edit mode
      THE DOCK IS GONE. There was a fixed panel here holding every site setting,
      and before that two panels inside the header and the footer. Both had the
      same trouble: finding the control for a thing already on screen meant
      reading a list of every control there is, and the header panel alone
      carried seventeen of them.

      Site edit mode points at the page now -- right click a part of it and the
      shared tooltip offers what belongs to that part. What is left here is the
      one reminder that the mode is on; the tooltip's own controls are styled
      with the rest of the tooltip in section 14.
   -------------------------------------------------------------------------- */

/* A quiet reminder that the site is being edited, so nobody wanders off with
   the mode still on. */
.site-edit-mode-active .site-header {
    box-shadow: inset 0 0 0 2px var(--colour-accent);
}
/* -----------------------------------------------------------------------------
   25. The collapsed menu
      Chosen by an administrator as the hamburger menu style. For a signed in
      visitor the button is their avatar with their name beside it; for anybody
      else it is three bars, because there is no avatar to use.

      The dropdown is positioned against .site-header__menu, which wraps the
      button, so it opens directly under the button wherever the alignment
      setting has put it. Positioning it against the header instead is what
      previously pinned it to one edge no matter what the alignment said.
   -------------------------------------------------------------------------- */

.site-header__menu-button {
    display: inline-flex;
    align-items: center;
    gap: var(--space-small);

    /* THE DECLARED WIDTH, AND THE HEIGHT EVERYTHING ELSE IN THE BAR IS MEASURED
       AGAINST. max-width is the floor under it: on a window too narrow to hold
       the branding and a button of this size, the button gives way rather than
       pushing the bar wider than the screen. Everything else in the header has
       already shrunk by then -- the branding shortens its text first, because it
       is the only flex item here that may shrink at all. */
    width: var(--site-header-menu-button-width);
    max-width: 100%;

    height: var(--site-header-button-height);

    /* THE SURFACE IS WHAT PAIRS THEM, NOT THE PICTURE MEETING THE EDGE. The
       picture used to run to the button's own edge, with the inset on the name's
       side only, so that the two read as one control rather than as a square
       next to one. They read as one control now because they sit on one colour,
       which says it outright instead of implying it -- and an inset the whole
       way round is what puts that colour between them and the button's edge. */
    padding: var(--site-header-menu-button-padding);

    font-family: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--colour-header-menu-button-text);
    background-color: var(--colour-header-menu-button-background);

    /* THE HOVER WASH LIVES HERE AT NOTHING, so that it has something to fade
       from. See the hover rule below for why the wash is a background image
       rather than a background colour. Two gradients of the same shape
       interpolate; a gradient arriving from `none` does not, and the hover would
       snap on instead of fading. */
    background-image: linear-gradient(transparent, transparent);

    border: 0;
    border-radius: var(--radius-small);
    cursor: pointer;
}

/* QUIET, as section 1c has it: a wash and a border. What changed is only how the
   wash is applied.

   IT LAYERS OVER THE BUTTON'S COLOUR INSTEAD OF REPLACING IT. background-color
   was the wash before, when there was nothing under it but the header itself.
   Now that the button has a surface of its own -- and one somebody may have
   chosen -- painting the wash on as a background colour would paint that colour
   out, and take the pairing with it: the control would come apart at the moment
   it was pointed at. A background image is drawn above the background colour, so
   the wash goes over whatever the colour is and both survive.

   BRIGHTNESS WAS THE OTHER CANDIDATE AND IS THE WRONG ONE HERE. Section 1c gives
   filter: brightness() to a filled surface in a colour of its own, which this
   now is. But the default surface is a fourteen percent wash, and darkening
   something that is mostly transparent moves it about two values -- an invisible
   hover on every site that has not chosen a colour. It would also darken the
   visitor's own picture, which is not a surface and should not react.

   THE RING STILL COMES FROM --colour-header-text rather than the button's own
   text colour. It is drawn against the header, not against the button, and the
   header's text is the one colour known to contrast with what is behind it. */
.site-header__menu-button:hover,
.site-header--menu-open .site-header__menu-button {
    box-shadow: inset 0 0 0 1px var(--colour-header-text);

    background-image: linear-gradient(
        var(--colour-header-hover-surface),
        var(--colour-header-hover-surface)
    );
}

.site-header__menu-name {
    flex: 1 1 auto;
    min-width: 0;

    /* THE SIDES ONLY, AND ON PURPOSE. Above and below there is nothing to gain:
       the name is one line centred in a button whose height is settled
       elsewhere, and padding there would either do nothing or push the text off
       centre. Either side it is the gap between the name and the picture on one
       hand and the button's own edge on the other, which is a real choice.

       IT IS INSIDE THE ELLIPSIS, not outside it. The overflow rules below cut a
       long name short at the edge of this box, so the inset is space the name
       cannot be cut into rather than space it is cut over. */
    padding-inline: var(--site-header-menu-name-padding);

    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    /* CENTRED IN WHAT IS LEFT AFTER THE PICTURE. It read from the left, which is
       what a label does when its box is only as wide as its words -- but the
       button is a declared width now, so a short name like "Main" sat against
       one edge with the rest of the button empty beside it. */
    text-align: center;
}

/* The name faces the middle of the header, so it always reads inwards: to the
   left of the avatar when the menu is on the right, to the right of it when the
   menu is on the left. The markup order is name then avatar; reversing the row
   is what swaps them.

   THE PADDING NO LONGER TURNS ROUND WITH THEM. It sat on the name's side only,
   so which side that was mattered and this rule had to say so. The inset is the
   same on all four sides now, and needs no second answer. */
.site-header--nav-left .site-header__menu-button {
    flex-direction: row-reverse;
}

/* Three bars, for a visitor who is not signed in and so has no avatar. */
/* WHAT A VISITOR WITHOUT AN ACCOUNT IS OFFERED, side by side in the bar where an
   account holder has their menu. The two things they can do are simply shown:
   folding them behind a button would mean a button whose picture and name were
   both placeholders for somebody who does not exist yet.

   NOT .site-header__link. That class belongs to the rows inside the dropdown now
   and carries their block layout and padding; these are controls in a bar. */
.site-header__sign-in,
.site-header__create-account {
    display: inline-flex;
    align-items: center;
    flex: 0 0 auto;

    padding: 0.5rem 0.85rem;

    font-family: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;

    border-radius: var(--radius-small);
}

.site-header__sign-in {
    color: var(--colour-header-text);
}

.site-header__sign-in:hover,
.site-header__sign-in:focus-visible {
    background-color: var(--colour-header-hover-surface);
}

.site-header__create-account {
    color: var(--colour-button-text);
    background-color: var(--colour-accent);
}

.site-header__create-account:hover,
.site-header__create-account:focus-visible {
    background-color: var(--colour-accent-hover);
}
/* The dropdown itself: closed, it takes no room and cannot be reached. */
.site-header__navigation {
    position: absolute;

    /* CLEAR OF THE HEADER, NOT OF THE BUTTON. The button sits inside the bar's
       own inset, so a gap measured from the bottom of the button opened the
       panel above the header's edge and across its border. The bar's lower
       inset is added back, and the gap sits below that.

       IT NAMES THE BAR'S PADDING RATHER THAN REPEATING THE NUMBER. It used to
       say --container-padding-tight, which was that padding at the time; when
       the bar's inset was cut to five pixels the panel went on clearing twelve
       and hung below the header with a gap half again too big. */
    top: calc(100% + var(--site-header-bar-padding) + 0.45rem);
    right: 0;
    z-index: 900;

    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    min-width: 12rem;
    padding: var(--space-small);

    background-color: var(--colour-header-menu-background);
    border: 1px solid var(--colour-border-strong);
    border-radius: var(--radius-medium);
    box-shadow: 0 10px 30px rgba(16, 24, 40, 0.22);

    visibility: hidden;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 110ms ease-out, transform 110ms ease-out, visibility 0s linear 110ms;
}

/* Hanging off the left edge of the button when the menu is on the left, so it
   never runs off the side of the window. */
.site-header--nav-left .site-header__navigation {
    left: 0;
    right: auto;
}

/* Open: snappy. The whole thing is done inside about a fifth of a second. */
.site-header--menu-open .site-header__navigation {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition: opacity 110ms ease-out, transform 110ms ease-out, visibility 0s;
}

/* Each item fades in just behind the one above it. Six steps of 25ms is barely
   a stagger, which is the point: it should read as one movement with a little
   life in it, not as a sequence. */
.site-header__navigation > * {
    opacity: 0;
    transform: translateY(-3px);
    transition: opacity 120ms ease-out, transform 120ms ease-out;
}

.site-header--menu-open .site-header__navigation > * {
    opacity: 1;
    transform: translateY(0);
}

.site-header--menu-open .site-header__navigation > *:nth-child(1) { transition-delay: 20ms; }
.site-header--menu-open .site-header__navigation > *:nth-child(2) { transition-delay: 45ms; }
.site-header--menu-open .site-header__navigation > *:nth-child(3) { transition-delay: 70ms; }
.site-header--menu-open .site-header__navigation > *:nth-child(4) { transition-delay: 95ms; }
.site-header--menu-open .site-header__navigation > *:nth-child(5) { transition-delay: 120ms; }
.site-header--menu-open .site-header__navigation > *:nth-child(n+6) { transition-delay: 145ms; }

/* In the collapsed menu every item is a full width row. */
.site-header__link {
    display: block;
    padding: 0.5rem 0.6rem;
    white-space: normal;
}

/* Somebody who asked for less motion gets the menu without the movement. The
   rule in section 8 already shortens every transition; this makes sure the
   items are simply there rather than arriving. */

/* THE COLLAPSED MENU IS THE ONLY MENU, so the rules above are not repeated
   here for a narrow screen. There used to be a second copy of every one of
   them behind a 64rem media query, which is what the responsive style was:
   the same dropdown, turned on by the width of the window instead of by a
   setting. With one style there is nothing to turn on. */

@media (prefers-reduced-motion: reduce) {
    .site-header__navigation > * {
        opacity: 1;
        transform: none;
        transition-delay: 0s !important;
    }
}

/* -----------------------------------------------------------------------------
   26. Gutters
      Whether the header and footer line up with the page content or run to the
      edge of the screen. Both are a max-width box centred in a full width bar,
      so removing the max width is all "edge to edge" means.
   -------------------------------------------------------------------------- */

.site-footer__inner {
    max-width: var(--layout-maximum-width);
    margin: 0 auto;
}

/* Gutters off: with no maximum width to centre, the first term of the sum
   collapses and the minimum gutter is all that is left -- the same arithmetic
   .page-main--edge-to-edge relies on, so the two still agree with each other
   when both are switched off. */
.site-header--edge-to-edge .site-header__container {
    --site-header-content-maximum-width: 100%;
}

.site-footer--edge-to-edge .site-footer__inner {
    max-width: none;
}

/* -----------------------------------------------------------------------------
   27. Vertical rhythm
      One set of rules for the space between the blocks a module is built from,
      so every screen breathes the same way whatever it is made of.
   -------------------------------------------------------------------------- */

/* Blocks that follow one another are always a large step apart, whichever kind
   of block they are. Without the grid combinations here, a card following a
   grid of cards ended up flush against it. */
.page-main > .card + .card,
.page-main > .feature-grid + .card,
.page-main > .card + .feature-grid,
.page-main > .feature-grid + .feature-grid,
.page-main > .table-scroll + .card,
.page-main > .card ~ .table-scroll {
    margin-top: var(--space-large);
}

/* Nothing should leave a gap under itself at the bottom of a card. This is what
   makes a row of cards with different contents end level. */
.card > *:last-child,
.card__subtitle:last-child,
.detail-list:last-child {
    margin-bottom: 0;
}

/* The first thing in a card never needs a gap above it either. */
.card > *:first-child {
    margin-top: 0;
}

/* -----------------------------------------------------------------------------
   28. Stat cards
      A number and a line about it, as used on the admin overview. The number is
      a paragraph rather than a heading: it is a value, not a title, and making
      it a heading put "14" into the document outline.
   -------------------------------------------------------------------------- */

.feature-grid > .card {
    display: flex;
    flex-direction: column;
}

/* The action is pushed to the bottom, so the buttons on a row of cards line up
   however much text each one carries above them. */
.feature-grid > .card > .form-actions:last-child {
    margin-top: auto;
    margin-bottom: 0;
    padding-top: var(--space-medium);
}

/* The card itself needs nothing beyond what .card and .feature-grid give it;
   the class is here so the two below have something to belong to. */
/* A TILE, NOT A CARD. These are several readings of one measurement, so they
   belong inside one card rather than being four containers standing next to
   each other -- see the layout standard. There was a .stat-card-grid class here
   for the old arrangement and it never had a rule at all, so the four cards
   were simply stacking; this is the grid it should have had. */
.stat-tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--space-small);
}

.stat-tile {
    /* A tile takes its own small inset. The card around it already holds the
       inset between the words and the page. */
    padding: var(--container-padding-tight);

    background-color: var(--colour-page-background);
    border-radius: var(--radius-small);
}

.stat-tile__value {
    margin: 0 0 var(--space-extra-small);

    font-family: var(--font-family-heading);
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--colour-heading-text);

    /* A byte size runs together as one token, and a narrow tile would otherwise
       push the page sideways rather than wrap it. */
    overflow-wrap: anywhere;
}

/* A word rather than a number needs to be smaller to sit on one line. */
.stat-tile__value--word {
    font-size: 1.35rem;
    text-transform: capitalize;
}

.stat-tile__label {
    margin: 0;
    line-height: 1.45;

    /* The store's folder path is one of these, and it has no spaces to break at. */
    overflow-wrap: anywhere;
}

/* -----------------------------------------------------------------------------
   29. A header that stays on screen
      THE HEADER NOW ALWAYS STAYS ON SCREEN. Since the page stopped scrolling as
      a whole, and only .page-main scrolls, the header cannot go anywhere: it is
      a fixed row of a layout the size of the window. The "Stay on screen"
      setting therefore describes what happens either way.

      These rules are kept because they cost nothing and are correct for the
      element in any context, and because removing the setting is a decision
      about the admin screens rather than about the stylesheet.
   -------------------------------------------------------------------------- */

/* STICKY OVERRIDES THE POSITION, NOT THE STACKING. The z-index is on
   .site-header above so that a header which is not sticky stacks the same way;
   800 here used to be the only one, and it put the menu below page content. */
.site-header--sticky {
    position: sticky;
    top: 0;
}

/* The edit controls used to be inside the header, so a sticky header carrying
   an open panel would have covered the page. They are in the dock now, which is
   fixed to the bottom of the window and has nothing to do with the header, so
   there is nothing left to undo here. */

.site-footer__copyright + .site-footer__text {
    margin-top: var(--space-extra-small);
}

/* -----------------------------------------------------------------------------
   31. Dragging a cropped picture into place
      A picture set to fill is cropped on one side or the other. In site edit
      mode it can be dragged to choose which part of it shows, which is a far
      better way to pick a focal point than two sliders.

      For that to work the pictures have to be reachable by the mouse, and they
      normally are not: they sit behind an overlay that covers the whole header.
      So in edit mode the overlay stops taking pointer events and hands them
      back to the things inside it, leaving every empty part of the header as
      drag surface.
   -------------------------------------------------------------------------- */

.site-edit-mode-active .site-header__media {
    pointer-events: auto;
}

.site-edit-mode-active .site-header__overlay {
    pointer-events: none;
}

/* The links and the menu keep working, so an administrator is not locked out of
   their own navigation while arranging the header. */
.site-edit-mode-active .site-header__branding,
.site-edit-mode-active .site-header__menu {
    pointer-events: auto;
}

/* Only a picture that is actually cropped is worth dragging, so only those
   invite it. */
.site-edit-mode-active .site-header--banner-fit-fill .site-header__banner,
.site-edit-mode-active .site-header--logo-fit-fill .site-header__logo {
    cursor: grab;
}

.site-edit-mode-active .site-header__media--dragging .site-header__banner,
.site-edit-mode-active .site-header__media--dragging .site-header__logo {
    cursor: grabbing;
}

/* A picture being dragged is outlined, so it is obvious which of the two is
   moving when they overlap. */
.site-header__crop-target {
    outline: 2px solid var(--colour-accent);
    outline-offset: -2px;
}

/* -----------------------------------------------------------------------------
   30b. The front page cards
      Three destinations, one of which may not be built yet.
   -------------------------------------------------------------------------- */

/* A whole card that is a link. The colour is inherited rather than taken from
   --colour-link, because a card painted link blue throughout reads as a mistake
   rather than as something to click; the hover border is what says it is. */
.feature-card {
    display: block;
    color: inherit;
    text-decoration: none;

    transition: border-color 120ms ease-out, transform 120ms ease-out;
}

.feature-card:hover,
.feature-card:focus-visible {
    color: inherit;
    text-decoration: none;
    border-color: var(--colour-accent);
}

/* A destination that does not exist yet. Shown, so a visitor knows it is
   coming, and plainly not a link: no pointer, no hover, and faded. */
.feature-card--pending {
    opacity: 0.6;
    cursor: default;
}

.feature-card--pending:hover {
    border-color: var(--colour-border);
}

/* -----------------------------------------------------------------------------
   33. The shared modal
      One per page, empty until a module puts something in it. Opened by
      anything carrying data-modal-open; see assets/js/modal_overlay.js.
   -------------------------------------------------------------------------- */

/* THE SAME FADE AS THE COLLAPSED MENU, deliberately: 110ms ease-out, a few
   pixels of travel, and visibility switched at the end rather than at the
   start. Two overlays on one site that appear at different speeds read as two
   different pieces of software.

   visibility is what keeps the panel out of the tab order while it is hidden.
   opacity alone leaves everything inside it focusable and invisible, which is a
   particularly unpleasant thing to hand somebody using a keyboard. */
.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 1200;

    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-medium);

    visibility: hidden;
    opacity: 0;
    transition: opacity 110ms ease-out, visibility 0s linear 110ms;

    /* SOFTLY BLURRED, NOT BLACKED OUT. The page stays legible enough to keep
       your place, which is the point of not navigating away from it. */
    background-color: rgba(12, 16, 24, 0.35);
    backdrop-filter: blur(4px);
}

/* Without backdrop-filter there is no blur to soften anything, so the tint does
   the whole job and has to be heavier to separate the panel from the page. */
@supports not (backdrop-filter: blur(4px)) {
    .modal-overlay {
        background-color: rgba(12, 16, 24, 0.62);
    }
}

.modal-overlay--open {
    visibility: visible;
    opacity: 1;
    transition: opacity 110ms ease-out, visibility 0s;
}

.modal-panel {
    display: flex;
    flex-direction: column;

    width: 100%;
    max-width: 44rem;
    /* Never taller than the window, and its body scrolls rather than the page
       behind it. A modal that runs off the bottom of the screen has an action
       nobody can reach. */
    max-height: calc(100dvh - var(--space-large));

    background-color: var(--colour-card-background);
    border-radius: var(--radius-medium);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);

    /* The panel arrives a moment after the overlay and travels slightly, the
       same as the menu items do. */
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity 110ms ease-out, transform 110ms ease-out;
    transition-delay: 40ms;
}

.modal-overlay--open .modal-panel {
    opacity: 1;
    transform: translateY(0);
}

/* Stays put while the body scrolls, so the way out is always in reach. */
.modal-panel__bar {
    flex-shrink: 0;

    display: flex;
    align-items: center;
    gap: var(--space-small);

    padding: var(--space-small) var(--space-medium);
    border-bottom: 1px solid var(--colour-border);
}

.modal-panel__title {
    flex: 1 1 auto;
    min-width: 0;

    margin: 0;
    font-size: 1rem;
    font-weight: 650;
}

.modal-panel__close {
    flex-shrink: 0;

    padding: 0 0.4rem;

    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;

    color: var(--colour-text-secondary);
    background: transparent;
    border: 0;
}

.modal-panel__close:hover {
    color: var(--colour-text-primary);
}

/* THE WAY BACK, WHEN ONE MODAL WAS OPENED FROM INSIDE ANOTHER. Styled as the
   close button's twin because they sit either side of the same title and do the
   same kind of thing at different scales: one step back, or all the way out.

   It carries the `hidden` attribute until there is something underneath, which
   is what keeps a single modal looking exactly as it did before stacking
   existed -- and keeps it out of the focus trap, since a hidden button has no
   offsetParent for findFocusableElements() to find. */
.modal-panel__back {
    flex-shrink: 0;

    padding: 0 0.4rem;

    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;

    color: var(--colour-text-secondary);
    background: transparent;
    border: 0;
}

.modal-panel__back:hover {
    color: var(--colour-text-primary);
}

/* [hidden] loses to the display the bar's flex layout gives its children, so it
   has to be said again louder. Without this the arrow shows on every modal. */
.modal-panel__back[hidden] {
    display: none;
}

/* min-height: 0 for the usual reason: without it a flex item will not shrink
   below its content and the panel grows past the window instead of scrolling. */
/* THE PADDED CONTAINER UNDER THE BAR. It is to the modal what .page-main is to
   a screen: it holds the gutter between the window and the cards, and nothing
   else. The cards inside keep their own inset, which is --container-padding and
   an administrator's to set, exactly as on a screen. */
.modal-panel__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;

    padding: var(--container-padding);
}

/* -----------------------------------------------------------------------------
   THE MODAL BODY IS A CONTENT AREA, AND CARDS IN IT ARE CARDS.

   The same three levels a screen has: a padded container below the bar, holding
   cards that carry their own inset, each free to divide itself into sections.
   A module contributes cards without knowing where they will be drawn, and this
   is what makes both places look the same to it.

   TRIED THE OTHER WAY FIRST, AND IT WAS WRONG: the cards were stripped of their
   background and border so as not to draw a panel inside a panel, and separated
   by hairlines instead. It removed the very thing that tells one card from the
   next, and it made the modal the one place on the site where a card is not a
   card. The panel is the window, not the surface.
   -------------------------------------------------------------------------- */

/* A PANEL THAT WAS A SCREEN ONCE may still carry a page heading. The modal has
   its own title bar saying the same thing, so the heading keeps its words and
   gives up its panel rather than drawing a second title above the first. */
.modal-panel__body .page-title,
.modal-panel__body .page-subtitle {
    padding: 0;

    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* IT KEEPS ITS BOTTOM MARGIN, THOUGH. The panel it used to draw is gone, but
   the space under it is what separates it from the first card, and a line of
   text sitting flush on a card reads as part of the card. */
.modal-panel__body .page-title {
    margin-bottom: var(--space-small);
}

.modal-panel__body .page-subtitle {
    margin-bottom: var(--space-large);
}

/* Somebody who asked for less movement gets the fade and none of the travel. */
@media (prefers-reduced-motion: reduce) {
    .modal-overlay,
    .modal-panel {
        transition-duration: 1ms;
        transition-delay: 0s;
    }

    .modal-panel {
        transform: none;
    }
}

/* -----------------------------------------------------------------------------
   17. Styling manager mode
        The other half of styling mode: an administrator deciding which classes
        everybody else is offered. It changes no colour of its own, so all of
        its marks are outlines rather than fills.
   -------------------------------------------------------------------------- */

/* THE STANDING MARK ON EVERYTHING THAT HAS BEEN OFFERED, in both styling mode
   and the styling manager. It appears the moment either mode opens, so "what
   can I change here" is answered before it is asked, and an administrator
   deciding is looking at exactly what everybody else will see.

   GREEN, AND NOT THE ACCENT. The accent is what .style-mode-selected uses for
   the one element under the cursor, and a standing mark in the same colour
   would compete with it on every page. Green also carries the right meaning:
   this one is allowed. */
.style-mode-editable {
    outline: 2px dashed var(--colour-success);
    outline-offset: 2px;
}

/* The cursor says a right click picks rather than acts, as in styling mode. */
.style-manager-active,
.style-manager-active a,
.style-manager-active button {
    cursor: context-menu;
}

/* Everything one class covers, while it is under consideration. Seeing the real
   reach of "panels" is the whole reason for designating by right click rather
   than from a list of names. */
.style-manager-matched {
    outline: 2px dotted var(--colour-text-secondary);
    outline-offset: 1px;
}

/* AN OFFERED CLASS STAYS GREEN WHILE IT IS BEING LOOKED AT. Both marks land on
   the same elements the moment an offered class is hovered, and without this
   the grey preview wins on source order alone -- so the one thing the mark is
   for, saying that this is offered, would disappear exactly when somebody
   pointed at it to check. */
.style-manager-matched.style-mode-editable {
    outline: 2px dashed var(--colour-success);
    outline-offset: 2px;
}

/* The offered / held back line in the manager's tooltip. Green when offered,
   matching the border it is describing. */
.style-manager-state--offered {
    color: var(--colour-success);
    font-weight: 600;
}

/* A class already offered, marked in the trail, so stepping out along it also
   shows what has been decided nearby. */
.tooltip-overlay__trail-step--offered {
    border-color: var(--colour-success);
}

/* Separates the site's own colours from the offer/hold decision above them.
   Two different questions in one tooltip, and a reader should not have to work
   out which controls answer which. */
.style-manager-site-heading {
    margin-top: var(--space-small);
    padding-top: var(--space-small);
    border-top: 1px solid var(--colour-border);
    font-weight: 600;
}

.style-manager-list {
    list-style: none;
    margin: 0;
    padding: 0;

    display: flex;
    flex-direction: column;
    gap: var(--space-small);
}

.style-manager-list__row {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;

    padding: var(--space-small);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* A row already offered, so the list reads at a glance. */
.style-manager-list__row--offered {
    border-color: var(--colour-accent);
}

.style-manager-list__switch {
    display: flex;
    align-items: center;
    gap: 0.4rem;

    cursor: pointer;
    font-weight: 600;
}

.style-manager-list__description,
.style-manager-list__count {
    font-size: 0.78rem;
    color: var(--colour-text-secondary);
}

.style-manager-list__label {
    font-size: 0.85rem;
}

/* -----------------------------------------------------------------------------
   18. Style presets
        A radio group, because one look is in force at a time and the group has
        to say which. Buttons could not: pressing one told you nothing about
        where you had been.

        EACH ROW CARRIES ITS COLOURS. A name is not enough to choose by, and
        applying one to find out what it does means applying it.
   -------------------------------------------------------------------------- */

/* THE SAME SEPARATOR THE MODE TOGGLES BELOW IT USE. These are sections of one
   card, so the rule between them has to be drawn the same way each time. */
.style-presets {
    margin: 0;
    padding: var(--space-medium) 0;

    border: 0;
    border-top: 1px solid var(--colour-border);
}

/* A LEGEND IS LAID OUT INSIDE THE FIELDSET'S TOP BORDER, which cuts the rule
   into two stubs either side of the words and makes this one heading look
   unlike every other heading in the card. Floating it takes it out of that slot
   so it sits below an unbroken line. The full width keeps what follows beneath
   it rather than wrapped around it.

   The fieldset stays: it is what tells a screen reader that these radios are
   one group, and that is worth more than the simpler markup would be. */
.style-presets__legend {
    float: left;
    width: 100%;
    padding: 0;
    font-weight: 600;
}

.style-presets__list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-small);

    margin-top: var(--space-small);
}

/* The whole row is the control, so the target is the size of the thing being
   described rather than of the radio dot. */
.style-presets__choice {
    display: flex;
    align-items: center;
    gap: var(--space-small);

    padding: var(--space-small);

    cursor: pointer;
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

.style-presets__choice:hover {
    border-color: var(--colour-border-strong);
}

/* The one in force. An outline rather than a fill, so the swatches beside it
   are still read as the preset's colours and not as this row's. */
.style-presets__choice--current {
    border-color: var(--colour-accent);
    box-shadow: inset 0 0 0 1px var(--colour-accent);
}

.style-presets__choice--saving {
    opacity: 0.6;
}

.style-presets__radio {
    flex: 0 0 auto;
    margin: 0;
}

/* -----------------------------------------------------------------------------
   The swatch
      A small card in the preset's own colours, with letters in its own face and
      the two colours that decide the rest of the page beside them. It is a
      miniature of the thing being chosen rather than a legend describing it.

      EVERY COLOUR COMES FROM A CUSTOM PROPERTY set per row by
      renderStylePresetSwatchStyles(), through the nonced stylesheet. The
      Content Security Policy is style-src 'self' with a nonce, and a nonce
      covers a style element but does nothing for a style attribute -- which the
      browser refuses, leaving a grey box where the colours should be.
   -------------------------------------------------------------------------- */

.style-presets__palette {
    flex: 0 0 auto;

    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;

    width: 3.2rem;
    padding: 3px;

    background-color: var(--preset-page, var(--colour-page-background));
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* The card, and the letters on it: the two things a reader judges a look by. */
.style-presets__sample {
    display: flex;
    align-items: center;
    justify-content: center;

    height: 1.6rem;

    font-family: var(--preset-font, inherit);
    font-size: 0.95rem;
    line-height: 1;
    color: var(--preset-text, var(--colour-heading-text));

    background-color: var(--preset-card, var(--colour-card-background));
    border-radius: 2px;
}

.style-presets__chips {
    display: flex;
    gap: 2px;
    height: 0.4rem;
}

.style-presets__chip {
    flex: 1 1 0;
    border-radius: 1px;
}

.style-presets__chip--page {
    background-color: var(--preset-card, var(--colour-card-background));
}

.style-presets__chip--accent {
    background-color: var(--preset-accent, var(--colour-accent));
}

.style-presets__words {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;

    min-width: 0;
}

.style-presets__name {
    font-weight: 600;
}

.style-presets__summary {
    font-size: 0.78rem;
    color: var(--colour-text-secondary);
}

.style-presets__note {
    margin-top: var(--space-small);
    font-size: 0.82rem;
}

/* -----------------------------------------------------------------------------
   The Site images pool

   A GRID THAT SCROLLS INSIDE ITSELF rather than one that grows the card without
   limit. A pool of eighty pictures inside an expandable card would otherwise make
   opening the card a commitment to scrolling past all of them to reach the mosaic
   tool underneath.

   The tiles are tiles in the layout standard's sense: things grouped inside a
   section of a card, each with its own small inset, drawing no panel of its own
   beyond a quiet surface to sit a picture on.
   -------------------------------------------------------------------------- */

.site-images__filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-extra-small);
    margin-bottom: var(--space-small);
}

.site-images__count {
    margin-left: var(--space-extra-small);
    font-size: var(--font-size-quiet);
    font-weight: 400;
    color: var(--colour-text-secondary);
}

.site-images__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
    gap: var(--space-small);

    max-height: 26rem;
    overflow-y: auto;

    /* Bleeds out by the container inset and pads itself back, the same way
       .table-scroll does, so the scrollbar sits at the card's edge while the
       tiles stay aligned with the text above them. */
    margin: 0 calc(var(--container-padding) * -1);
    padding: var(--space-extra-small) var(--container-padding);
}

.site-image-tile {
    position: relative;

    display: flex;
    flex-direction: column;
    gap: var(--space-extra-small);

    margin: 0;
    padding: var(--container-padding-tight);

    background-color: var(--colour-page-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

.site-image-tile[hidden] {
    display: none;
}

/* CONTAIN, NOT COVER. A logo cropped to fill a square is a logo somebody cannot
   recognise, and recognising it is the whole reason the grid exists. The
   chequerboard shows through anything transparent, so a white icon on a
   transparent ground is not an empty tile. */
.site-image-tile__picture {
    width: 100%;
    height: 5.5rem;
    object-fit: contain;

    border-radius: var(--radius-small);
    background-color: var(--colour-card-background);
    background-image:
        linear-gradient(45deg, rgba(127, 127, 127, 0.14) 25%, transparent 25%),
        linear-gradient(-45deg, rgba(127, 127, 127, 0.14) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, rgba(127, 127, 127, 0.14) 75%),
        linear-gradient(-45deg, transparent 75%, rgba(127, 127, 127, 0.14) 75%);
    background-size: 12px 12px;
    background-position: 0 0, 0 6px, 6px -6px, -6px 0;
}

.site-image-tile__caption {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

.site-image-tile__name {
    font-size: 0.82rem;
    font-weight: 600;

    /* A name longer than the tile wraps rather than pushing the grid wider. */
    overflow-wrap: anywhere;
}

.site-image-tile__meta {
    font-size: 0.72rem;
    color: var(--colour-text-secondary);
}

/* Always present rather than appearing on hover: a control that only exists
   under a pointer is a control a touch screen never has. */
.site-image-tile__remove {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;

    width: 1.5rem;
    height: 1.5rem;
    padding: 0;

    font-size: 1rem;
    line-height: 1;
    color: var(--colour-text-secondary);

    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    border-radius: 50%;
    cursor: pointer;
}

.site-image-tile__remove:hover,
.site-image-tile__remove:focus-visible {
    color: var(--colour-text-inverse);
    background-color: var(--colour-error, #b3261e);
    border-color: transparent;
}

.site-images__slider {
    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.site-images__slider input[type="range"] {
    flex: 1 1 auto;
    min-width: 0;
}

.site-images__readout {
    flex: 0 0 3rem;
    text-align: right;
    font-family: var(--font-family-mono);
    font-size: 0.8rem;
    color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   The account's identity summary

   THE PICTURE BESIDE THE NAME IT BELONGS TO. The avatar used to sit in a heading
   panel above this card, next to the word "Account" rather than to anything
   about the account, while the card underneath listed the address the panel had
   just given. One of the two had to go, and the panel was the one saying less.

   IT WRAPS RATHER THAN SHRINKS. A large avatar beside a two column definition
   list leaves the list no room to be two columns, so below the breakpoint the
   picture takes its own line and the list gets the width back.
   -------------------------------------------------------------------------- */

.identity-summary {
    display: flex;
    align-items: flex-start;
    gap: var(--space-medium);
    flex-wrap: wrap;
}

.identity-summary > .avatar {
    flex: 0 0 auto;
}

.identity-summary__details {
    flex: 1 1 18rem;
    min-width: 0;
}

/* -----------------------------------------------------------------------------
   The avatar builder

   THE PREVIEW BESIDE THE CONTROLS, not above them. Every control changes the
   figure, so the figure has to stay in view while they are used; a preview at
   the top of a column of selects is scrolled away by the third one.

   It moves above the controls on a narrow window, where there is no room for
   two columns and the figure is the more important half.
   -------------------------------------------------------------------------- */

.avatar-builder {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-medium);
    align-items: start;
}

@media (min-width: 34rem) {
    .avatar-builder {
        grid-template-columns: 12rem minmax(0, 1fr);
    }

    /* The buttons and the message sit under both columns rather than under one. */
    .avatar-builder .form-actions,
    .avatar-builder .alert {
        grid-column: 1 / -1;
    }
}

/* The picture, the hint and the way back to the whole figure, stacked as one
   column of the builder's grid.

   IT STICKS, SO THE FIGURE IS NEVER OFF SCREEN. The point of the builder is
   watching the drawing change, and on a short window or a phone the controls are
   taller than the modal however densely they are packed -- so the answer cannot
   only be to fit more in. The picture holds its place at the top of whatever is
   scrolling and the controls move under it.

   THE BACKGROUND IS NOT DECORATION. Without it the hint and the button are
   transparent, and the controls scrolling underneath read straight through
   them. */
.avatar-builder__framing {
    position: sticky;
    top: 0;
    z-index: 1;

    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-extra-small);

    padding-bottom: var(--space-extra-small);
    background-color: var(--colour-card-background);
}

.avatar-builder__framing-hint {
    margin: 0;
    text-align: center;
}

/* A CROP IS DRAGGED, SO THE BROWSER MUST NOT ALSO SCROLL. Without touch-action
   a drag on a phone pans the page and the avatar at the same time, and there is
   no way to tell the two apart once the gesture has started. user-select stops
   a drag that runs off the picture from selecting the words underneath it. */
.avatar-builder__preview {
    display: flex;
    align-items: flex-start;
    justify-content: center;

    padding: var(--container-padding-tight);

    background-color: var(--colour-page-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);

    cursor: grab;
    touch-action: none;
    user-select: none;

    /* FULL WIDTH OF ITS COLUMN, WHATEVER THAT COLUMN IS. Stacked on a phone
       that is the whole modal and the drawing is big enough to judge; beside
       the controls it is the 12rem track, and the same rule serves both. */
    width: 100%;
}

.avatar-builder__preview--grabbing {
    cursor: grabbing;
}

/* A BUILT AVATAR IS AN SVG, NOT AN IMG, so the avatar sizes have to reach it as
   well. The size classes set width and height; this keeps the drawing square
   inside whichever one is applied. */
.avatar--built {
    display: block;
    flex: 0 0 auto;
}

/* BIG ENOUGH TO JUDGE, SMALL ENOUGH TO EDIT UNDER. The drawing used to fill the
   whole width of the modal on a phone, which on a 375 pixel screen is a figure
   about 330 pixels tall -- and since the picture is sticky, that is most of the
   window occupied before the first menu appears. Somebody adjusting hair had to
   scroll to the control, scroll back to see the change, and scroll down again.
   Capped, the figure and three or four rows of controls are on screen together,
   which is the whole point of watching it change. */
.avatar-builder__preview .avatar--built {
    width: 100%;
    max-width: 9rem;
    height: auto;
}

@media (min-width: 34rem) {
    .avatar-builder__preview .avatar--built {
        width: 8rem;
        height: 8rem;
    }
}

/* SMALLER TYPE THAN THE REST OF THE SITE, AND ONLY HERE. The builder is a modal
   holding more controls than any screen in the template -- nine menus, four rows
   of colour and a slider -- and at the site's own size that is a scroll on every
   window rather than only a phone. A step down in the label and the menu buys a
   couple of hundred pixels of height and reads perfectly well, because these are
   one-word labels beside a picture rather than prose. */
.avatar-builder__controls {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: var(--space-extra-small) var(--space-small);
    align-content: start;

    font-size: 0.85rem;
}

/* SCALED FROM THE SETTING, NOT INSTEAD OF IT. --font-size-input is something a
   visitor can raise for themselves in styling mode, and a flat 0.85rem here
   would have thrown that away on the one screen with the most controls on it.
   Multiplying keeps their choice and still buys the height back. */
.avatar-builder__controls .form-field__input,
.avatar-builder__controls .form-field__label {
    font-size: calc(var(--font-size-input) * 0.85);
}

/* The menus lose a little more again. They are the tallest thing repeated most
   often in here, and a one-word answer needs less room around it than a typed
   address does. */
.avatar-builder__controls .form-field__input {
    padding: 0.35rem 0.5rem;
}

.avatar-builder__field {
    margin-bottom: 0;
}

/* The part and its colour on one line: the menu takes the room, the swatch
   takes what it needs. */
.avatar-builder__field-row {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);
}

.avatar-builder__field-row .form-field__input {
    flex: 1 1 auto;
    min-width: 0;
}

/* The reading sits on the label's own line, hard against the right, so the row
   costs no more height than a menu does. */
.avatar-builder__field .form-field__label {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-extra-small);
}

.avatar-builder__measurement-value {
    font-weight: 400;
    color: var(--colour-text-secondary);
    white-space: nowrap;
}

/* A range input is the one control here the browser draws entirely itself, so
   this only sets the room it takes and leaves its parts alone. */
.avatar-builder__slider {
    width: 100%;
    min-width: 0;
    height: 2.25rem;
    margin: 0;

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

/* A row of swatches takes the whole width of the controls, because ten squares
   beside a menu is neither.

   THE TITLE SITS BESIDE THEM, NOT ABOVE. Stacked, the two rows cost about a
   hundred and thirty pixels and put the modal back to scrolling; on one line
   with the squares they cost the height of a square. */
.avatar-builder__field--full {
    grid-column: 1 / -1;

    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.avatar-builder__field--full .form-field__label {
    flex: 0 0 auto;
    margin: 0;

    /* Enough for "Background", which is the longest of them, so the four rows of
       colour line their squares up with one another instead of each starting
       wherever its own word happens to end. */
    min-width: 4.5rem;
}

.avatar-builder__swatches {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-extra-small);
}

.avatar-builder__swatch-option {
    display: inline-flex;
    margin: 0;
    cursor: pointer;
    border-radius: var(--radius-small);
}

/* THE RADIO IS STILL THERE, JUST NOT DRAWN. Hiding it with display:none would
   take it out of the tab order and off the arrow keys, which is most of what
   makes a group of radios worth using in the first place. */
.avatar-builder__swatch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.avatar-builder__chip {
    display: block;

    /* A shade smaller than before, so ten of them reach the end of a row on a
       phone rather than wrapping onto a second line each time. */
    width: 1.35rem;
    height: 1.35rem;

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* The chosen one, and the one the keyboard is on. A ring outside the chip rather
   than a change to it, so the colour underneath is never altered by being
   picked. */
.avatar-builder__swatch-input:checked + .avatar-builder__chip {
    border-color: var(--colour-accent);
    box-shadow: 0 0 0 2px var(--colour-accent);
}

.avatar-builder__swatch-input:focus-visible + .avatar-builder__chip {
    border-color: var(--colour-accent);
    box-shadow: 0 0 0 3px var(--colour-accent-soft),
                0 0 0 4px var(--colour-accent);
}

/* A swatch is as wide as it is tall. It used to be a field of its own filling a
   grid cell, which beside a menu would be a second menu-sized block of colour. */
.avatar-builder__swatch {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0.15rem;

    background-color: var(--colour-input-background);
    border: 1px solid var(--colour-input-border);
    border-radius: var(--radius-small);
}

/* The avatar and the way to change it, stacked under one another so the button
   reads as belonging to the picture above it rather than to the list beside.

   A ROW OF ITS OWN, THE WHOLE WIDTH OF THE CARD. It used to sit beside the
   detail list and wrap under it only on a narrow window, which put the largest
   thing on the screen in the corner of the card with the list crowded against
   it. Taking the full basis puts it on its own line at every width, and the
   centring this rule already had is what then centres it in the card rather
   than only within a column the width of the picture. */
/* AT THE LEFT OF THE CARD, WITH THE DETAILS BESIDE IT. It spent a while taking
   a full width row with everything in it centred, which put a hundred and
   twenty eight pixels of picture in the middle of the card and the list of
   details underneath -- and on a wide window, most of a card's width of nothing
   to the right of the face. Back to its own column at the left, which is where
   the wrapping rule further up was written for. */
.identity-summary__avatar {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-small);

    flex: 0 0 auto;
}

/* BIGGER HERE THAN ANYWHERE ELSE, AND ONLY HERE. .avatar--large is four and a
   half rems wherever it appears, which is right beside a name in a list and too
   small for the one screen whose subject is the picture itself. The size is set
   on the card rather than on the class, so the profile page and the pickers are
   left as they were. */
.identity-summary__avatar .avatar {
    width: 128px;
    height: 128px;
}

/* The initial has to grow with the circle it sits in, or a 128 pixel picture
   has a letter sized for a 72 pixel one in the middle of it. */
.identity-summary__avatar .avatar--initial {
    font-size: 3.25rem;
}


/* -----------------------------------------------------------------------------
   The avatar's face

   A MOOD IS A DRAWING; THE MOVEMENT IS HERE. Every expression is built from the
   same eye -- a white block with a dark pupil in it -- so one set of keyframes
   animates all eight of them and a new mood needs no CSS of its own.

   THIS HAS TO BE A STYLESHEET. The content security policy allows no inline
   style and no injected <style>, so a transform written onto the element would
   be dropped without a word. SMIL (<animate> inside the SVG) would have worked
   and was not used: it is a second animation system to keep alive, it ignores
   prefers-reduced-motion, and it cannot be turned off from one rule the way
   this can.
   -------------------------------------------------------------------------- */

/* Blinking is the eye flattening about its own middle, which is why the group
   needs fill-box: without it the transform is measured against the whole 64 by
   64 canvas and the eye slides off the face instead of closing. */
.avatar-eye {
    transform-box: fill-box;
    transform-origin: center;

    animation: avatarBlink 6.4s infinite;
}

/* A wink is a blink that only one eye does, held longer so it reads as
   deliberate, on a cycle that shares no factor with the blink -- otherwise the
   two would meet every few seconds and the face would look like it was simply
   closing both eyes. */
.avatar-eye--wink {
    animation: avatarWink 4.5s infinite;
}

/* The pupil sits at the left of its white at rest and slides within it. Both
   pupils carry the same animation and start together, so the eyes always agree
   with each other. */
.avatar-pupil {
    animation: avatarGlance 9.3s infinite;
}

/* THE LID IS THE OTHER HALF OF A BLINK. The eye flattening is the movement, but
   a white that has flattened to nothing leaves a gap in the face; the lid is a
   line drawn across where the eye was, switched on for exactly the frames the
   eye is shut. It sits outside the group that scales, or it would flatten with
   everything else at the moment it is needed. */
.avatar-lid {
    opacity: 0;

    animation: avatarLid 6.4s step-end infinite;
}

.avatar-lid--wink {
    animation: avatarLidWink 4.5s step-end infinite;
}

/* SLEEPY IS A BLINK INVERTED. Every other mood is open with a moment shut; this
   one is shut with a moment open, about two thirds to one, which is the whole
   of the expression. Its own cycle length shares no neat factor with the blink,
   so a room of avatars never falls into step. */
.avatar-eye--sleepy {
    animation: avatarSleepyEye 7.8s step-end infinite;
}

.avatar-lid--sleepy {
    animation: avatarSleepyLid 7.8s step-end infinite;
}

/* THE YAWN SHARES THE SLEEPY CYCLE, so the mouth opens in the same third of it
   that the eyes are shut for. On the ordinary mouth cycle it would have arrived
   at whatever moment the two lengths happened to line up on, which is most often
   the middle of a perfectly alert face. */
.avatar-mouth--yawn {
    opacity: 0;

    animation: avatarYawn 7.8s step-end infinite;
}

.avatar-mouth--yawn-rest {
    opacity: 1;

    animation: avatarYawnRest 7.8s step-end infinite;
}

@keyframes avatarYawn {
    0% {
        opacity: 0;
    }

    66% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

@keyframes avatarYawnRest {
    0% {
        opacity: 1;
    }

    66% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes avatarLid {
    0% {
        opacity: 0;
    }

    93% {
        opacity: 1;
    }

    96% {
        opacity: 0;
    }
}

@keyframes avatarLidWink {
    0% {
        opacity: 0;
    }

    73% {
        opacity: 1;
    }

    85% {
        opacity: 0;
    }
}

@keyframes avatarSleepyEye {
    0% {
        transform: scaleY(1);
    }

    66% {
        transform: scaleY(0.08);
    }

    100% {
        transform: scaleY(1);
    }
}

@keyframes avatarSleepyLid {
    0% {
        opacity: 0;
    }

    66% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

@keyframes avatarBlink {
    0%,
    93%,
    100% {
        transform: scaleY(1);
    }

    93.5%,
    96% {
        transform: scaleY(0.08);
    }
}

@keyframes avatarWink {
    0%,
    73%,
    100% {
        transform: scaleY(1);
    }

    73.5%,
    85% {
        transform: scaleY(0.08);
    }
}

/* 1.7 user units is as far as the pupil can travel and still leave a sliver of
   white on the far side. Going flush to the edge reads as a fault in the
   drawing rather than as a glance. */
@keyframes avatarGlance {
    0%,
    38% {
        transform: translateX(0);
    }

    44%,
    60% {
        transform: translateX(1.7px);
    }

    66%,
    100% {
        transform: translateX(0);
    }
}

/* A MOOD'S MOUTH IS NOT HELD FOR EVER. Both mouths are in the drawing and one
   of them is shown at a time: the expression for about two thirds of the cycle,
   then the resting mouth for the other third. A face whose eyes moved while its
   mouth never did read as a mask with blinking eyes cut into it.

   STEP-END, SO IT IS A CUT RATHER THAN A FADE. Easing between them puts both
   strokes on the face at half opacity for the length of the transition, which at
   avatar size is a smudge where the mouth should be.

   THE CYCLE SHARES NO NEAT FACTOR with the blink, the wink or the glance, so the
   four never fall into step and start looking like one animation. */
.avatar-mouth--mood {
    opacity: 1;

    animation: avatarMoodMouth 10.7s step-end infinite;
}

.avatar-mouth--neutral {
    opacity: 0;

    animation: avatarRestingMouth 10.7s step-end infinite;
}

@keyframes avatarMoodMouth {
    0% {
        opacity: 1;
    }

    66% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

@keyframes avatarRestingMouth {
    0% {
        opacity: 0;
    }

    66% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}
/* A ROOM FULL OF AVATARS MUST NOT BLINK IN UNISON, which is what they did when
   every one of them started its cycle at page load. The phase comes from the
   recipe, so it is the same every time for the same person -- a face that
   blinked differently on each reload would be a different kind of wrong. */
.avatar-phase-1 .avatar-eye,
.avatar-phase-1 .avatar-pupil,
.avatar-phase-1 .avatar-mouth,
.avatar-phase-1 .avatar-lid {
    animation-delay: -1.3s;
}

.avatar-phase-2 .avatar-eye,
.avatar-phase-2 .avatar-pupil,
.avatar-phase-2 .avatar-mouth,
.avatar-phase-2 .avatar-lid {
    animation-delay: -2.6s;
}

.avatar-phase-3 .avatar-eye,
.avatar-phase-3 .avatar-pupil,
.avatar-phase-3 .avatar-mouth,
.avatar-phase-3 .avatar-lid {
    animation-delay: -3.9s;
}

.avatar-phase-4 .avatar-eye,
.avatar-phase-4 .avatar-pupil,
.avatar-phase-4 .avatar-mouth,
.avatar-phase-4 .avatar-lid {
    animation-delay: -5.2s;
}

@media (prefers-reduced-motion: reduce) {
    /* Open eyes looking left, which is the pose the drawing was designed
       around, so stopping the movement costs the expression nothing. */
    .avatar-eye,
    .avatar-eye--wink,
    .avatar-eye--sleepy,
    .avatar-lid,
    .avatar-lid--wink,
    .avatar-lid--sleepy,
    .avatar-pupil,
    .avatar-mouth--mood,
    .avatar-mouth--neutral,
    .avatar-mouth--yawn,
    .avatar-mouth--yawn-rest {
        animation: none;
    }
}


/* -----------------------------------------------------------------------------
   Filter panels, and the category tree
   -----------------------------------------------------------------------------

   MOVED HERE FROM module_shop.css, WHICH IS WHERE THEY WERE WRITTEN. Both were
   the shop's until the trade module's admin screen grew the same two controls:
   a filter panel over a list, and a drag-and-drop tree of categories. A class
   defined in a module's stylesheet only exists on a site running that module, so
   leaving them there meant the trade screen rendering unstyled the moment
   somebody removed the shop -- and module_trade.css already carried a comment
   warning about exactly that trap.

   The same rule that put assets/js/category_tree.js in the shared folder rather
   than inside the shop: what two modules need belongs to the template. Leaving
   the script shared and its stylesheet in a module was the inconsistency.

   THE NAMES KEPT THEIR shop- PREFIX. It is historical rather than a claim about
   who owns them; renaming would have touched every filter form on the site to
   say nothing new. assets/js/page_shop.js is shared under its own shop name for
   the same reason.
   -------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   A FILTER RAIL BESIDE WHAT IT FILTERS
   ---------------------------------------------------------------------------
   MOVED HERE FROM module_shop.css WHEN THE TRADE MODULE NEEDED IT, which is
   the same journey the filter panel and the category tree below already made,
   and for the same reason: a class defined in a module's own sheet only
   exists on a site that still has that module. The trade map's market draws
   this layout now, so it belongs to the template.

   THE shop- PREFIX IS HISTORICAL AND NOT A CLAIM, exactly as the note at the
   bottom of module_shop.css says of the others. Renaming it would touch every
   filter form on the site to say nothing new. */
.shop-layout {
    display: grid;
    /* The filters take a fixed column while there is room for one, and the grid
       takes the rest. Below that width they stack, filters first, because
       choosing what to see comes before seeing it. */
    grid-template-columns: minmax(0, 14rem) minmax(0, 1fr);
    gap: var(--space-large);
    align-items: start;
}

@media (max-width: 52rem) {
    .shop-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    /* CAPPED ONCE IT IS ABOVE THE GRID RATHER THAN BESIDE IT. Stacked, the whole
       filter form stands between the visitor and the first product -- on a phone
       that was about five hundred pixels of search box and tick lists before a
       single tile appeared. Beside the grid it can be as tall as it likes, which
       is why this lives in the stacking query and not in the rule itself.

       IT SCROLLS, BECAUSE A CAP WITHOUT ONE HIDES THE FILTERS AT THE BOTTOM.
       This is the only scroller in the panel: the blocks inside it are plain
       .shop-filter fieldsets with no height of their own. (.filter-list, which
       does cap itself, belongs to the management screen and never appears in
       here -- worth saying, because a second scroller inside this one would be
       a different and much worse thing to use on a phone.) */
    .shop-filters {
        max-height: 400px;
        overflow-y: auto;

        /* Reserved whether or not it is scrolling, so the fields inside do not
           shift sideways as the panel grows past the cap. */
        scrollbar-gutter: stable;
    }
}

.shop-filters {
    display: flex;
    flex-direction: column;
    gap: var(--space-medium);

    /* THE SETTING, NOT A NUMBER THAT LOOKED LIKE IT. This is a panel with a
       background, a border and a corner, so its inset is the site's own -- it
       was --space-medium, which is the scale for gaps between things and does
       not move when somebody changes the space inside containers. */
    padding: var(--container-padding);

    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
}

/* A fieldset carries the grouping for a screen reader, and browsers give it a
   border and padding that have nothing to do with this design. */
.shop-filter {
    margin: 0;
    padding: 0;
    border: 0;

    display: flex;
    flex-direction: column;
    gap: var(--space-extra-small);
}

.shop-filter__title {
    padding: 0;

    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--colour-text-secondary);
}

.shop-filter__option {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);

    padding: 0.15rem 0.25rem;

    font-size: var(--font-size-quiet);
    border-radius: var(--radius-small);
    cursor: pointer;
}

.shop-filter__option:hover {
    background-color: var(--colour-accent-soft);
}

/* The name takes the space left between the box and the count, so the counts
   line up in a column however long the names are. */
.shop-filter__option-name {
    flex: 1 1 auto;
    min-width: 0;
}

.shop-filter__option-count {
    flex-shrink: 0;
    font-family: var(--font-family-mono);
    font-size: 0.7rem;
    color: var(--colour-text-secondary);
}

/* AN AVATAR IN FRONT OF THE NAME, where the thing being filtered on is a person
   rather than a business or a category. The shop's rail lists producers, which
   have no face; the trade market's rail lists members, who do. Same option row,
   one more part in front of the name. */
.shop-filter__option-avatar {
    flex-shrink: 0;
    display: block;
    width: 1.4rem;
    height: 1.4rem;
}

.shop-filter__option-avatar svg {
    display: block;
    width: 100%;
    height: 100%;
}

/* AND ONLY THOSE ROWS CLIP THEIR NAME. A category or a producer is named by an
   administrator and wraps onto a second line quite happily in a fourteen rem
   column. A username is chosen by whoever signed up, can be forty characters of
   one unbroken word, and wrapping does nothing for it -- so the rows that carry
   an avatar, and only those, cut it off instead. */
.shop-filter__option:has(> .shop-filter__option-avatar) .shop-filter__option-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* THE CATEGORY TREE IS SHOWN BY INDENTATION, and the rows are siblings rather
   than nested lists. Nesting a checkbox inside another checkbox's label makes
   clicking the child toggle the parent, which is maddening and hard to spot. */
/* A SCROLLING LIST RATHER THAN A LONGER CARD. A shop with forty categories
   otherwise pushes the products it is meant to be filtering off the screen.
   Roughly eight rows are showing, which is enough to see that it is a list and
   that there is more of it. */
.filter-list {
    max-height: 12.5rem;
    overflow-y: auto;

    margin: 0;
    padding: 0;
    list-style: none;

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* THE SHOP'S FILTERS STAND IN A NARROW COLUMN beside the grid, so they stack.
   These have a whole card's width, so they lie across it: the search over the
   top, the two lists side by side beneath. */
.shop-filters--admin {
    display: grid;

    /* TWO COLUMNS, NAMED RATHER THAN FITTED. auto-fit would make three at this
       width and leave the third empty, because the search box and the buttons
       span the whole row and so keep every track occupied. There are two lists
       to place, so the grid says two. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--container-padding-tight);
    align-items: start;

    margin-bottom: var(--container-padding-tight);
}

@media (max-width: 40rem) {
    .shop-filters--admin {
        grid-template-columns: minmax(0, 1fr);
    }
}

.shop-filter--wide {
    grid-column: 1 / -1;
}

.shop-filter__option--depth-2 { padding-left: 1.1rem; }
.shop-filter__option--depth-3 { padding-left: 2.2rem; }
.shop-filter__option--depth-4 { padding-left: 3.3rem; }

.category-tree {
    display: flex;
    flex-direction: column;
    gap: var(--space-extra-small);
}

/* A HEIGHT, AND ITS OWN SCROLLBAR. A shop with forty categories otherwise made
   a card taller than the screen, and the card below it unreachable without
   scrolling past the whole tree.

   ONLY THE DRAGGABLE TREE. The same class lays out the category checkboxes in
   the product editor, which sits in a modal that scrolls its own body already.

   Dragging a row to the edge scrolls this; see category_tree.js. That is not a
   nicety: the rows carry touch-action: none so a drag is a drag rather than a
   scroll, which means a touch screen has no other way to reach the rest of the
   list while holding one. */
.category-tree[data-category-tree] {
    max-height: 400px;
    overflow-y: auto;

    /* So the scrollbar sits beside the rows rather than over their edges. */
    padding-right: var(--space-extra-small);
}

.category-tree__row {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    flex-wrap: wrap;

    padding: var(--space-extra-small);

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
}

/* THE NAME IS THE BUTTON. It carries no button styling because it is a line in
   a list rather than a control to press: the underline on hover is what says it
   can be clicked, in the same way a link does.

   Written as a button and not a link because it opens something on this page
   rather than going anywhere, and because a button gives Enter and Space for
   free. That matters here: the row around it uses Space to pick a category up,
   and both of the tree's handlers ignore events coming from a button, so the
   two gestures cannot be confused for one another. */
.tree-row__open {
    flex: 1 1 10rem;
    min-width: 0;
    text-align: left;

    padding: var(--space-extra-small) 0;
    margin: 0;
    border: 0;
    background: none;

    font-family: inherit;
    font-size: inherit;
    font-weight: 600;
    color: inherit;
    cursor: pointer;
}

.tree-row__open:hover {
    text-decoration: underline;
}

.tree-row__open:focus-visible {
    outline: 3px solid var(--colour-accent);
    outline-offset: 2px;
    border-radius: var(--radius-small);
}


.category-tree__row--depth-2 { margin-left: 1.5rem; }
.category-tree__row--depth-3 { margin-left: 3rem; }
.category-tree__row--depth-4 { margin-left: 4.5rem; }
.category-tree__row--depth-5 { margin-left: 6rem; }

/* THE ROW IS THE DRAG HANDLE, so it says so: grab everywhere except on the
   controls sitting in it, which keep the cursor that matches what they do. */
.category-tree__row {
    cursor: grab;

    /* Stops a drag on a touch screen scrolling the page instead. The pointer
       handlers call preventDefault as well, but only once a drag has started,
       and by then the browser may already have decided the gesture was a
       scroll. This is the part that has to be declared in advance. */
    touch-action: none;
    user-select: none;
}

/* TEXT RATHER THAN AUTO, NOW THAT THE DOCUMENT HAS AN OPINION. user-select: auto
   resolves to whatever the parent is using, so once site_styles.css set the page
   to none this asked for none as well and the field inside a draggable row could
   not be corrected. The button beside it needs no selection at all and is left
   with the row's. */
.category-tree__row input,
.category-tree__row button {
    cursor: auto;
    touch-action: auto;
}

.category-tree__row input {
    -webkit-user-select: text;
    user-select: text;
}

.category-tree__row button {
    cursor: pointer;
}

.category-tree__grip {
    flex-shrink: 0;
    color: var(--colour-text-secondary);
    font-size: 1.1rem;
    line-height: 1;
}

/* The way out of a branch, and a drop target like any other row. */
.category-tree__top-level {
    padding: var(--space-extra-small) var(--space-small);
    margin-bottom: var(--space-extra-small);

    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--colour-text-secondary);

    border: 1px dashed var(--colour-border);
    border-radius: var(--radius-small);
}

.category-tree__count {
    flex-shrink: 0;
    font-family: var(--font-family-mono);
    font-size: 0.75rem;
}

/* BEING CARRIED. Faded rather than hidden, and it moves through the tree with
   the pointer: the row sits where it would land, so what you are looking at is
   the answer rather than a highlight you have to interpret.

   It keeps a dashed outline while it travels, which is what separates "this is
   a preview" from "this is where things are". */
.category-tree__row--carried {
    opacity: 0.55;
    cursor: grabbing;
    border-style: dashed;
    border-color: var(--colour-accent);
    background-color: var(--colour-accent-soft);
}

/* THE BRANCH BEING CARRIED IS INVISIBLE TO THE POINTER. Every row of it: the
   category itself and everything filed inside it, all of which travel together.

   Not decoration. Previewing a move puts the branch immediately after the row
   the pointer is over, so without this the next movement of the pointer landed
   on the branch, which is refused as a target, which undid the preview, which
   put the original row back under the pointer -- and the tree flickered between
   the two arrangements until the pointer moved away. See
   rememberWhereTheBranchCameFrom() in category_tree.js. */
.category-tree__row--travelling {
    pointer-events: none;
}

/* THE ROWS SLIDE TO THEIR NEW PLACES rather than appearing in them. The move is
   already done by the time this runs -- the transition only makes the jump
   legible, which on a tree of a dozen rows is the difference between seeing
   what happened and being told afterwards.

   Short on purpose. This fires on every change of target while a row is being
   dragged around, so anything slower would still be running when the next one
   started. */
.category-tree__row--sliding {
    transition: transform 160ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .category-tree__row--sliding {
        transition: none;
    }
}

/* The label under the pointer says what will happen, not just what is held, so
   it turns red when the answer is "nothing". */
.category-tree__floating--refused {
    background-color: var(--colour-error);
}

/* Where it would land. */
.category-tree__row--target,
.category-tree__top-level.category-tree__row--target {
    border-color: var(--colour-accent);
    border-style: solid;
    background-color: var(--colour-accent-soft);
}

/* SOMEWHERE IT CANNOT GO, marked as refused rather than left unmarked. An
   unmarked target and a target that is simply not under the pointer look
   identical, which leaves somebody dropping repeatedly and wondering why
   nothing happens. */
.category-tree__row--refused,
.category-tree__top-level.category-tree__row--refused {
    border-color: var(--colour-error);
    border-style: dashed;
    cursor: not-allowed;
}

/* The label that follows the pointer while dragging. Fixed, so it is positioned
   against the window the pointer coordinates are measured in, and pointer-events
   off so it never becomes the thing under the pointer. */
.category-tree__floating {
    position: fixed;
    z-index: 1000;
    pointer-events: none;

    padding: 0.2rem 0.5rem;

    font-size: var(--font-size-quiet);
    font-weight: 600;
    color: var(--colour-text-inverse);
    background-color: var(--colour-accent);
    border-radius: var(--radius-small);
    /* The template's own card shadow rather than a second recipe for one. */
    box-shadow: var(--shadow-card);
}
