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

    /* THE BACKDROP BEHIND THE LOGO AND SITE NAME, a colour and a transparency
       for a browser in light mode and another pair for dark mode -- see
       .site-header__branding-backdrop. The header's own colour by default, so on
       a plain header it cannot be seen, and over a banner it is the header
       showing through behind the name. Styling mode's Site name and byline
       target sets all four. */
    --colour-site-branding-backdrop:            var(--colour-header-background);
    --colour-site-branding-backdrop-dark:       var(--colour-header-background);
    --site-branding-backdrop-transparency:      25;
    --site-branding-backdrop-transparency-dark: 25;
    --colour-input-background:     #ffffff;
    --colour-input-text:           #1c2230;
    /* THE ONLY EDGE A FIELD HAS. --colour-input-background is #ffffff and so is
       --colour-card-background, so this line is the whole of what says "type
       here" -- and at #b9c0cb it was 1.83:1 against the card, under the 3:1
       WCAG asks of a control's own boundary. Every preset that leaves the input
       the same colour as the card inherits that, which was all but two of them. */
    --colour-input-border:         #828fa3;
    --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, A SET FOR EACH KIND OF SURFACE, and the tokens the rules
       use pointing at one of them. The dark mode block below points them at the
       other. So does the server, for a look whose card colour settles the page
       as light or dark whatever the device asks for -- Midnight chosen in light
       mode is a dark page, and kept the light set's #17663f green on a #181b23
       card, which is barely there. A preset carries no status colours of its
       own, so the server cannot take them from it; it can only say which of
       these two the page is. See buildStyleOverrideDeclarations().

       THE VALUES LIVE HERE ONCE. The server names a set, never a colour, so a
       change to a hex below reaches every page that uses it. */
    --colour-success-on-light:      #17663f;
    --colour-success-soft-on-light: #e2f4ea;
    --colour-error-on-light:        #a3232b;
    --colour-error-soft-on-light:   #fbe7e8;
    --colour-warning-on-light:      #8a5a00;
    --colour-info-on-light:         #1f5673;
    --colour-info-soft-on-light:    #e5f1f7;

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

    --colour-success:              var(--colour-success-on-light);
    --colour-success-soft:         var(--colour-success-soft-on-light);
    --colour-error:                var(--colour-error-on-light);
    --colour-error-soft:           var(--colour-error-soft-on-light);

    /* THE VALUE .badge--warning ALREADY USED, given a name. Warning was the one
       state of the four without a token, so the amber lived hard-coded in the
       one rule that wanted it -- and the second thing to want it would have
       copied the hex rather than found it. */
    --colour-warning:              var(--colour-warning-on-light);
    --colour-info:                 var(--colour-info-on-light);
    --colour-info-soft:            var(--colour-info-soft-on-light);

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

    /* ONE SIZE, AND THE REST ARE MULTIPLES OF IT.
       --font-size-base is the midpoint of the page: body text is set at it, and
       everything else is a ratio away -- a heading is nearly twice it, help text
       a little under it, the byline under that. Moving this one value therefore
       rescales the whole page in proportion, which is what the Text size control
       on the account screen moves and the only thing it has to move.

       IT REPLACED A PRESET. There was a Large print preset whose entire content
       was six sizes bumped together, which meant large print was a look you chose
       instead of your own rather than a size you set alongside it -- and any
       preset added afterwards had to remember to ship a large variant or not
       offer one. The ratios below do that arithmetic for every preset at once,
       including ones that do not exist yet.

       EACH IS STILL ITS OWN VARIABLE, so a preset or a person can pin one
       outright and it will stay pinned: an explicit --font-size-heading is
       emitted at :root:root and beats this. The ratio is the default, not a
       rule. */
    --font-size-base:    16px;

    /* AND THE RATIOS THEMSELVES ARE VARIABLES, which is what lets somebody change
       one without leaving the system. Styling mode used to offer these as pixel
       sliders: setting a heading to 30px wrote 30px, an absolute value beat the
       ratio, and that heading then sat still for ever afterwards while the rest
       of the page moved with the text size. The control now moves the ratio, so
       a chosen heading is "nearly twice the body" rather than "thirty pixels"
       and stays that way at every text size.

       A BARE NUMBER, NOT A LENGTH, and that is what makes it safe to store: it is
       checked against /^\d{1,2}(\.\d{1,3})?$/ and multiplied here, so nothing a
       person chooses can reach the stylesheet as anything but a number. See
       STYLE_VALUE_TYPE_SCALE in includes/style_service.php. */
    /* HOW HEAVY THE TEXT IS, on the same principle as the sizes above: one
       setting, and everything emphatic is an offset from it. Body text was the
       browser default and every heading, label and button carried a hard 600 or
       700, so making the page lighter or heavier meant editing eighty
       declarations and there was no way to offer it as a choice at all.

       OFFSETS RATHER THAN RATIOS, unlike the sizes, because weight is not a
       linear scale -- doubling 400 gives 800, which is not "twice as bold", and
       most faces only have a handful of real weights to land on anyway. Adding a
       constant keeps the distance between body and heading the same at every
       setting, which is the thing that has to stay recognisable.

       THE RANGE IS BOUNDED BY THE ARITHMETIC. --font-weight-base is offered from
       300 to 600, so bold tops out at 900 and never leaves the 1-900 a weight is
       allowed to be. A font-weight outside that is an invalid declaration, which
       browsers drop entirely -- so the text would not go heavier, it would go
       back to normal. */
    --font-weight-base:     400;
    --font-weight-semibold: calc(var(--font-weight-base) + 200);
    --font-weight-bold:     calc(var(--font-weight-base) + 300);

    --font-scale-button:      1;
    --font-scale-input:       1;
    --font-scale-heading:     1.875;
    --font-scale-quiet:       0.875;
    --font-scale-site-name:   1.125;
    --font-scale-site-byline: 0.875;

    --font-size-button:  calc(var(--font-size-base) * var(--font-scale-button));
    --font-family-input: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
    --font-size-input:   calc(var(--font-size-base) * var(--font-scale-input));
    --font-size-heading: calc(var(--font-size-base) * var(--font-scale-heading));

    /* Subtitles, help text and anything marked secondary. Most of the prose on
       the site is this rather than the base size. */
    --font-size-quiet:   calc(var(--font-size-base) * var(--font-scale-quiet));

    /* The wordmark's own font and sizes. The stack is written out to match the
       'System default' entry in getBuiltInFontStacks() 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:    calc(var(--font-size-base) * var(--font-scale-site-name));
    --font-size-site-byline:  calc(var(--font-size-base) * var(--font-scale-site-byline));

    --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 banner's height matches what
       includes/site_settings_service.php works out from its default
       percentage, which is 30% of the 512 pixel maximum. The logo is a share
       of the header's button height, the whole of it until somebody chooses
       less, and a shape -- width over height -- that is square until a picture
       says otherwise. See findSiteLogoAspectRatio(). */
    --site-logo-image:   none;
    --site-banner-image: none;
    --site-logo-scale:   1;
    --site-logo-aspect-ratio: 1;
    --site-banner-height: 154px;
    --site-banner-width: 100%;

    /* Which part of a cropped banner stays in view. Dragging the picture in
       site edit mode is what moves it. */
    --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:     #616b7e;

        --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:          var(--colour-success-on-dark);
        --colour-success-soft:     var(--colour-success-soft-on-dark);
        --colour-error:            var(--colour-error-on-dark);
        --colour-error-soft:       var(--colour-error-soft-on-dark);
        --colour-warning:          var(--colour-warning-on-dark);
        --colour-info:             var(--colour-info-on-dark);
        --colour-info-soft:        var(--colour-info-soft-on-dark);

        --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 {
        /* THE HOVER MOVES TOWARDS THE TEXT ON THE CARD, NOT TOWARDS BLACK.
           This is the same fix section 1b-ii made for the header, for the same
           reason, one surface further in -- and it is worth saying why the
           obvious version does not work.

           Darkening the accent assumes the surface under it is light. That is
           true of the template's own card and false the moment a preset says
           otherwise, and the mode makes it worse rather than better: a preset is
           emitted as :root:root, so it beats the dark mode block, and Old Way in
           dark mode kept its white card while this rule LIGHTENED the accent for
           a dark surface that was not there. #047857 became a pale green on
           #ffffff at 2.61:1. Midnight is the same mistake mirrored -- a dark card
           in light mode, hovering darker still, at 3.17:1.

           --colour-card-text is the one colour guaranteed to contrast with the
           card, because it is what the card already prints on it. Mixing towards
           it darkens on a light card and lightens on a dark one, works out
           correctly for a preset that sets neither, and needs no dark mode
           counterpart -- which is why the block that used to be here is gone. */
        --colour-accent-hover: color-mix(in srgb, var(--colour-accent) 82%, var(--colour-card-text));

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

    @media (prefers-color-scheme: dark) {
        :root {
            /* Only the wash is deepened here. A translucent wash takes its
               lightness from whatever is under it, so this is a matter of how
               much tint reads well on a dark card, not a correction. */
            --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
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   hidden MEANS HIDDEN, WHICH IT OTHERWISE DOES NOT

   THE BROWSER'S OWN RULE IS display: none AT THE LOWEST POSSIBLE WEIGHT, so any
   class that sets a display beats it. .button is display: inline-block, so a
   button hidden with element.hidden = true stayed on the screen -- which is how a
   form that saves itself ended up still showing "Save the details".

   IT HAS BEEN PATCHED TWICE BEFORE, ONE ELEMENT AT A TIME. See
   .modal-panel__back[hidden] and .site-image-tile[hidden] further down, both of
   which found this the hard way and fixed it where they stood. This is the same
   fix said once, for everything.

   !important IS RIGHT HERE AND ALMOST NOWHERE ELSE. "Not displayed" is not a
   preference to be weighed against a layout; an element that is hidden is not
   participating, and there is no rule that should be allowed to disagree.
   -------------------------------------------------------------------------- */
[hidden] {
    display: none !important;
}

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

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

    /* SAY WHICH SCHEMES THIS PAGE IS PAINTED IN, because a page that does not
       say is a page every browser guesses about -- and they guess differently.
       The parts of a form the stylesheet cannot reach take their colours from
       this and nothing else: the list a <select> drops down, the autofill
       wash, a date picker, the scrollbars. Left undeclared, a browser in dark
       mode draws those dark and sets them against a card the site is painting
       white, which is how a light preset ends up with a black dropdown hanging
       off a white field.

       It also settles the more aggressive case. A browser doing its own forced
       dark pass -- Chrome's auto dark theme on Android is the one people meet --
       rewrites author colours on pages that never declared a scheme, and leaves
       alone the ones that did. Declaring it is the opt out.

       BOTH VALUES HERE, ONE VALUE WHEN A PRESET PINS THE SURFACES. This is the
       template's own palette, which does follow the system, so both are true. A
       preset that fixes its card colour is light or dark in BOTH system modes,
       and renderStyleOverrideBlock() then emits a single value to say which --
       see buildStyleOverrideDeclarations(). */
    color-scheme: light dark;

    /* THE ROOT SIZE IS THE TEXT SIZE, which is what makes one control move the
       whole page. Most of the type in this stylesheet is set in rem -- 163
       declarations against 31 using the size tokens -- and rem answers to the
       root font size, not to --font-size-base. So moving the size setting used
       to enlarge the handful of things wired to the tokens and leave everything
       else exactly where it was; putting it back down then looked like it had
       not worked, because the parts that never grew had nothing to shrink.

       IT IS 16px BY DEFAULT, so this changes nothing until somebody moves it,
       and every rem length -- spacing and the content column included -- then
       scales with the text. That is deliberate: a page whose words grow while
       its gaps and its measure stay put is a page that runs out of room. Media
       queries are unaffected, because a breakpoint in rem resolves against the
       browser's own default rather than this. */
    font-size: var(--font-size-base);

    /* 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.

   THE LIST BELOW IS THE TEMPLATE'S OWN, AND IT IS NOT THE ONLY WAY IN. A module
   with a good reason relaxes the rule for its own markup by marking it
   .is-selectable -- see the note on that class further down -- rather than
   adding a selector here. The template does not want a list of every module's
   class names in it, and a module should not have to edit a template file to
   let somebody copy a sentence.

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

/* AND ANYTHING THAT ASKS FOR IT.

   THE RULE ABOVE IS ABOUT THE BLUE SMEAR ON A SITE OF CLICKABLE ROWS, and it is
   right nearly everywhere. It is wrong wherever the words are the point rather
   than the furniture: something somebody wrote to be read and quoted, an address
   to copy, a reference number to paste somewhere else. Those places say so with
   this class instead of the template keeping a list of them.

   text, NOT auto, AND THE DIFFERENCE HAS BITTEN THIS FILE BEFORE. user-select:
   auto resolves to whatever the parent is using, so against a document set to
   none it asks for none as well and changes nothing -- see the note on
   .category-tree__row input, which is the same mistake found the hard way.

   IT REACHES THE CHILDREN, because the property inherits: marking a post body
   covers the paragraphs, quotes and lists inside it without naming any of them.

   IT IS STILL A COMFORT, NOT A PERMISSION. Nothing here decides who may read
   anything; it decides whether a drag paints the words blue. */
.is-selectable {
    -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);
    font-weight: var(--font-weight-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.

   THE WEIGHT IS STATED HERE RATHER THAN LEFT TO THE BROWSER. A heading with no
   author weight takes the user agent's `bold`, which is a fixed 700 and answers
   to nothing -- so when the text weight control was added, body text and labels
   moved and every heading on the site stayed exactly where it was. At the
   default this computes to the same 700 the browser was giving, so nothing looks
   different until somebody moves the setting. */
h1, h2, h3, h4,
.page-title,
.card__title {
    color: var(--colour-heading-text);
    font-family: var(--font-family-heading);
    font-weight: var(--font-weight-bold);
}

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: var(--font-weight-semibold);
    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;

    /* HOW TALL THE COMPACT HEADER IS, in either header: half the picture, the
       button's inset, the menu's and the bar's, each twice -- the same sum the
       compact header's own layout comes to. Named for a live banner drawn to the
       compact header's scale, whose full header banner is a number of these; see
       .site-header__stage--sized-by-scene. It used to be restated there without
       the menu's inset, which is nothing until somebody sets it. */
    --site-header-compact-height: calc(
        var(--site-header-avatar-size) / 2
        + var(--site-header-menu-button-padding) * 2
        + var(--site-header-menu-padding) * 2
        + var(--site-header-bar-padding) * 2
    );
}

/* 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: var(--font-weight-semibold);
    color: var(--colour-header-text);
    letter-spacing: -0.01em;
}

/* THE HEADER IS NOT THE PAGE, AND ITS LINKS MUST NOT TAKE THE PAGE'S HOVER
   COLOUR. The generic a:hover hands out --colour-link-hover, which every preset
   picks to sit on the page background -- and the branding is a link sitting on
   the header background instead. Old Way sets a dark green link hover and a
   dark green header, so hovering the site name took it from 7.28:1 to 1.04:1:
   the same colour as what is behind it, which is not dim, it is gone.

   IT IS THE ELEMENT, NOT THE WORDS, THAT WAS EXPOSED. .site-header__name
   carries --colour-site-name of its own, so today the visible name survives and
   only the link box changes underneath it -- which is why this reads as safe
   until a site has a byline link, drops the name span, or puts anything else in
   the bar. The colour the header already uses for its own text is the one that
   is known to work against it. */
.site-header__branding:hover,
.site-header__branding:focus-visible {
    color: var(--colour-header-text);
}

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

/* A CARD TITLE LEANS ON ITS SUBTITLE FOR THE GAP UNDER IT, which is fine until
   there is no subtitle -- then the first field sits directly beneath the
   heading with a quarter of a line between them. The same shape as
   .page-title:not(:has(+ .page-subtitle)) further down, and for the same
   reason: the two are one panel and either half may be missing.

   AFTER THE RULE IT OVERRIDES. It wins on specificity either way, but this file
   is read by people as well as by browsers and an override above its base reads
   as a mistake. */
.card__title:not(:has(+ .card__subtitle)) {
    margin-bottom: var(--space-medium);
}

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

/* A SCREEN'S HEADING IS FOR SCREEN READERS, NOT A PANEL ON THE PAGE. The panel
   said what the menu button and the first card were already saying -- "Deals"
   over "Your deals", "Checkout" over "Your cart" -- and on a phone it spent the
   first screenful doing it. The heading stays in the page, so the screen still
   opens with one h1 for anybody listening, and the subtitle under it with it.

   ONLY THE ONE AT THE TOP OF THE CONTENT AREA. A heading inside a heading row is
   part of what the screen shows -- a thread's title beside its actions, a
   person's name beside their picture -- and is left alone. A screen whose
   heading IS its content says so with .page-title--shown: a hike's name, with
   how hard it is under it, or a page whose subtitle is the only thing it says. */
.page-main > h1.page-title:not(.page-title--shown),
.page-main > h1.page-title:not(.page-title--shown) + .page-subtitle {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
    box-shadow: none;
}

/* 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. */
/* .card__note IS .text-secondary WEARING A NAME. The look is identical and
   deliberately so -- what differs is that one says what a thing IS and the other
   says what colour it is, and only the first can be safely made editable. See
   getEditableCopyClassNames(). */
.text-secondary,
.card__note,
.card__subtitle,
.form-field__help,
.stat-tile__label {
    font-size: var(--font-size-quiet);
    color: var(--colour-text-secondary);
}

/* -----------------------------------------------------------------------------
   EACH PIECE OF EDITABLE COPY, SET ON ITS OWN

   THE STYLE TARGETS ABOVE ARE ONLY HALF OF IT. matchSelectors decide which target
   a right click finds; these rules are what makes the target's variables do
   anything. A target whose variables nothing reads is an entry in the styling
   manager that quietly changes nothing, which is worse than not offering it.

   EVERY ONE FALLS BACK TO WHAT IT INHERITED, so a class nobody has styled looks
   exactly as it did -- the quiet size and secondary colour it has always had. The
   variable is empty until somebody sets it.

   THEY COME AFTER THE SHARED RULE ABOVE and repeat its two properties, because
   the fallback has to be the shared value rather than the browser's.
   -------------------------------------------------------------------------- */
.card__subtitle {
    color: var(--colour-card-subtitle, var(--colour-text-secondary));
    font-family: var(--font-family-card-subtitle, inherit);
    font-size: calc(var(--font-size-base) * var(--font-scale-card-subtitle, var(--font-scale-quiet)));
    font-weight: var(--font-weight-card-subtitle, inherit);
}

.card__note {
    color: var(--colour-card-note, var(--colour-text-secondary));
    font-family: var(--font-family-card-note, inherit);
    font-size: calc(var(--font-size-base) * var(--font-scale-card-note, var(--font-scale-quiet)));
    font-weight: var(--font-weight-card-note, inherit);
}

.form-field__help {
    color: var(--colour-form-field-help, var(--colour-text-secondary));
    font-family: var(--font-family-form-field-help, inherit);
    font-size: calc(var(--font-size-base) * var(--font-scale-form-field-help, var(--font-scale-quiet)));
    font-weight: var(--font-weight-form-field-help, inherit);
}

/* THE MARKER CARRIES NO LOOK OF ITS OWN, so every one of these inherits until it
   is set. A <th> and a <button> wearing it should still look like a table header
   and a button -- the point of the class is reach, not appearance. */
.site-copy-text {
    color: var(--colour-site-copy-text, inherit);
    font-family: var(--font-family-site-copy-text, inherit);
    font-weight: var(--font-weight-site-copy-text, inherit);

    /* SCALED AGAINST WHAT IT INHERITED, NOT AGAINST THE BASE. This one class is
       worn by a table header, a field label and the writing on a button, and
       those are three different sizes on purpose. A rem calc would flatten them
       all to one the moment the class was added; 1em times an unset scale is
       exactly the size the element already had. */
    font-size: calc(1em * var(--font-scale-site-copy-text, 1));
}

/* QUIET COPY STAYS QUIET UNTIL IT IS STYLED. The marker's fallbacks are
   "inherit", which is right for a header or a button, whose own look comes from
   a rule that outweighs one class -- and wrong for a note, a help line, a
   subtitle or secondary text, whose look is one class declared above this one.
   At equal weight the later rule won, so every one of those wearing the marker,
   about ninety across the site, was drawn at the page's full size and in its main
   text colour. Here each falls back to its own look, and the site copy target
   still wins once somebody sets it. */
.card__note.site-copy-text {
    color: var(--colour-site-copy-text, var(--colour-card-note, var(--colour-text-secondary)));
    font-family: var(--font-family-site-copy-text, var(--font-family-card-note, inherit));
    font-weight: var(--font-weight-site-copy-text, var(--font-weight-card-note, inherit));
    font-size: calc(var(--font-size-base) * var(--font-scale-card-note, var(--font-scale-quiet))
                    * var(--font-scale-site-copy-text, 1));
}

.card__subtitle.site-copy-text {
    color: var(--colour-site-copy-text, var(--colour-card-subtitle, var(--colour-text-secondary)));
    font-family: var(--font-family-site-copy-text, var(--font-family-card-subtitle, inherit));
    font-weight: var(--font-weight-site-copy-text, var(--font-weight-card-subtitle, inherit));
    font-size: calc(var(--font-size-base) * var(--font-scale-card-subtitle, var(--font-scale-quiet))
                    * var(--font-scale-site-copy-text, 1));
}

.form-field__help.site-copy-text {
    color: var(--colour-site-copy-text, var(--colour-form-field-help, var(--colour-text-secondary)));
    font-family: var(--font-family-site-copy-text, var(--font-family-form-field-help, inherit));
    font-weight: var(--font-weight-site-copy-text, var(--font-weight-form-field-help, inherit));
    font-size: calc(var(--font-size-base) * var(--font-scale-form-field-help, var(--font-scale-quiet))
                    * var(--font-scale-site-copy-text, 1));
}

.text-secondary.site-copy-text,
.stat-tile__label.site-copy-text {
    color: var(--colour-site-copy-text, var(--colour-text-secondary));
    font-size: calc(var(--font-size-quiet) * var(--font-scale-site-copy-text, 1));
}

.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: var(--font-weight-semibold);
    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: var(--font-weight-semibold);
    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: var(--font-weight-semibold);
    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.

       AND IT COMES DOWN FROM THE TOP. The bottom of a phone is where the typing
       happens -- the map's chat bar, the scene's Say box, a pinned Save, the
       keyboard itself -- and a notice there sat over the very thing that had
       just been used: ticking a have on the map put "Saved." across the chat
       bar. The top is the header, which nothing is being done to. */
    .saved-toast {
        left: var(--container-padding);
        right: var(--container-padding);
        top: var(--container-padding);
        bottom: auto;
        text-align: center;
        transform: translateY(-6px);
    }

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

    .saved-toast--leaving {
        transform: translateY(-6px);
    }
}

@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. */
/* ---------------------------------------------------------------------------
 * THE SIGN-IN AND SIGN-UP FORMS, WHICH ARE TIGHTER THAN A FORM ON A SCREEN
 * ------------------------------------------------------------------------ */

/* A COLUMN OF SEVEN THINGS, MOST OF WHICH RESERVE A LINE FOR A MESSAGE THAT IS
   USUALLY NOT THERE.
   
   Measured before touching it: an empty notice cost about forty pixels -- the
   eighteen it reserves plus twenty-two of margin on either side of it -- and
   there are four of them on the sign-up form. That is a hundred and sixty
   pixels of nothing in a form somebody is trying to get to the bottom of.
   
   THE RESERVATION ITSELF STAYS. min-height on the notice is what stops the
   fields below jumping down when a message finally arrives, which is worth far
   more than the space it costs; what goes is the margin stacked around it.
   
   HERE RATHER THAN GLOBALLY, because the rhythm of a form laid out on a full
   screen is not the rhythm of one in a narrow card that has to be read in a
   single glance. The account and admin screens keep theirs. */
.page-authentication .form-field {
    margin-bottom: var(--space-small);
}

/* A NOTICE STILL NEEDS A GAP UNDER IT, JUST A SMALLER ONE. Taken to zero this
   read fine while every notice was empty and collided the moment one filled:
   the red line sat directly against the next field's label with nothing between
   them. Measured with a message in place, not with the form at rest. */
.page-authentication .form-field__notice {
    margin-bottom: var(--space-extra-small);
}

.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: var(--font-weight-bold);
    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: var(--font-weight-semibold);
    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: var(--font-weight-semibold);
    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: var(--font-weight-semibold);
    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: var(--colour-warning); }
.badge--error   { color: var(--colour-error); }
.badge--accent  { color: var(--colour-accent); }

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

/* A ROW WHOSE PRESS IS ON ITS WAY: the admin Modules screen's rows open a module
   by loading the screen again -- see page_admin_modules.js -- and a row that looks
   unpressed for that moment gets pressed twice. */
.data-table tbody tr[aria-busy="true"] {
    cursor: progress;
    opacity: 0.6;
}

.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);
    text-transform: uppercase;
    letter-spacing: 0.06em;

    /* THE VARIABLES ARE READ HERE RATHER THAN BESIDE THE OTHER COPY TARGETS,
       because this rule comes later in the file and would otherwise win and
       undo them. Unset, every one of these is what it always was. */
    font-size: calc(var(--font-size-base) * var(--font-scale-card-section-title, 0.8));
    font-family: var(--font-family-card-section-title, inherit);
    font-weight: var(--font-weight-card-section-title, inherit);
    color: var(--colour-card-section-title, 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: var(--font-weight-bold);
    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: var(--font-weight-semibold);
}

.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-input-text);
    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 MACHINE STRING PUT ON SCREEN TO BE READ -- a URL, a file path. It is one long
   unbreakable word, so at 375px it pushes its container sideways rather than
   wrapping, and only .table-scroll is allowed to scroll horizontally. Found by
   measuring: the release address overflowed .page-main by 43px on a phone, and
   the list of edited files scrolled inside itself with the path cut off. */
.wraps-anywhere,
.wraps-anywhere code {
    overflow-wrap: anywhere;
}

/* THE ONE ROW IN AN UPDATE PLAN THAT LOSES SOMETHING. Every other line of that
   table is a file being brought up to date, which is what was asked for; this is
   a file somebody edited here being written over, and it has to be findable in a
   list of two hundred that otherwise all look alike. */
.plan-row--overwritten {
    color: var(--colour-danger, #b3261e);
    font-weight: var(--font-weight-semibold);
}

/* 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: var(--font-weight-semibold);
}

/* 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: var(--font-weight-bold);
    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 SLIDER SWITCH

   role="switch" ON A BUTTON, NOT A CHECKBOX. A checkbox says "tick this and then
   submit something"; a switch says "this is on now", which is what these are --
   pressing one writes a line of configuration immediately. Screen readers
   announce it as a switch and read aria-checked, and it takes Enter and Space
   from the keyboard for free because it is a button.

   THE LABEL IS ITS OWN ELEMENT so script can rewrite the word without touching
   the track and the thumb. Setting textContent on the button would delete them,
   which is exactly what the first version did.

   IT IS A BUTTON IN A DATA TABLE, so it carries no button chrome of its own --
   the track is the whole control and a border round the pair would read as a
   second boundary inside a cell that already has one.
   -------------------------------------------------------------------------- */
.switch {
    display: inline-flex;
    align-items: center;
    gap: var(--space-extra-small);

    margin: 0;
    padding: 0;

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

    font: inherit;
    color: inherit;
    text-align: left;
}

.switch[disabled] {
    cursor: not-allowed;
    opacity: 0.55;
}

.switch__track {
    position: relative;
    flex: 0 0 auto;

    /* IN em SO IT FOLLOWS THE TYPE. Somebody enlarging the site's text in styling
       mode gets a larger switch rather than a shrinking one beside bigger words. */
    width: 2.2em;
    height: 1.2em;

    background-color: var(--colour-border);
    border-radius: 999px;

    transition: background-color 120ms ease;
}

.switch__thumb {
    position: absolute;
    top: 0.15em;
    left: 0.15em;

    width: 0.9em;
    height: 0.9em;

    background-color: var(--colour-card-background);
    border-radius: 50%;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);

    transition: transform 120ms ease;
}

.switch[aria-checked="true"] .switch__track {
    background-color: var(--colour-success);
}

.switch[aria-checked="true"] .switch__thumb {
    transform: translateX(1em);
}

/* SOME ON, SOME OFF: the thumb halfway and the track half coloured.
   For the switch that stands for a whole column, where the rows disagree. A
   switch has no third state to announce, so it announces off and the words
   beside it say "Some published"; this is the same fact drawn, so the eye does
   not have to read the words to see the column is mixed. */
.switch.is-mixed .switch__track {
    background: linear-gradient(to right, var(--colour-success) 50%, var(--colour-border) 50%);
}

.switch.is-mixed .switch__thumb {
    transform: translateX(0.5em);
}

/* A PART THAT COULD NOT BE CHECKED OR UPDATED, said in the row it belongs to.
   The card's alert says how many; this says which and why, beside the switch
   that put it in the run. */
.sync-result--problem {
    color: var(--colour-danger, #a3341f);
}

/* THE COLUMN'S SWITCH, UNDER THE COLUMN'S NAME. On its own line so the heading
   still reads as a heading, and at ordinary weight because it is a control and
   not part of the title. */
.published-versions__all {
    display: flex;
    margin-top: var(--space-extra-small);
    font-weight: var(--font-weight-base);
}

/* THE FOCUS RING GOES ROUND THE TRACK, not the whole control. The label is text
   beside it and a ring round both reads as a box that has appeared. */
.switch:focus-visible {
    outline: none;
}

.switch:focus-visible .switch__track {
    outline: 2px solid var(--colour-accent);
    outline-offset: 2px;
}

.switch__label {
    font-size: var(--font-size-quiet);
}

/* A switch that is off says so quietly; on, it is the ordinary text colour. */
.switch[aria-checked="false"] .switch__label {
    color: var(--colour-text-secondary);
}

@media (prefers-reduced-motion: reduce) {
    .switch__track,
    .switch__thumb {
        transition: none;
    }
}

/* 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) {
    /* ONE ROW, ABOUT THIRTY PIXELS. The banner is a label, a sentence and a way
       out, and on a phone the sentence is what makes it three rows and a third
       of the screen -- measured at 130px on a 375 wide screen, against 52 on a
       wide one. Nothing here is worth that much of a phone.

       THE SENTENCE IS HIDDEN, NOT DELETED. It explains what the mode does, which
       is worth keeping for anybody listening to the page even when there is no
       room to draw it -- so it is clipped the way every other visually hidden
       label in this sheet is, rather than display: none, which would take it out
       of the accessibility tree along with the layout.

       AND THE WAY OUT SHRINKS WITH IT. The button was 35px on its own, so
       capping the bar without touching the button would only have clipped it. */
    .mode-banner {
        flex-wrap: nowrap;
        gap: var(--space-small);

        padding: 0.15rem var(--space-small);
        font-size: 0.7rem;
    }

    .mode-banner__message {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
        border: 0;
    }

    .mode-banner .button {
        padding: 0.1rem 0.4rem;
        font-size: 0.7rem;
        line-height: 1.3;
    }
}

/* -----------------------------------------------------------------------------
   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: var(--font-weight-bold);
    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: var(--font-weight-bold);
    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: var(--font-weight-semibold);
    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: var(--font-weight-semibold);
}

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

/* 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: var(--font-weight-bold);
    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);
}

/* PICTURES ON THEIR OWN NEED LESS ROOM. The width above is for the
   administrator's list, where a name and a Remove button sit under each one. A
   member choosing a picture gets the picture alone, and at that width a phone
   showed two a row with a picture's width of nothing between them. */
.avatar-picker:has(> .avatar-picker__option--choosable) {
    grid-template-columns: repeat(auto-fill, minmax(4rem, 1fr));
}

.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: var(--font-weight-semibold);

    /* 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, the menu, and the step
   that carries the name and byline past the logo. Not the banner, which runs
   the full width of the window whatever the gutters do -- see
   .site-header__banner-area.
   
   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))
    );
}

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

/* WHERE THE BANNER SITS: THE WHOLE WIDTH OF THE WINDOW, ALWAYS. The banner is the
   header's backdrop, and the gutter setting is about where the header's contents
   line up -- the logo, the name and the menu -- not about how wide the ground
   under them is. It used to stop at the gutters, which left a highway ending in
   two strips of header colour a few centimetres short of either edge, and a
   picture framed by stripes nobody chose. Now the backdrop runs edge to edge and
   the contents line up on top of it exactly as they would without one.

   Nothing about the logo enters into this either. The logo is drawn on top at
   the left gutter and the banner carries on underneath; 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. */
.site-header__banner-area {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}

/* The banner width setting is a percentage of that area -- the window -- so at
   100 it runs edge to edge. */
.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;
}

/* A LIVE BANNER takes the picture's box and draws in it on the graphics card.
   The picture itself is kept for when the live banner is switched off, so it is
   only not drawn here. See assets/js/live_banner.js. */
.site-header__banner--live {
    background-image: none;
}

.site-header__live-banner {
    display: block;
    width: 100%;
    height: 100%;
}

/* A LIVE BANNER DRAWN TO THE COMPACT HEADER'S SCALE IS AS TALL AS ITS SCENE,
   whatever the Banner height setting says -- that sizes the picture. The
   highway is the first: each road filling its own banner made the full
   header's lanes and cars seven tenths as wide again as the compact one's, and
   a road at the compact scale in a banner taller than it needed left a roadside
   nobody wanted. So its banner is its four lanes at the two lane road's width.

   A NUMBER OF COMPACT HEADERS, declared by the scene and sent by the server as
   --site-live-banner-compact-heights -- see getLiveBannerDefinitions(). The
   class is only on the stage of a scene that declares one, so a scene that
   does not keeps the Banner height setting. Set before the page is drawn
   rather than by the scene, which runs afterwards and would make the header
   jump on every visit; the compact header's own rule still takes the height
   away there. */
.site-header__stage--sized-by-scene {
    --site-banner-height: calc(
        var(--site-header-compact-height) * var(--site-live-banner-compact-heights)
    );
}

/* AS TALL AS THE HEADER'S BUTTONS, AND AS WIDE AS ITS OWN SHAPE MAKES IT. The
   admin square and the menu button beside it are both
   --site-header-button-height, so a logo of that height sits on the bar as one
   more thing of the same size, and the Logo size setting scales it down from
   there. Compact halves the buttons, and the logo follows without a rule of its
   own.

   IT WAS A SQUARE, and a wide wordmark in a square box was either cropped to its
   middle -- fill, the default fit, did that -- or shrunk to a strip with air
   above and below. The server reads the picture's width over its height when the
   page is drawn and hands it over as --site-logo-aspect-ratio; the box takes that
   shape, so the picture fills it exactly and there is nothing left to crop,
   stretch or tile. See findSiteLogoAspectRatio().

   THE WIDTH IS SET AND THE HEIGHT FOLLOWS, rather than the other way round, so
   that on a phone too narrow for a long logo the cap below shortens it and it
   keeps its shape. Set by its height, the cap would squeeze its width alone. The
   branding gives up its words before it gives up the logo -- see the wordmark's
   ellipsis rules. */
.site-header__logo-area {
    --site-logo-drawn: calc(var(--site-header-button-height) * var(--site-logo-scale));

    flex: 0 0 auto;
    display: block;

    width: calc(var(--site-logo-drawn) * var(--site-logo-aspect-ratio));
    max-width: 100%;
    aspect-ratio: var(--site-logo-aspect-ratio);
}

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

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

    /* CONTAIN, WHICH IS AN EXACT FIT while the box has the picture's shape, and
       still the whole picture should the two ever disagree -- a file whose size
       could not be read is taken as square. Anchored left, so that any air left
       over then falls away from the gutter rather than between it and the logo. */
    background-size: contain;
    background-position: left center;
}

/* THE LOGO FOR DARK MODE, and its own shape, on a device in dark mode. Each
   falls back to the logo's own where the site has no dark one, so a site with
   one logo draws exactly what it did.

   ONLY ON A HEADER THAT FOLLOWS THE DEVICE, and the stylesheet cannot tell
   which headers those are -- a preset can hold one dark on a light page, or
   light on a dark device. The server can, and for a header held to one scheme
   it sends no dark tokens at all, so there is nothing here to swap to. See
   findLogoColourScheme(). */
@media (prefers-color-scheme: dark) {
    .site-header__logo-area {
        width: calc(var(--site-logo-drawn) * var(--site-logo-aspect-ratio-dark, var(--site-logo-aspect-ratio)));
        aspect-ratio: var(--site-logo-aspect-ratio-dark, var(--site-logo-aspect-ratio));
    }

    .site-header__logo {
        background-image: var(--site-logo-image-dark, var(--site-logo-image));
    }
}

/* Fill crops to cover the box; stretch distorts to fit it exactly; tile repeats
   the picture at its own size. Only the banner has the choice: the logo's box is
   the logo's shape. */
.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: var(--font-weight-base);
    color: var(--colour-site-byline);
}

/* Compact: half the height and no banner picture. A live banner is still drawn,
   at the height of the bar rather than the banner's own -- the highway as two
   lanes. The logo needs nothing here: it is measured from the button height,
   which compact has already halved. */
.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: var(--font-weight-bold);
    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 HOLDS ONE THING, the backdrop, and the backdrop is the row. */
.site-header__branding {
    display: flex;
    align-items: center;
    min-width: 0;
}

/* THE LOGO AND NAME ON A BACKDROP OF THEIR OWN: the logo, then the name and byline
   beside it, on a colour that lets them be read over a banner. As tall as the
   header's buttons, rounded as they are, so it reads as one more thing in the bar.

   A CONTAINER, SO ITS EDGE IS THE ONE ON THE GUTTER. Like a card, the box lines up
   with the page and what is in it sits one tight inset inside. It cannot reach
   out into the gutter instead: the branding link clips what overflows it, which
   is how a long name is cut short rather than pushing the menu away.

   A COLOUR AND A TRANSPARENCY FOR EACH MODE, by the browser's setting as the
   template's own palette is. Mixed with transparent rather than given an alpha,
   because a stored colour is six hex digits and the transparency is its own
   number -- see the tokens at the top of this file. */
.site-header__branding-backdrop {
    display: flex;
    flex: 0 1 auto;
    align-items: center;
    gap: var(--space-small);
    min-width: 0;
    min-height: var(--site-header-button-height);
    padding-inline: var(--container-padding-tight);
    border-radius: var(--radius-small);
    background-color: color-mix(
        in srgb,
        var(--colour-site-branding-backdrop) calc(100% - var(--site-branding-backdrop-transparency) * 1%),
        transparent
    );
}

@media (prefers-color-scheme: dark) {
    .site-header__branding-backdrop {
        background-color: color-mix(
            in srgb,
            var(--colour-site-branding-backdrop-dark) calc(100% - var(--site-branding-backdrop-transparency-dark) * 1%),
            transparent
        );
    }
}

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

/* WHO IS SIGNED IN, BY NAME, where the site has no pictures of people -- see
   areAvatarsEnabled(). It takes the picture's place at the end of the button,
   quieter than the screen's name beside it, and gives way to it first: the
   screen's name is what the button is labelled with. */
.site-header__menu-person {
    flex: 0 1 auto;
    min-width: 0;
    max-width: 50%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    padding-inline-end: var(--site-header-menu-name-padding, var(--space-small));
    font-size: 0.85em;
    opacity: 0.85;
}

/* 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: var(--font-weight-semibold);
    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 COMES FROM THE BUTTON'S OWN TEXT COLOUR, AND USED TO COME FROM THE
   HEADER'S. The old reasoning was that the ring is drawn against the header, so
   the header's text is the one colour known to contrast with what is behind it --
   true while the button was a fourteen percent wash and effectively the header.
   It stopped being true the moment a preset gave the button a surface of its
   own: the ring is inset, so it is drawn on that surface, and Old Way's pale
   button took a near white ring at 1.07:1 and showed nothing at all on hover.

   IT CHANGES NOTHING FOR ANYBODY WHO HAS NOT CHOSEN ONE, because
   --colour-header-menu-button-text defaults to var(--colour-header-text) -- the
   same colour by another name. What it fixes is exactly the case the old rule
   could not see. */
.site-header__menu-button:hover,
.site-header--menu-open .site-header__menu-button {
    box-shadow: inset 0 0 0 1px var(--colour-header-menu-button-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: var(--font-weight-semibold);
    text-decoration: none;
    white-space: nowrap;

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

/* THE ADMIN VIEW, left of the menu -- see includes/page_layout.php. A square as tall as the menu button beside it,
   with an A in it, and filled with the accent while the admin tools are showing, so which way it is switched reads
   without reading it. */
.site-header__admin-view {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--site-header-button-height);
    height: var(--site-header-button-height);
    padding: 0;
    font-weight: var(--font-weight-semibold);
    line-height: 1;
    border: 1px solid var(--colour-header-text);
    border-radius: var(--radius-small);
    background-color: transparent;
    color: var(--colour-header-text);
    font: inherit;
    white-space: nowrap;
    cursor: pointer;
}

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

.site-header__admin-view--on,
.site-header__admin-view--on:hover,
.site-header__admin-view--on:focus-visible {
    border-color: var(--colour-accent);
    background-color: var(--colour-accent);
    color: var(--colour-button-text);
}

.site-header__admin-view:disabled {
    cursor: progress;
    opacity: 0.6;
}

.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: var(--font-weight-bold);
    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 the banner
   invites it: the logo's box is the logo's shape and crops nothing. */
.site-edit-mode-active .site-header--banner-fit-fill .site-header__banner {
    cursor: grab;
}

.site-edit-mode-active .site-header__media--dragging .site-header__banner {
    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: var(--font-weight-semibold);
}

.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: var(--font-weight-semibold);
}

/* 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: var(--font-weight-semibold);
}

.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: var(--font-weight-semibold);
}

.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: var(--font-weight-semibold);
}

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

    /* INHERITED FROM THE ROW, which wears the preset's own font-choice class --
       the same class the font menu hangs on its options. This used to read a
       --preset-font custom property that only this one rule knew about, so the
       square was in the preset's face and the name beside it was in the
       reader's. */
    font-family: 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: var(--font-weight-semibold);
}

.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: var(--font-weight-base);
    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: var(--font-weight-semibold);

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

/* -----------------------------------------------------------------------------
   HOW LONG THIS PAGE TOOK

   An administrator's readout at the foot of a screen -- see
   includes/request_timing.php. A detail list with room for long labels, and the
   copied measurements in a box to select where there is no clipboard.
   -------------------------------------------------------------------------- */

.request-timing__list dd {
    font-variant-numeric: tabular-nums;
}

.request-timing__copy-box {
    width: 100%;
    min-height: 10rem;
    margin-top: var(--space-small);
    font-family: monospace;
    font-size: 0.75rem;
}

/* -----------------------------------------------------------------------------
   THE ADMIN PHOTOS SCREEN

   THE SITE IMAGE TILE'S LOOK, AT A SIZE FOR BROWSING. The Appearance grid is a
   small scrolling box inside a card of other controls; here the grid is the
   screen, so it does not scroll on its own and each tile is a button that opens
   the photo's editor. See system/modules/admin/screen_photos.php.
   -------------------------------------------------------------------------- */

.photo-library__grid {
    display: grid;
    /* TWO ACROSS ON A PHONE. At 375 pixels a wider minimum left one tile to a row,
       and a column of single photos is a long way to scroll to find one. */
    grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
    gap: var(--space-small);

    margin: var(--space-small) 0;
    padding: 0;
    list-style: none;
}

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

    width: 100%;
    height: 100%;
    padding: var(--container-padding-tight);

    font: inherit;
    color: inherit;
    text-align: left;

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

.photo-library__tile:hover {
    background-color: var(--colour-accent-soft);
}

.photo-library__tile:focus-visible {
    outline: 3px solid var(--colour-accent);
    outline-offset: 2px;
}

/* CONTAIN, AND ON THE CHEQUERBOARD, for the site image tile's reasons: a logo
   cropped to a square is not recognisable, and a white icon on nothing is not an
   empty tile. */
.photo-library__picture,
.photo-library__preview {
    display: block;
    width: 100%;
    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;
}

.photo-library__picture {
    height: 7rem;
}

.photo-library__preview {
    max-height: 22rem;
}

.photo-library__preview-link {
    display: block;
    margin-bottom: var(--space-small);
}

.photo-library__caption {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
}

.photo-library__name {
    font-size: 0.82rem;
    font-weight: var(--font-weight-semibold);
    overflow-wrap: anywhere;
}

.photo-library__meta {
    font-size: 0.72rem;
    color: var(--colour-text-secondary);
    overflow-wrap: anywhere;
}

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

/* WHETHER THE PICTURE IS SAVED.
 *
 * QUIET AT REST AND ONLY LOUD WHEN IT IS BAD NEWS. "Saved" is the answer nearly
 * every time somebody looks, so it is small and grey -- there to be found rather
 * than to be noticed. A save that failed is the one state worth interrupting
 * for, and it is the only one that takes a colour.
 *
 * IT RESERVES ITS OWN LINE. The three words are different lengths, and letting
 * the height change as they swap would nudge the controls below every time
 * somebody moved a slider.
 */
.avatar-builder__save-state {
    margin: var(--space-extra-small) 0 0;
    min-height: 1.25rem;

    font-size: 0.8125rem;
    text-align: center;
    color: var(--colour-text-secondary, #6b6257);
}

.avatar-builder__save-state[data-avatar-save-state="failed"] {
    color: var(--colour-danger, #a3341f);
    font-weight: 600;
}

/* 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: var(--font-weight-base);
    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. */
/* ---------------------------------------------------------------------------
 * THE FACE ON THE SIGN-UP FORM
 * ------------------------------------------------------------------------ */

/* A ROW, NOT A COLUMN, WHICH IS THE DIFFERENCE FROM THE ACCOUNT SCREEN. There
   the avatar is the subject of the card and stands above its own button; here it
   sits inside a form between two fields, and a stacked block would read as a
   third field somebody has to fill in. Beside the button it reads as a picture
   with a way to change it. */
.register-avatar {
    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.register-avatar__face .avatar {
    width: 100px;
    height: 100px;
}

/* A TWO COLUMN GRID HOLDING THREE BUTTONS.
   1fr for the labelled ones and auto for the die, so auto placement puts Edit
   avatar and the die on the first row and Use photo on the second -- and both
   labelled buttons land in the same 1fr column, which is what makes them the
   same width. The column sizes itself to the wider of the two labels, so
   neither button carries a width anybody has to keep up to date. */
.register-avatar__aside {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--space-extra-small);
    align-items: stretch;
    justify-items: stretch;
}

/* THE ACCOUNT SCREEN'S PAIR STACK AND MATCH TOO. That column is narrow and two
   buttons side by side there wrap mid-word. */
.identity-summary__avatar .button {
    width: 100%;
}

/* ---------------------------------------------------------------------------
 * CHOOSING A PICTURE: BROWSE, OR CREATE
 * ------------------------------------------------------------------------ */

/* A STRIP OF TWO, UNDERLINED RATHER THAN BOXED. A pair of filled buttons reads
   as two actions; an underline under the live one reads as which of two places
   you are standing in, which is what a tab is. */
/* THE STYLE PRESETS' LIGHT AND DARK MODE TABS ARE THE SAME STRIP, for the same
   reason: which of two lists you are looking at, not two things to do. */
.avatar-chooser__tabs,
.style-presets__modes {
    display: flex;
    gap: var(--space-small);
    margin-bottom: var(--container-padding-tight);
    border-bottom: 1px solid var(--colour-border);
}

.avatar-chooser__tab,
.style-presets__mode {
    padding: var(--space-extra-small) var(--space-small);
    border: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    background: none;
    color: var(--colour-text-secondary);
    font: inherit;
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
}

.avatar-chooser__tab:hover,
.style-presets__mode:hover {
    color: var(--colour-text);
}

.avatar-chooser__tab[aria-selected="true"],
.style-presets__mode[aria-selected="true"] {
    color: var(--colour-accent);
    border-bottom-color: var(--colour-accent);
}

/* THE GRID IS A ROW OF BUTTONS NOW, NOT OF DIVS. Each option is clickable, so
   it has to be a control -- which brings a button's own background, border and
   padding with it, all of which have to go for the picture to be the button. */
.avatar-picker__option--choosable {
    padding: var(--space-extra-small);
    border: 1px solid transparent;
    border-radius: var(--radius-medium);
    background: none;
    cursor: pointer;
}

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

.avatar-picker__option--choosable:focus-visible {
    outline: 2px solid var(--colour-accent);
    outline-offset: 2px;
}

.avatar-picker__option--choosable:disabled {
    opacity: 0.5;
    cursor: progress;
}

/* ---------------------------------------------------------------------------
 * THE MOSAIC STUDIO
 * ------------------------------------------------------------------------ */

/* THE PICTURE AND THE CONTROLS SIT SIDE BY SIDE AND STACK WHEN THEY CANNOT.
   Sliders whose effect is off the top of the screen are sliders somebody drags
   blind, so the preview has to stay in view while they move -- which on a phone
   means the preview first and the controls under it, and on anything wider
   means both at once. minmax(0, ...) on both tracks because an SVG in a grid
   cell will otherwise refuse to shrink below its own width. */
.mosaic-studio__layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--container-padding);
    align-items: start;
}

@media (max-width: 40rem) {
    .mosaic-studio__layout {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* THE PREVIEW STAYS PUT WHILE THE CONTROLS SCROLL PAST IT on a tall window.
   sticky rather than fixed, so it stops at the end of its own column instead of
   floating over what comes after. */
.mosaic-studio__stage {
    position: sticky;
    top: 0;
}

/* A CHEQUERBOARD BEHIND THE GLASS, because a mosaic with pale grout on a pale
   card has no edge, and the first thing anybody does is wonder where it ends.
   Drawn with gradients rather than an image so it costs no request. */
.mosaic-studio__preview {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 12rem;
    padding: var(--container-padding-tight);
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium);
    background-color: var(--colour-page-background);
    background-image:
        linear-gradient(45deg, var(--colour-border) 25%, transparent 25%),
        linear-gradient(-45deg, var(--colour-border) 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, var(--colour-border) 75%),
        linear-gradient(-45deg, transparent 75%, var(--colour-border) 75%);
    background-size: 16px 16px;
    background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

/* A SQUARE HOLE, BECAUSE THE AVATAR IS A SQUARE. The mosaic inside covers the
   whole photograph and the viewBox picks the square out of it, so the drawing
   fills this exactly -- what is on screen is what gets kept. */
.mosaic-studio__preview {
    aspect-ratio: 1;
    overflow: hidden;
}

.mosaic-studio__preview svg {
    display: block;
    width: 100%;
    height: 100%;
    border-radius: var(--radius-small);

    /* THE PICTURE IS DRAGGED, SO IT MUST NOT ALSO BE SELECTED OR SCROLLED.
       touch-action none is what stops a phone treating a one-finger drag as a
       page scroll and a two-finger pinch as a page zoom, which would leave the
       gestures fighting the browser for the same movement. */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    cursor: grab;
}

.mosaic-studio__preview--grabbing svg {
    cursor: grabbing;
}

.mosaic-studio__hint {
    margin: var(--space-extra-small) 0 0;
    font-size: 0.8rem;
    color: var(--colour-text-muted);
}

/* THE SWATCH ROW, DRAWN AS THE AVATAR BUILDER DRAWS ITS OWN. The radio itself
   is off screen rather than display:none, so it keeps its place in the tab
   order and a screen reader still announces the group. */
.mosaic-studio__swatches {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-extra-small);
    margin-top: var(--space-extra-small);
}

.mosaic-studio__swatch {
    display: inline-flex;
    cursor: pointer;
}

.mosaic-studio__swatch-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/* EVERY CHIP HAS AN EDGE, WHICH MATTERS FOR EXACTLY ONE OF THEM. White grout
   is the default and the most useful setting on the row, and on a white card it
   was a gap between two other swatches -- the one people most want was the one
   they could not see. A hairline costs nothing on the other nine. */
.mosaic-studio__chip {
    width: 1.6rem;
    height: 1.6rem;
    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small);
    outline: 2px solid transparent;
    outline-offset: 2px;
}

/* THE TICKED ONE AND THE FOCUSED ONE BOTH SHOW, and they are different things:
   an outline for the choice, and the focus ring for where the keyboard is. */
.mosaic-studio__swatch-input:checked + .mosaic-studio__chip {
    outline-color: var(--colour-accent);
}

.mosaic-studio__swatch-input:focus-visible + .mosaic-studio__chip {
    outline-color: var(--colour-text);
    outline-offset: 3px;
}

.mosaic-studio__empty {
    margin: 0;
    color: var(--colour-text-muted);
}

/* HALF LIT WHILE A NEW ONE IS BEING CUT. The reply takes long enough to notice
   at small tile sizes, and a preview that simply sits there reads as a control
   that did nothing. */
.mosaic-studio--working .mosaic-studio__preview {
    opacity: 0.55;
}

.mosaic-studio__measure {
    margin: var(--space-extra-small) 0 0;
    font-size: 0.85rem;
    color: var(--colour-text-muted);
    font-variant-numeric: tabular-nums;
}

.mosaic-studio__controls {
    display: flex;
    flex-direction: column;
    gap: var(--space-small);
}

.mosaic-studio__slider input[type="range"] {
    width: 100%;
}

/* THE TWO ENDS SAY WHAT THE ENDS MEAN, which is the whole label a slider needs
   once it has a name above it. A number would be worse: nobody knows what a
   colour variation of 0.35 is. */
.mosaic-studio__ends {
    display: flex;
    justify-content: space-between;
    margin: 0;
    font-size: 0.78rem;
    color: var(--colour-text-muted);
}

.mosaic-studio__colour {
    height: 2.4rem;
    padding: 0.2rem;
}

.mosaic-studio__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-small);
    margin-top: var(--container-padding-tight);
}

/* A SQUARE, AND SQUARE BY DERIVATION RATHER THAN BY DECREE. Equal padding on
   every side and an aspect ratio of one, so it stays square at any font size and
   in any preset -- none of which move layout, but the type scale does. */
.register-avatar__reroll {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.4rem;
    aspect-ratio: 1;
}

.register-avatar__reroll svg {
    display: block;
    width: 1.1rem;
    height: 1.1rem;
}


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

       THE TIGHT ONE, because it is a panel of controls rather than of reading:
       a search box and some tick lists, each already with room of its own. The
       full inset round them read as a box padded inside a padded page. */
    padding: var(--container-padding-tight);

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

/* INSIDE A CARD OR THE WINDOW, NO PANEL OF ITS OWN. The card already gives it a
   surface, an edge and an inset; a second set inside the first was a box in a
   box, padded twice -- what the management screens and the trade inventory drew.
   The card's inset is the only one. */
.card .shop-filters,
.modal-panel__body .shop-filters {
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
}

/* 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: var(--font-weight-bold);
    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;
}

/* -----------------------------------------------------------------------------
   FILTERS FOLDED BEHIND THE SEARCH BOX ON A PHONE

   THE WRAPPER IS NOT SUPPOSED TO BE THERE. page_shop.js gathers the filter blocks
   into one panel so a single class can fold them, and display:contents hands
   their layout straight back to the form -- which matters because
   .shop-filters--admin is a two column grid, and a real box round its children
   would put all of them in one cell.

   THE MEDIA QUERY IS THE WHOLE OF THE MOBILE ONLY RULE. No script measures the
   window; above the breakpoint the toggle is not drawn and the panel is always
   laid out, so a wide screen behaves as it did before any of this existed.
   -------------------------------------------------------------------------- */
.shop-filters__panel {
    display: contents;
}

.shop-filters__toggle {
    display: none;
}

@media (max-width: 40rem) {
    .shop-filters__toggle {
        display: inline-block;

        /* IT LINES UP UNDER THE SEARCH BOX rather than beside it. At this width
           there is no room beside anything. */
        width: 100%;
    }

    /* THE ADMIN GRID SPANS IT, or a two column grid would put the button in one
       half and leave the other empty. */
    .shop-filters--admin .shop-filters__toggle {
        grid-column: 1 / -1;
    }

    .shop-filters:not(.is-filters-open) .shop-filters__panel {
        display: none;
    }
}


.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: var(--font-weight-semibold);
    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: var(--font-weight-bold);
    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: var(--font-weight-semibold);
    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);
}

/* -----------------------------------------------------------------------------
   The font and size controls under the preset list
   -----------------------------------------------------------------------------
   TWO FIELDS SIDE BY SIDE WHERE THERE IS ROOM, stacked where there is not. They
   are one decision in two halves -- which face, and how big -- so they read
   better as a pair than as two rows of a form. */
.style-basics {
    margin: var(--space-medium) 0 0;
    padding: 0;

    border: none;
}

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

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

/* The per-font rules themselves come from renderFontChoiceStyles(), which has to
   generate them: there are twenty odd faces and the policy forbids a style
   attribute. All this file says is that the menu is allowed to be taller than the
   text, since a face with tall ascenders would otherwise be clipped. */
#styleBasicsFont,
#styleBasicsFont option {
    line-height: 1.6;
}

/* -----------------------------------------------------------------------------
   The header on a phone
   -----------------------------------------------------------------------------
   THE MENU BUTTON MUST GIVE WAY TOO, ONCE THERE IS LITTLE ENOUGH ROOM. The rule
   further up sets it to --site-header-menu-button-width and lets the branding be
   the only thing that shrinks, which is the right order on a desktop: the button
   is a fixed target and the site name can lose a word without harm.

   It is the wrong order at 375px. The button held its full 250px -- two thirds of
   the whole bar -- and left the wordmark 77px to render 209px of text in, so the
   site was called "General..." on every phone. Nothing was overflowing, so no
   amount of looking for a scrollbar would have found it.

   min() RATHER THAN A NEW NUMBER, so an administrator who has set the width to
   something smaller keeps it: this is a ceiling on a phone, not a replacement
   value. The branding still shrinks first; it simply is no longer the only thing
   that can. */
@media (max-width: 40rem) {
    .site-header__menu-button {
        width: min(var(--site-header-menu-button-width), 52vw);
    }
}

/* -----------------------------------------------------------------------------
   The header on two rows, when a phone has not the width for one
   -----------------------------------------------------------------------------
   BRANDING ABOVE, MENU BELOW, EACH WITH THE WHOLE WIDTH. The bar is one flex row
   with the two pushed apart, which is right until they are both competing for the
   same three hundred pixels -- past that the only question is which of them gets
   cut, and the answer was always the site's own name.

   Stacked, neither has to give: the wordmark gets the full width to say what the
   site is called, and the menu button gets the full width under it, which also
   makes it a target somebody can hit with a thumb.

   SET BY AN ADMINISTRATOR, NOT BY THE WIDTH ALONE. Whether one row still works at
   375px depends on how long the name is and how wide the button has been made, so
   this is a choice on the Header in site edit mode rather than something the
   stylesheet decides for everybody. Above the breakpoint the class does nothing:
   the rule is in here. */
@media (max-width: 40rem) {
    .site-header--stacked-on-mobile .site-header__bar {
        flex-direction: column;
        align-items: stretch;
        justify-content: flex-start;
    }

    /* THE SCROLLBAR GUTTER IS FOR KEEPING A ROW ALIGNED. In a column it is an
       empty block carrying a negative margin -- a gap where nobody wanted one,
       and nothing to align with. */
    .site-header--stacked-on-mobile .site-header__scrollbar-gutter {
        display: none;
    }

    /* THE BRANDING STOPS CUTTING ITSELF SHORT. It is the only flex item in the
       bar allowed to shrink -- flex: 1 1 auto with overflow: hidden -- which is
       what truncated the site name to make room for the button. On its own row
       there is nothing to make room for. */
    .site-header--stacked-on-mobile .site-header__branding {
        overflow: visible;
    }

    /* CENTRED, BECAUSE A LEFT ALIGNED NAME OVER A FULL WIDTH BUTTON READS AS TWO
       UNRELATED THINGS. Stacked, they are one block and should look like one. */
    .site-header--stacked-on-mobile .site-header__branding {
        justify-content: center;
        text-align: center;
    }

    /* FULL WIDTH, WHICH IS THE POINT OF STACKING. --site-header-menu-button-width
       is a ceiling for a row that has to share; a row of its own has no one to
       share with, so a button left at that ceiling sits stranded in the middle of
       its own line.

       IT IS .site-header__menu THAT HAS TO STRETCH, and that is worth writing
       down because it is not the obvious element. The bar's children are the
       branding, the MENU, and the scrollbar gutter -- the <nav> is inside the
       menu, and the button inside that. Stretching the nav or the button alone
       does nothing, because the box around them is still only as wide as its
       contents. */
    .site-header--stacked-on-mobile .site-header__menu {
        width: 100%;
        max-width: none;
    }

    .site-header--stacked-on-mobile .site-header__navigation,
    .site-header--stacked-on-mobile .site-header__menu-button {
        width: 100%;
        max-width: none;
    }
}

/* -----------------------------------------------------------------------------
   The second row as links rather than a button
   -----------------------------------------------------------------------------
   THE MENU IS ALREADY A LIST OF LINKS; on a phone there is room to just show it.
   A button that opens a panel is the right answer when the menu is competing for
   width with the branding. Once the two are on separate rows, the whole width is
   free, and a row of links people can hit directly beats one they have to open
   first.

   THE AVATAR STAYS, AS A SQUARE AT THE LEFT of that row. It is the only thing in
   the header that says who you are signed in as, and losing it to save a menu tap
   would be a poor trade.

   THE NAV IS TAKEN OUT OF ITS PANEL, not revealed in place. Collapsed, it is an
   absolutely positioned card floating clear of the header with its own
   background, border and shadow; shown inline it has to give all of that back or
   it is a card sitting inside the bar. */
@media (max-width: 40rem) {
    .site-header--mobile-links .site-header__menu {
        display: flex;
        align-items: center;
        gap: var(--space-small);

        /* IT IS AS TALL AS THE LINKS, NOT AS TALL AS THE BUTTON. The menu is
           given the button's height so the header is a predictable strip -- fine
           when it holds one button, wrong when it holds a list that wraps. Left
           at 64px the second row of links hung out of the bottom of the header
           and over the top of the page. */
        height: auto;
        min-height: var(--site-header-button-height);

        /* NO WRAP HERE, THOUGH THE LINKS INSIDE IT WRAP. Letting this row wrap
           put the nav on a line of its own under the avatar, which is a square
           sitting on top of a list rather than beside it. The nav is the flexible
           item: it takes what is left of the row and wraps within it. */
        flex-wrap: nowrap;
    }

    /* THE SQUARE. Just the picture: the name beside it is what made the button
       wide, and on this row the links need that width more than the word "Home"
       does. */
    .site-header--mobile-links .site-header__menu-button {
        flex: 0 0 auto;
        width: auto;
        min-width: 0;
    }

    .site-header--mobile-links .site-header__menu-name {
        display: none;
    }

    /* OUT OF THE PANEL AND INTO THE ROW. Every one of these undoes something the
       collapsed state needs -- see the .site-header__navigation rule further up,
       which positions it absolutely and hides it until --menu-open. */
    .site-header--mobile-links .site-header__navigation {
        position: static;
        min-width: 0;
        width: auto;
        max-width: none;
        visibility: visible;
        opacity: 1;
        transform: none;
        transition: none;

        /* A ROW THAT WRAPS, NOT A COLUMN. Collapsed, the nav is a dropdown panel
           and stacks its links -- flex-direction: column, min-width: 12rem. Shown
           inline those two turn it into a tall narrow list spilling out of the
           header, which is exactly what happened the first time this was tried.
           Both have to be given back, not just the positioning. */
        flex-direction: row;
        flex-wrap: wrap;
        flex: 1 1 auto;
        min-width: 0;
        justify-content: flex-start;
        align-items: center;
        gap: var(--space-small) var(--space-medium);

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

/* The width switch at the top of a site edit panel. Two radios on one line, set
   apart from the sliders under them so it reads as the question they answer to
   rather than as another control among them. */
.tooltip-overlay__field--width-mode {
    padding-bottom: var(--space-small);
    border-bottom: 1px solid var(--colour-border);
}

.tooltip-overlay__width-choices {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-small) var(--space-medium);
}

.tooltip-overlay__width-choice {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);
    cursor: pointer;
}

/* -----------------------------------------------------------------------------
   The copy editor
   -----------------------------------------------------------------------------
   THE SAME DASHED OUTLINE THE OTHER MODES USE, because it already means "this is
   a thing you may change" everywhere else on the site -- see .style-mode-editable
   above. A third visual language for a third mode would be three things to learn
   for one idea. */
.copy-mode-active [data-copy-key] {
    outline: 2px dashed var(--colour-accent);
    outline-offset: 3px;
    cursor: text;
}

.copy-mode-active [data-copy-key]:hover {
    outline-width: 3px;
}

/* WHAT IS BEING EDITED RIGHT NOW, told apart from everything else that could be.
   The same relationship .style-mode-selected has to .style-mode-editable. */
.copy-mode-active [data-copy-key].copy-mode-chosen {
    outline: 3px dashed var(--colour-accent);
    background-color: var(--colour-accent-soft);
}

/* HIDDEN, AND ONLY AN ADMINISTRATOR IS SEEING THIS AT ALL. Everybody else never
   receives the element -- see applySiteCopyToRenderedPage(), which drops it from
   the markup rather than hiding it in CSS, so it is out of the flow, out of the
   accessibility tree and out of the page source.

   GREYED RATHER THAN STRUCK THROUGH. A strike reads as "deleted", and this is
   reversible by clicking the thing it is drawn on. */
.site-copy--hidden {
    opacity: 0.45;
}

.copy-mode-active .site-copy--hidden {
    outline-style: dotted;
}

/* -----------------------------------------------------------------------------
   Marks for the developer, left in site edit mode
   -----------------------------------------------------------------------------
   HIDDEN FOR EVERYBODY UNTIL SITE EDIT MODE IS ON, administrators included, so
   the page can be seen as a visitor will see it. The style block in the head
   does this before the first paint -- see renderLayoutMarkStyleBlock() -- and
   the class below does it for a mark made since the page was drawn.

   IN SITE EDIT MODE, GREYED AND STILL THERE, so it can be found and unmarked:
   the same rule hidden copy follows. A note is a dotted line round the thing it
   is about, in amber rather than the accent, so a mark is never mistaken for the
   outline of what the pointer is on. See assets/js/layout_marks.js. */
body:not(.site-edit-mode-active) .layout-mark--hidden:not([data-layout-mark-unmatched]) {
    display: none !important;
}

.site-edit-mode-active .layout-mark--hidden {
    opacity: 0.45;
    outline: 2px dashed var(--colour-warning);
    outline-offset: 2px;
}

.site-edit-mode-active .layout-mark--noted {
    outline: 2px dotted var(--colour-warning);
    outline-offset: 2px;
}

/* THE ELEMENT THE MARKING SECTION IS ABOUT, which is often smaller than the
   region the panel's layout controls are about. */
.site-edit-mode-active .layout-mark-picked {
    outline: 3px dashed var(--colour-warning);
    outline-offset: 3px;
}

/* The marking section of the right click panel, below the region's layout. */
.layout-mark-panel {
    display: grid;
    gap: var(--space-small);
    margin-top: var(--space-small);
    padding-top: var(--space-small);
    border-top: 1px solid var(--colour-border);
}

.layout-mark-panel .tooltip-overlay__title,
.layout-mark-panel .tooltip-overlay__scope,
.layout-mark-panel .tooltip-overlay__hint {
    margin: 0;
}

.layout-mark-panel__switch {
    display: flex;
    align-items: center;
    gap: var(--space-small);
    font-size: 0.85rem;
}

.layout-mark-panel__note {
    width: 100%;
    font-size: 0.85rem;
    resize: vertical;
}

/* THE CHANGES TO MAKE LIST AS TEXT, shown only when the browser would not copy it,
   selected and ready for Ctrl+C. See page_admin_appearance.js. */
.layout-marks-text {
    width: 100%;
    min-height: 12rem;
    margin-top: var(--space-small);
    font-family: var(--font-family-monospace, monospace);
    font-size: 0.8rem;
}

/* The little editor that opens under the words being changed. Absolutely
   positioned against the document, so it stays with the text when the page
   scrolls rather than floating over whatever happens to be under it. */
.copy-editor-panel {
    position: absolute;
    z-index: 60;
    width: min(28rem, calc(100vw - 2rem));

    display: grid;
    gap: var(--space-small);

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

    color: var(--colour-card-text);
    background-color: var(--colour-card-background);
    border: 1px solid var(--colour-border-strong, var(--colour-border));
    border-radius: var(--radius-medium);
    box-shadow: var(--shadow-card);
}

.copy-editor-panel__field {
    width: 100%;
    font-family: var(--font-family-base);
}

.copy-editor-panel__note {
    margin: 0;
    font-size: var(--font-size-quiet);
    color: var(--colour-text-secondary);
}

.copy-editor-panel__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-small);
}

.copy-editor-panel__hide {
    display: flex;
    align-items: center;
    gap: var(--space-extra-small);
    margin-right: auto;
    font-size: var(--font-size-quiet);
    cursor: pointer;
}

/* -----------------------------------------------------------------------------
   TILES OR A LIST, AND SORTING THE LIST

   THE SWITCH IS TWO LINKS IN ONE OUTLINE, so the pair reads as one control with
   two states rather than two buttons that happen to sit together. The current one
   is filled; the other is not.
   -------------------------------------------------------------------------- */
.list-view-toggle {
    display: inline-flex;
    align-self: flex-end;
    margin-bottom: var(--space-small);

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

.list-view-toggle__choice {
    padding: 0.2rem var(--space-small);

    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    text-decoration: none;

    color: var(--colour-text-secondary);
    background: none;
}

.list-view-toggle__choice + .list-view-toggle__choice {
    border-left: 1px solid var(--colour-border);
}

.list-view-toggle__choice.is-current {
    color: var(--colour-button-text);
    background-color: var(--colour-accent);
}

.list-view-toggle__choice:not(.is-current):hover,
.list-view-toggle__choice:not(.is-current):focus-visible {
    color: var(--colour-text-primary);
    background-color: var(--colour-accent-soft);
}

/* THE WHOLE HEADING IS THE TARGET, not a word inside it. A column heading is a
   small thing to hit and the cell is already exactly the right size. */
.sortable-column {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-extra-small);

    color: inherit;
    text-decoration: none;
}

.sortable-column:hover,
.sortable-column:focus-visible {
    text-decoration: underline;
}

/* THE MARKER HOLDS ITS WIDTH WHETHER OR NOT IT IS SORTED, so the headings do not
   shift sideways as the reader moves the sort from column to column. */
.sortable-column__marker {
    flex: 0 0 auto;
    width: 1em;

    font-size: 0.75em;
    text-align: center;

    opacity: 0.45;
}

.sortable-column.is-sorted .sortable-column__marker {
    opacity: 1;
}

/* THE PICTURE AND THE WORDS SIDE BY SIDE, and the words take the slack. A fixed
   thumbnail keeps every row the same height and the left edge of the names in one
   straight line, which is what makes a list scannable. */
.product-list__name-cell {
    display: flex;
    align-items: flex-start;
    gap: var(--space-small);
}

.product-list__picture {
    flex: 0 0 auto;
    width: 3rem;
    height: 2.25rem;

    object-fit: cover;
    border-radius: var(--radius-small);

    /* SOMETHING BEHIND IT WHILE IT LOADS, so a slow image is a quiet rectangle in
       the row rather than the row jumping when it arrives. */
    background-color: var(--colour-page-background);
}

.product-list__name {
    flex: 1 1 auto;
    min-width: 0;
}

/* A DESCRIPTION UNDER THE NAME, not beside it. On one line the two run together
   and the eye cannot find where the name ends. */
.product-list__description {
    display: block;

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

/* Prices line up as digits, which is the only way a sorted price column can be
   read down at a glance. */
.product-list__price {
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

/* -----------------------------------------------------------------------------
   THE LIST ON A NARROW SCREEN

   THE SAME MOVE THE FORUM'S THREAD TABLE ALREADY MAKES: the cells stop being
   columns and stack, each printing the heading it has lost from its own
   data-label. Four columns of product, producer, price and quantity cannot be
   four columns at 375px without either scrolling sideways -- which the layout
   standard forbids -- or squeezing the price into two characters a line.

   THE HEADINGS STAY, AS A ROW OF SORTS. This is where the forum's version had to
   be changed rather than copied: it hides thead outright, which was right when a
   heading was only a label and wrong now that it is the only way to sort. Stacked,
   the header row becomes a wrapping bar of sort links -- the same links, not a
   second copy of them, so there is nothing to keep in step.
   -------------------------------------------------------------------------- */
@media (max-width: 38rem) {
    .product-list,
    .product-list tbody,
    .product-list tbody tr {
        display: block;
        width: 100%;
    }

    .product-list thead,
    .forum-threads thead {
        display: block;
    }

    .product-list thead tr,
    .forum-threads thead tr {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-extra-small);

        padding-bottom: var(--space-extra-small);
    }

    /* A CHIP RATHER THAN A CELL. Table borders and alignment mean nothing once
       the row is a flex line, and the underline a heading carries would run the
       width of the screen. */
    /* THE HEADINGS' OWN SIZE, NOT A SMALLER ONE. It was 0.7rem -- twelve pixels of
       capitals, and an arrow of nine -- inside a chip that a touch screen makes 44
       pixels tall, so the row read as a set of empty boxes with something faint
       written in them. Every stacked table draws its sorts the same way; see
       .table-scroll table.is-stacked further down. */
    .product-list thead th,
    .forum-threads thead th {
        display: inline-block;
        width: auto;
        padding: 0.15rem var(--space-small);

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

        font-size: 0.8rem;
    }

    .product-list thead th[aria-sort]:not([aria-sort="none"]),
    .forum-threads thead th[aria-sort]:not([aria-sort="none"]) {
        border-color: var(--colour-accent);
    }

    /* A HEADING THAT CANNOT BE SORTED IS NOT A CHIP. Only the sortable ones carry
       aria-sort, and drawing a border round the others made "In your cart" look
       like a button that did nothing when pressed. */
    .product-list thead th:not([aria-sort]),
    .forum-threads thead th:not([aria-sort]) {
        border-color: transparent;
        color: var(--colour-text-secondary);
    }

    .product-list tbody tr {
        padding: var(--container-padding-tight) 0;
        border-bottom: 1px solid var(--colour-border);
    }

    .product-list tbody td {
        display: block;
        width: auto;
        padding: 0.1rem 0;
        border: 0;
        text-align: left;
    }

    /* THE NAME CELL KEEPS ITS TWO COLUMNS. Everything else stacks, but a picture
       above its own name would push the actual reading matter a thumbnail further
       down every row for no gain. */
    .product-list tbody td.product-list__name-cell {
        display: flex;
    }

    /* WHAT THE MISSING HEADING SAID, from the attribute so the two cannot drift.
       The name needs none: it is the first line and obviously the product. */
    .product-list tbody td[data-label]::before {
        content: attr(data-label) ": ";
        color: var(--colour-text-secondary);
        font-size: 0.8rem;
    }

    /* The price stops being a column to line up with, so it stops being right
       aligned -- a lone right aligned line in a stack reads as a mistake. */
    .product-list__price {
        text-align: left;
    }
}

/* -----------------------------------------------------------------------------
   AN ACCOUNT'S NAME, WITH ITS FACE

   THE AVATAR IS PART OF THE NAME, so the two sit on one line and the name takes
   the slack. A fixed tile keeps every row the same height and the left edge of the
   names in one straight column, which is what makes a list of people scannable.
   -------------------------------------------------------------------------- */
.account-row__name-cell {
    display: flex;
    align-items: center;
    gap: var(--space-small);
}

.account-row__name-cell .avatar {
    flex: 0 0 auto;
}

.account-row__name {
    flex: 1 1 auto;
    min-width: 0;
}

/* -----------------------------------------------------------------------------
   A SUBCARD: A GROUP INSIDE A CARD THAT HAS ITS OWN START AND END

   WHEN TO REACH FOR ONE, AND WHEN NOT TO. A .card__section divides a card into
   parts that are read in order -- a rule between siblings and nothing else, which
   is right when the parts are one subject in stages. A subcard is for a group that
   is its own thing: a form that can be filled in and saved without touching the
   one below it. The member editor is the case that asked for it -- account details
   and group membership are two independent forms that sat flush against each
   other with nothing to say where one ended.

   AN OUTLINE AND NOT A FILL, WHICH IS THE WHOLE OF THE DISTINCTION. The layout
   standard's objection to boxing a section is that a background makes a panel
   inside a panel -- two surfaces where the reader can only be standing on one. An
   unfilled outline marks a boundary without claiming to be a second surface, so
   the card stays the surface and the subcard is a line drawn on it.

   NO SHADOW, FOR THE SAME REASON. A shadow is what a card uses to sit above the
   page; a group inside one is not above anything.
   -------------------------------------------------------------------------- */
.card__subcard {
    padding: var(--container-padding);

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

.card__subcard + .card__subcard {
    margin-top: var(--container-padding);
}

/* THE TITLE SITS ON THE EDGE IT NAMES, so the group reads as beginning there
   rather than as a heading that happens to be followed by one. */
.card__subcard-title {
    margin: 0 0 var(--space-small);

    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;

    color: var(--colour-card-subtitle, var(--colour-text-secondary));
    font-family: var(--font-family-card-section-title, inherit);
    font-weight: var(--font-weight-semibold);
}

/* THE LAST THING IN A SUBCARD DOES NOT NEED SPACE UNDER IT -- the subcard's own
   inset is already there, and a trailing margin would double it. */
.card__subcard > :last-child {
    margin-bottom: 0;
}

/* INSIDE A MODAL IT KEEPS ITS OUTLINE, deliberately. A card in the modal gives up
   its panel because the window is the panel -- see .modal-panel__body .card -- but
   a subcard was never a panel, and the boundary is the only thing it was for. */
.modal-panel__body .card__subcard {
    border: 1px solid var(--colour-border);
    padding: var(--container-padding);
}

/* A SUBCARD THAT FOLDS. The expandable card's summary, one level down: the title
   and a line saying what is inside, with the same marker across the bottom. See
   renderExpandableSubcardStart() in includes/page_layout.php.

   SHUT, THE TITLE KEEPS NO MARGIN UNDER IT. Its usual gap separates it from the
   content, and shut there is no content to separate it from -- only the line and
   the marker, which carry their own. */
.card__subcard-summary {
    display: block;
    cursor: pointer;
    list-style: none;
}

.card__subcard-summary::-webkit-details-marker {
    display: none;
}

.card__subcard-summary .card__subcard-title {
    margin-bottom: 0;
}

.card__subcard-summary .card__subcard-title,
.card__subcard-summary .card__subcard-line {
    pointer-events: none;
}

.card__subcard-line {
    display: block;
    margin-top: var(--space-extra-small);

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

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

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

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

/* -----------------------------------------------------------------------------
   THE LANGUAGE SWITCHER

   ABSOLUTELY POSITIONED, WHICH IS WHAT KEEPS IT INDEPENDENT OF THE FOOTER. The
   footer can be turned off in the layout manager and the switcher still has to
   be there -- so it is a sibling of the footer, taken out of the flow, and it
   neither moves the footer nor is moved by its absence.

   SEMI-TRANSPARENT, SO IT SITS OVER THE PAGE RATHER THAN CUTTING A HOLE IN IT.
   A solid panel pinned to the bottom corner reads as a piece of the page that
   the page forgot to lay out; letting what is underneath show through says it
   is a control resting on top.

   THE COLOUR COMES FROM THE CARD TOKENS AND THE ALPHA IS ADDED HERE, so it
   follows every preset and both themes without a colour of its own to keep in
   step -- color-mix does the alpha against the token rather than against a
   hard-coded white that would go black in Midnight.
   -------------------------------------------------------------------------- */

.language-switcher {
    /* PINNED TO THE BOTTOM CENTRE OF THE CONTENT COLUMN. .page-main is the
       module content container and is already position: relative, so this hangs
       off that rather than off the window -- it lines up with the content
       whatever the header, footer or gutters are doing.

       THE FOOTER IS NOT ITS PARENT, and must not be: the footer can be turned
       off in the layout manager, and the way to change the site's language
       cannot go with it. */
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 0;
    z-index: 7;

    /* THE HANDLE LAST, SO IT IS THE PART TOUCHING THE BOTTOM. Collapsed, the
       panel above it is gone and the handle is all that is left -- still at the
       centre of the very bottom, which is where somebody will look for it. */
    display: flex;
    flex-direction: column;
    align-items: center;

    /* NO BACKGROUND OF ITS OWN. The panel and the handle each carry their own,
       so that a collapsed switcher is a small tab rather than a tab sitting on
       an invisible box the width of nine languages. */
    max-width: 100%;
}

.language-switcher__panel {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 2px;

    max-width: 100%;
    padding: 0.25rem;

    border: 1px solid color-mix(in srgb, var(--colour-border) 60%, transparent);
    border-bottom: 0;
    border-radius: var(--radius-small) var(--radius-small) 0 0;

    /* SEMI-TRANSPARENT, SO IT RESTS ON THE PAGE RATHER THAN CUTTING A HOLE IN
       IT, and the colour is mixed from the card token so it follows every preset
       and both themes without a colour of its own to keep in step. */
    background-color: color-mix(in srgb, var(--colour-card-background) 82%, transparent);
    backdrop-filter: blur(6px);
}

/* color-mix IS NOT EVERYWHERE, and a background-color a browser cannot parse is
   dropped -- which would leave text on text. Declared first so a browser that
   understands the mix overrides it, and one that does not keeps something
   solid. */
@supports not (background-color: color-mix(in srgb, red 50%, transparent)) {
    .language-switcher__panel,
    .language-switcher__handle {
        background-color: var(--colour-card-background);
    }
}

.language-switcher__choice {
    padding: 0.15rem 0.45rem;
    border: 0;
    border-radius: var(--radius-small);

    background: none;
    color: var(--colour-text-secondary);
    font: inherit;
    font-size: 0.78rem;
    line-height: 1.4;
    white-space: nowrap;
    cursor: pointer;
}

.language-switcher__choice:hover,
.language-switcher__choice:focus-visible {
    color: var(--colour-text-primary);
    background-color: var(--colour-accent-soft, rgba(127, 127, 127, 0.14));
}

/* THE ONE IN FORCE IS MARKED BY MORE THAN COLOUR. aria-pressed says it to a
   screen reader; weight says it to everybody who cannot rely on a hue. */
.language-switcher__choice--current {
    color: var(--colour-text-primary);
    font-weight: var(--font-weight-semibold);
}

.language-switcher__choice:disabled {
    opacity: 0.6;
    cursor: progress;
}

.language-switcher__handle {
    display: flex;
    align-items: center;
    gap: 0.3rem;

    padding: 0.1rem 0.6rem;
    border: 1px solid color-mix(in srgb, var(--colour-border) 60%, transparent);
    border-bottom: 0;
    border-radius: var(--radius-small) var(--radius-small) 0 0;

    background-color: color-mix(in srgb, var(--colour-card-background) 82%, transparent);
    backdrop-filter: blur(6px);

    color: var(--colour-text-secondary);
    font: inherit;
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    cursor: pointer;
}

.language-switcher__handle:hover,
.language-switcher__handle:focus-visible {
    color: var(--colour-text-primary);
}

/* THE CHEVRON POINTS THE WAY THE PANEL WILL GO. Up while shut, because clicking
   it opens the panel upwards; down while open, because clicking it puts the
   panel away. A marker that never moves is decoration. */
.language-switcher__chevron {
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 5px solid currentColor;
}

.language-switcher[data-collapsed="no"] .language-switcher__chevron {
    border-bottom: 0;
    border-top: 5px solid currentColor;
}

/* ON A PHONE THE NINE WRAP RATHER THAN RUN OFF THE EDGE. The panel is centred on
   a column narrower than the strip, so without this the first and last
   languages would sit outside the screen with no way to reach them. */
@media (max-width: 40rem) {
    .language-switcher__panel {
        max-width: calc(100vw - (2 * var(--container-padding-tight)));
    }

    .language-switcher__choice {
        font-size: 0.72rem;
        padding: 0.15rem 0.35rem;
    }
}


/* -----------------------------------------------------------------------------
   WORDS STILL WAITING FOR A TRANSLATION

   WHAT THE MARK MEANS. The reader asked for a language this string is not in
   yet, so they are being shown the English -- either because it has never been
   translated, or because the English was reworded and the old translation was
   withdrawn as no longer describing it. Both are the same thing to a reader:
   this sentence is not in your language.

   AN ASTERISK RATHER THAN A COLOUR ALONE. Tinting the words themselves would
   fail for anybody who cannot rely on hue, and would fight every preset -- the
   text still has to be readable against its own background. A mark after the
   words carries the meaning in its shape and takes the warning colour as
   reinforcement rather than as the whole message.

   aria-hidden BECAUSE THE ELEMENT SAYS IT PROPERLY. The fallback carries
   lang="en", which is what actually tells a screen reader to pronounce it as
   English; hearing "asterisk" after every untranslated label would be noise
   saying the same thing worse.
   -------------------------------------------------------------------------- */

.copy-awaiting-translation {
    margin-left: 0.15em;
    color: var(--colour-warning);
    font-weight: var(--font-weight-bold);

    /* IT MUST NOT CHANGE THE LINE. A mark that alters the height of every
       untranslated heading would move the page about as translations arrive. */
    line-height: 1;
    vertical-align: baseline;
    cursor: help;
}

/* WHO ELSE IS GETTING THIS, on the owner's own profile only.
 *
 * QUIETER THAN THE VALUE IT FOLLOWS. It is a note about the field rather than
 * part of it, and a reader scanning their own details wants the details first --
 * so it is smaller, secondary, and sits on its own line rather than competing
 * with the value for the start of the row. */
.detail-list__audience {
    display: block;
    font-size: 0.8125rem;
    color: var(--colour-text-secondary, #4d5f56);
}

/* ---------------------------------------------------------------------------
 * Your friends
 *
 * A ROW IS A FACE, A NAME AND WHAT CAN BE DONE ABOUT THEM, and the name takes
 * whatever is left between the other two. Sized in fractions of the row rather
 * than in pixels so a long username wraps instead of pushing the buttons off the
 * edge -- the card is inside an expandable card inside the account column, and
 * at 375px there is less room here than anywhere else on the site.
 *
 * IT SITS DIRECTLY ABOVE THE PRIVACY CARD, and the two are read as one subject.
 * Nothing here introduces a spacing value they do not share.
 * ------------------------------------------------------------------------ */

.friends-card {
    display: grid;
    gap: var(--space-medium);
}

.friends-list {
    display: grid;
    gap: var(--space-extra-small);
    margin: 0;
    padding: 0;
    list-style: none;
}

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

    /* THE SAME THUMB-SIZED ROW AS THE PRIVACY SWITCHES. Two lists of people-sized
       decisions on one screen should not be two different heights. */
    min-height: 2.75rem;
    padding-block: var(--space-extra-small);
}

/* A DIVIDING LINE BETWEEN PEOPLE, NOT AROUND THEM. A box per row would make a
   list of three friends look like three cards. */
.friends-list__person + .friends-list__person {
    border-top: 1px solid var(--colour-border, #d8ded9);
}

.friends-list__avatar {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
}

.friends-list__name {
    /* IT TAKES THE SPACE AND GIVES IT BACK. Growing pushes the buttons to the
       right edge; the zero basis with a minimum width is what lets a long name
       wrap rather than widen the row past the card. */
    flex: 1 1 6rem;
    min-width: 6rem;
    overflow-wrap: anywhere;
}

.friends-list__actions {
    display: flex;
    gap: var(--space-extra-small);
    flex: 0 0 auto;
    align-items: center;
}

/* THE FIELD AND ITS BUTTON END LEVEL. The field brings a label and help text
   above it, so aligning the two on their tops would leave the button floating
   beside the label rather than beside the box it acts on. */
.friends-card__ask {
    display: flex;
    gap: var(--space-small);
    align-items: flex-end;
    flex-wrap: wrap;
}

.friends-card__ask .form-field {
    flex: 1 1 12rem;
    margin-bottom: 0;
}

/* ---------------------------------------------------------------------------
 * What others can see
 *
 * ONE ROW PER DECISION, WITH ROOM BETWEEN THEM. These come from different
 * modules and read as a list of unrelated questions, so the spacing has to do
 * the work a heading would -- each switch, its explanation and its answer are
 * one group, and the gap is what says where one ends.
 *
 * THE STATUS LINE SITS UNDER ITS OWN SWITCH. A single line for the whole card
 * would report a change under a row nobody was looking at.
 * ------------------------------------------------------------------------ */

.privacy-settings {
    display: grid;
    gap: var(--space-medium);
}

.privacy-settings__item {
    display: grid;
    gap: var(--space-extra-small);
}

/* THE QUESTION, THE EXPLANATION AND THE ANSWER, STACKED. It was a tick box with
   the words beside it, which worked while the answer was yes or no; a menu is
   wide enough that putting it on the same line either squeezes the question or
   pushes the menu off a phone. */
.privacy-settings__label {
    display: block;
    font-weight: var(--font-weight-semibold);
    cursor: pointer;
}

.privacy-settings__choice {
    /* IT DOES NOT STRETCH TO THE CARD. A menu of four short phrases spanning the
       full width puts its words at one end and its arrow at the other, and the
       eye has to travel the gap to check what it says. */
    max-width: 22rem;

    /* BIG ENOUGH TO HIT WITH A THUMB, like the map's layer toggle. */
    min-height: 2.75rem;
}

.privacy-settings__status {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--colour-text-secondary, #4d5f56);
}

/* THE ONE STATE WORTH INTERRUPTING FOR. A setting that did not save means the
   site disagrees with what the switch is showing, which is the whole thing this
   card exists to be trusted about. */
.privacy-settings__status--error {
    color: var(--colour-danger, #a3341f);
    font-weight: 600;
}

/* ---------------------------------------------------------------------------
 * The corner of the window
 *
 * ONE DOCK, SHARED. Anything that has to be in reach on every screen and belongs
 * to none of them sits here -- the friends button, and a module's own control such
 * as a language switcher -- so two of them never land on the same spot in the same
 * corner. See the dock in renderPageFooter().
 *
 * AND THE DOCK A MENU, OPENED BY ONE BUTTON. It was a row of small buttons along the
 * bottom edge, every one of them over somebody else's screen; it is one button now,
 * and the dock a panel above it with each control a row, named beside its icon.
 * Their own panels still hang from the dock, above the menu, as they hung from the
 * row. See window_menu.js.
 *
 * THE EMPTY PART OF IT TAKES NO PRESSES. It is laid over the page, and a box as
 * wide as its widest open panel would otherwise swallow clicks on whatever it
 * covers. Only the things inside it answer the pointer.
 * ------------------------------------------------------------------------ */

.window-menu {
    position: fixed;
    right: var(--space-small, 0.5rem);
    bottom: var(--space-small, 0.5rem);
    z-index: 40;

    display: flex;
    flex-direction: column;
    align-items: flex-end;

    pointer-events: none;
}

.window-menu > * {
    pointer-events: auto;
}

.window-menu__toggle {
    position: relative;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-small, 0.5rem);

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small, 0.25rem);
    background-color: var(--colour-card-background);
    color: var(--colour-text-primary);
    line-height: 1;
    cursor: pointer;

    /* THE SAME RESTRAINT THE BUTTONS IT GATHERS HAD: over somebody else's screen, it
       should not compete with it until it is wanted. */
    opacity: 0.75;
}

.window-menu__toggle[hidden] {
    display: none;
}

.window-menu__toggle:hover,
.window-menu__toggle:focus-visible,
.window-menu__toggle[aria-expanded="true"] {
    opacity: 1;
}

.window-menu__icon {
    display: block;
    width: 1.15rem;
    height: 1.15rem;
}

/* WHAT HAS NOT BEEN SEEN, IN ALL OF IT: a number on the corner of the button, as the
   bell's and the friends' own are. */
.window-menu__badge {
    position: absolute;
    top: -0.4rem;
    right: -0.4rem;
    min-width: 1.1rem;
    padding: 0.1rem 0.25rem;

    border-radius: 999px;
    background-color: var(--colour-danger, #a3341f);
    color: #ffffff;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1;
    text-align: center;
}

.window-menu__badge[hidden] {
    display: none;
}

/* THE MENU: the dock, above the button and shut until it is opened. Positioned, so
   the panels of the controls in it hang from it as they hung from the row. */
.window-dock {
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-small, 0.5rem));

    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-extra-small, 0.25rem);
    min-width: 14rem;
    padding: var(--space-small, 0.5rem);

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-medium, 0.5rem);
    background-color: var(--colour-card-background);
    color: var(--colour-text-primary);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.22);
}

.window-menu.is-open > .window-dock {
    display: flex;
}

/* EACH CONTROL A ROW: its button the width of the menu, its name written beside its
   icon -- the name its button already gives a screen reader -- and none of the
   restraint it had over the page, since it is only seen when it is wanted. */
.window-dock [data-window-dock-opener] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-small, 0.5rem);
    width: 100%;
    min-width: 0;
    /* ONE HEIGHT FOR EVERY ROW, whoever drew it. Each module's button brought
       its own icon and line height, and the menu came out 39, 37 and 49 pixels
       a row -- a list that looked assembled from parts, with two of the three
       under the size a finger needs. */
    min-height: 3rem;
    padding: var(--space-small, 0.5rem) var(--space-medium, 1rem);

    font-size: 0.875rem;
    text-align: left;
    opacity: 1;
}

.window-dock [data-window-dock-opener]::before {
    content: attr(aria-label);
    flex: 1 1 auto;
    min-width: 0;
}

/* ---------------------------------------------------------------------------
 * Friends and chat
 *
 * THE MENU AND THE CONVERSATION HANG FROM THE DOCK, NOT FROM THE BUTTON. Both are
 * positioned against .window-dock, which is the nearest positioned box, so their
 * right edges line up with the corner however many buttons the dock holds -- and
 * on a phone they can take the width of the window rather than the width of a
 * button's column.
 *
 * NO BACKGROUND IS INVENTED. Every surface here is the card background and every
 * line the border colour, so a site's palette and a member's own styling reach the
 * corner the same way they reach a card.
 * ------------------------------------------------------------------------ */

/* HIDDEN MEANS HIDDEN. Several of these set display for their layout, and a class
   that says display: flex outweighs the browser's own [hidden] rule -- a menu that
   stayed on screen after being hidden was the first thing this would have done. */
.friend-chat [hidden] {
    display: none !important;
}

.friend-chat__toggle {
    position: relative;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-small, 0.5rem);

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small, 0.25rem);
    background-color: var(--colour-card-background);
    color: var(--colour-text-primary);
    line-height: 1;
    cursor: pointer;

    /* THE SAME RESTRAINT AS THE LANGUAGE BUTTON BESIDE IT. Both sit over somebody
       else's screen and should not compete with it until they are wanted. */
    opacity: 0.75;
}

.friend-chat__toggle:hover,
.friend-chat__toggle:focus-visible,
.friend-chat__toggle[aria-expanded="true"] {
    opacity: 1;
}

.friend-chat__icon {
    display: block;
    width: 1.15rem;
    height: 1.15rem;
}

/* ONLINE, AWAY, OFFLINE: SAID IN SHAPE AS WELL AS COLOUR. Online and away are
   filled, offline is an empty ring, and everywhere a dot appears in a list the word
   is written beside it -- nobody should need to tell green from amber. */
.friend-chat__presence {
    display: inline-block;
    flex: 0 0 auto;
    box-sizing: border-box;
    width: 0.625rem;
    height: 0.625rem;

    border: 2px solid var(--colour-text-secondary, #4d5f56);
    border-radius: 50%;
    background-color: transparent;
}

.friend-chat__presence--online {
    border-color: var(--colour-success, #2f8a4f);
    background-color: var(--colour-success, #2f8a4f);
}

.friend-chat__presence--away {
    border-color: var(--colour-warning, #c7851a);
    background-color: var(--colour-warning, #c7851a);
}

/* ON THE BUTTON, WHAT FRIENDS CURRENTLY SEE YOU AS, in the corner of it. Outlined in
   the card background so it reads as a badge on the button rather than a smudge. */
.friend-chat__toggle > .friend-chat__presence {
    position: absolute;
    left: -0.2rem;
    bottom: -0.2rem;
    box-shadow: 0 0 0 2px var(--colour-card-background);
}

.friend-chat__badge {
    min-width: 1.15rem;
    padding: 0 0.3rem;

    border-radius: 999px;
    background-color: var(--colour-accent);
    color: var(--colour-text-inverse, #ffffff);

    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.15rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.friend-chat__toggle > .friend-chat__badge {
    position: absolute;
    top: -0.45rem;
    right: -0.45rem;
}

/* The menu the button opens: your status, then your friends. */
.friend-chat__menu {
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-small, 0.5rem));

    /* ABOVE AN OPEN CONVERSATION, which hangs from the same corner. The menu is the
       thing somebody just asked for. */
    z-index: 2;

    display: grid;
    gap: var(--space-small, 0.5rem);
    box-sizing: border-box;
    width: min(18rem, calc(100vw - 2 * var(--space-small, 0.5rem)));
    max-height: 70vh;
    overflow-y: auto;
    padding: var(--container-padding-tight);

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small, 0.25rem);
    background-color: var(--colour-card-background);
    color: var(--colour-text-primary);
    box-shadow: 0 2px 8px rgb(0 0 0 / 0.18);
}

.friend-chat__heading {
    margin: 0;
    padding: 0;
    color: var(--colour-text-secondary, #4d5f56);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.friend-chat__hint {
    margin: 0;
    color: var(--colour-text-secondary, #4d5f56);
    font-size: 0.75rem;
}

.friend-chat__status {
    display: grid;
    gap: var(--space-extra-small, 0.25rem);
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.friend-chat__status-choice {
    display: flex;
    align-items: center;
    gap: var(--space-small, 0.5rem);

    /* THUMB-SIZED, like the privacy switches. A radio the size of its dot is a
       miss waiting to happen on a phone. */
    min-height: 2.25rem;
    font-size: 0.9rem;
    cursor: pointer;
}

.friend-chat__friends {
    display: grid;
    gap: 1px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.friend-chat__friend {
    display: flex;
    align-items: center;
    gap: var(--space-small, 0.5rem);
    box-sizing: border-box;
    width: 100%;
    min-height: 2.5rem;
    padding: 0 var(--space-small, 0.5rem);

    border: 0;
    border-radius: var(--radius-small, 0.25rem);
    background: none;
    color: var(--colour-text-primary);

    font: inherit;
    font-size: 0.9rem;
    text-align: start;
    cursor: pointer;
}

.friend-chat__friend:hover,
.friend-chat__friend:focus-visible {
    background-color: var(--colour-accent-soft, rgb(0 0 0 / 0.06));
}

/* A LONG NAME SHORTENS; IT DOES NOT PUSH THE STATE OFF THE ROW. */
.friend-chat__friend-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.friend-chat__friend-state {
    flex: 0 0 auto;
    color: var(--colour-text-secondary, #4d5f56);
    font-size: 0.75rem;
}

/* The conversation. */
.friend-chat__conversation {
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-small, 0.5rem));

    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    width: min(22rem, calc(100vw - 2 * var(--space-small, 0.5rem)));
    overflow: hidden;

    border: 1px solid var(--colour-border);
    border-radius: var(--radius-small, 0.25rem);
    background-color: var(--colour-card-background);
    color: var(--colour-text-primary);
    box-shadow: 0 2px 8px rgb(0 0 0 / 0.18);
}

.friend-chat__bar {
    display: flex;
    align-items: center;
}

.friend-chat__conversation:not(.friend-chat__conversation--collapsed) .friend-chat__bar {
    border-bottom: 1px solid var(--colour-border);
}

.friend-chat__collapse {
    display: flex;
    flex: 1 1 auto;
    align-items: center;
    gap: var(--space-small, 0.5rem);
    min-width: 0;
    min-height: 2.75rem;
    padding: 0 var(--container-padding-tight);

    border: 0;
    background: none;
    color: inherit;

    font: inherit;
    font-weight: 600;
    text-align: start;
    cursor: pointer;
}

.friend-chat__with {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.friend-chat__close {
    flex: 0 0 auto;
    width: 2.75rem;
    height: 2.75rem;

    border: 0;
    background: none;
    color: var(--colour-text-secondary, #4d5f56);

    font-size: 1.25rem;
    line-height: 1;
    cursor: pointer;
}

.friend-chat__close:hover,
.friend-chat__close:focus-visible {
    color: var(--colour-text-primary);
}

.friend-chat__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-small, 0.5rem);
    min-height: 0;
    padding: var(--container-padding-tight);
}

.friend-chat__earlier {
    align-self: center;
}

/* A FIXED HEIGHT, SO THE WINDOW DOES NOT GROW AS THE CONVERSATION DOES. A panel that
   got taller with every message would climb up the screen over whatever somebody
   was reading, and scroll position inside it would mean nothing. */
.friend-chat__messages {
    display: flex;
    flex-direction: column;
    gap: var(--space-small, 0.5rem);
    height: min(20rem, 45vh);
    margin: 0;
    padding: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    list-style: none;
}

.friend-chat__message {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    align-self: flex-start;
    max-width: 85%;
}

.friend-chat__message--mine {
    align-items: flex-end;
    align-self: flex-end;
}

.friend-chat__text {
    margin: 0;
    padding: var(--space-extra-small, 0.25rem) var(--space-small, 0.5rem);

    border-radius: var(--radius-small, 0.25rem);
    background-color: var(--colour-page-background);

    /* LINE BREAKS ARE THE WRITER'S, and a long address or a pasted link breaks
       wherever it must rather than widening the window. */
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.friend-chat__message--mine .friend-chat__text {
    background-color: var(--colour-accent-soft, rgb(0 0 0 / 0.06));
}

/* WHAT WAS ACTUALLY WRITTEN, UNDER A TRANSLATION. Fine and italic because it is a
   footnote to the message rather than a second one, and one line with an ellipsis
   so a long original never doubles the height of the conversation. Pressing it
   opens the whole thing, as ordinary selectable text. */
.friend-chat__original {
    display: block;
    box-sizing: border-box;
    max-width: 100%;
    margin-top: 0.15rem;
    padding: 0 var(--space-extra-small, 0.25rem);

    color: var(--colour-text-secondary, #4d5f56);
    font-size: 0.72rem;
    font-style: italic;
    line-height: 1.35;
    text-align: start;

    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: pointer;
}

.friend-chat__original--expanded {
    overflow: visible;
    overflow-wrap: anywhere;
    white-space: pre-wrap;
    cursor: text;
}

.friend-chat__original:focus-visible {
    outline: 2px solid var(--colour-accent);
    outline-offset: 1px;
}

.friend-chat__time {
    margin-top: 0.15rem;
    padding: 0 var(--space-extra-small, 0.25rem);
    color: var(--colour-text-secondary, #4d5f56);
    font-size: 0.68rem;
    font-variant-numeric: tabular-nums;
}

.friend-chat__composer {
    display: flex;
    align-items: flex-end;
    gap: var(--space-small, 0.5rem);
}

.friend-chat__input {
    flex: 1 1 auto;
    box-sizing: border-box;
    min-width: 0;
    min-height: 2.5rem;
    max-height: 8rem;
    padding: var(--space-extra-small, 0.25rem) var(--space-small, 0.5rem);

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

    font: inherit;
    font-size: 0.9rem;
    resize: vertical;
}

/* -----------------------------------------------------------------------------
   34. Phones and touch screens
      What a finger and a phone need that a mouse and a wide screen do not: text in
      a field large enough that the page does not zoom, controls large enough to
      hit, and tables that turn into a stack of rows once their columns no longer
      fit. Found by walking every screen at 375 pixels with touch on.
   -------------------------------------------------------------------------- */

/* NO FIELD BELOW SIXTEEN PIXELS ON A TOUCH SCREEN, WHATEVER SIZE THE TEXT IS.
   An iPhone zooms the whole page the moment a field smaller than that takes the
   focus, and leaves it zoomed after the keyboard goes, so the reader has to pinch
   back out every time they type. The audit found one administrator's own text
   size putting every field on the site at 14 pixels and the map's tool menus at
   11 -- nobody chose a zooming site; they chose a smaller font.

   A FLOOR, NOT A STYLE, WHICH IS WHY IT IS !important. Modules size their own
   compact fields -- the map's tool menus, the price box in a haves list -- and each
   would otherwise have to remember this. max() keeps anything larger: a person who
   chose large print keeps it.

   .pin-input IS THE ONE FIELD DRAWN LARGER ON PURPOSE, and a floor must not pull a
   larger thing down to it.

   TOUCH ONLY, BECAUSE THE ZOOM IS. A small field under a mouse costs nothing. */
@media (pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]):not(.pin-input),
    select,
    textarea {
        font-size: max(16px, var(--font-size-input)) !important;
    }
}

/* FORTY-FOUR PIXELS FOR ANYTHING A FINGER HAS TO HIT. Apple's guidance; Google's is
   forty-eight. Measured short of it: sort links at 15 pixels, publish switches at 19,
   a window's close button at 25 by 21, checkboxes at 13, fields at 41.

   IN px AND em, NEVER rem. The root follows the reader's chosen text size -- see the
   html rule in section 1 -- so a touch size in rem would shrink for exactly the
   person who made their text smaller, who is the one with the least to aim at.

   ONLY THE TEMPLATE'S OWN CONTROLS. A module's compact toolbar or stepper is its own
   to size, and a blanket rule on every button would also have grown the map's
   controls and the editor's formatting row without anybody looking at them. */
@media (pointer: coarse) {
    .form-field__input,
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="number"],
    input[type="search"],
    input[type="tel"],
    input[type="url"],
    select {
        min-height: 44px;
    }

    /* THE PADDING THAT MAKES THE HEIGHT, RATHER THAN A min-height. A button is
       inline-block, so a min-height would leave its label at the top with the
       extra space under it; padding worked out from the line height keeps the
       label in the middle at every text size. 1.3em is .button's line height, and
       the 1px is its border. */
    .button,
    .button--small {
        padding-top: max(0.4rem, calc((44px - 1.3em) / 2 - 1px));
        padding-bottom: max(0.4rem, calc((44px - 1.3em) / 2 - 1px));
    }

    .switch,
    .sortable-column {
        min-height: 44px;
    }

    .modal-panel__close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
    }

    /* LARGER BOXES, AND THE ROW STILL THE TARGET. A checkbox is almost always inside
       its label here, so the whole line answers a tap; the box only has to be big
       enough to read as ticked or not. A box hidden on purpose -- the haves and
       wants lists keep theirs out of sight -- is left as it is.

       :where(), SO IT WEIGHS NOTHING. It is a size for a box nobody has sized, and
       any rule a module wrote for its own boxes has to win. At the weight of a
       class and two attributes it beat them: the delivery ratings keep their star
       radios at one pixel and see-through, with the star drawn as the label, and
       this put a 22 pixel box back in front of every star. */
    :where(input[type="checkbox"]:not(.visually-hidden), input[type="radio"]:not(.visually-hidden)) {
        width: 22px;
        height: 22px;
    }

    .checkbox-grid__item {
        min-height: 44px;
    }
}

/* A TABLE THAT NO LONGER FITS BECOMES A STACK OF ROWS, on a phone.

   SIDEWAYS SCROLLING WAS THE ANSWER, AND ON A PHONE IT HIDES MOST OF THE TABLE. The
   accounts list showed names and nothing else; the datastore's tables showed half a
   table name; the checkout put the + button and the total off the edge of the card.
   A reader does not know there is anything to the right.

   ONLY TABLES THAT OVERFLOW. assets/js/stacking_tables.js measures each table against
   its .table-scroll and sets is-stacked on the ones wider than it, and labels every
   cell from its column heading. A two-column table that fits stays a table, because
   a stack is longer and there was nothing wrong with it.

   AND ONLY BELOW 40rem, so a wide screen keeps the columns and its own scrolling
   whatever the script decided -- a narrow window on a desktop is not a phone.

   A TABLE THAT MUST KEEP ITS COLUMNS carries data-table--columns, and the script
   leaves it alone.

   THE FIRST CELL IS THE ROW'S TITLE and carries no label: it is the name, the
   product, the table -- what the reader is looking for down the stack. The others
   are labelled above their value rather than beside it, because a value can be
   anything -- a field, a list, a button -- and only a label above survives all of
   them. */
@media (max-width: 40rem) {
    .table-scroll table.is-stacked,
    .table-scroll table.is-stacked tbody,
    .table-scroll table.is-stacked tr,
    .table-scroll table.is-stacked td,
    .table-scroll table.is-stacked tbody th {
        display: block;
        width: auto;
    }

    /* THE HEADINGS GO, EXCEPT THE ONES THAT DO SOMETHING. A sort link lives in its
       heading; dropping the row would take the sorting with it, so those headings
       stay as a wrapped row of controls above the stack. A heading that only names
       its column has handed its words to the cells and has nothing left to say. */
    .table-scroll table.is-stacked thead tr {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-extra-small);

        padding-bottom: var(--space-extra-small);
    }

    .table-scroll table.is-stacked thead th {
        display: none;
        border-bottom: 0;
        padding-left: 0;
        padding-right: 0;
    }

    /* A CHIP, AS THE SHOP'S AND THE FORUM'S ARE. These were bare words in a row,
       so the one control left on the table looked like a caption, and the same
       sort was a box on one screen and a word on the next. */
    .table-scroll table.is-stacked thead th:has(a, button) {
        display: block;
        padding: 0.15rem var(--space-small);

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

    .table-scroll table.is-stacked thead th[aria-sort]:not([aria-sort="none"]) {
        border-color: var(--colour-accent);
    }

    .table-scroll table.is-stacked tbody tr {
        padding: var(--space-small) 0;
        border-bottom: 1px solid var(--colour-border);
    }

    /* BREAKING ANYWHERE, because a stacked cell has the row's width and nothing to
       scroll into. A table's own name -- trade_product_category_assignments -- is
       one word as far as wrapping is concerned, and it ran 59 pixels out of the
       datastore's list without this. */
    .table-scroll table.is-stacked td,
    .table-scroll table.is-stacked tbody th {
        padding: var(--space-extra-small) 0;
        border-bottom: 0;
        white-space: normal;
        overflow-wrap: anywhere;
    }

    .table-scroll table.is-stacked td:empty {
        display: none;
    }

    /* A FIELD IN A STACKED CELL TAKES THE ROW'S WIDTH AND NO MORE. An editing
       table -- the forum's boards, edited in place -- gives its fields widths that
       suit a column: the summary's min-width of 288 pixels stuck out of the stack
       by 27, and a min-width beats a max-width, so both are set. */
    .table-scroll table.is-stacked td :is(input, select, textarea) {
        min-width: 0;
        max-width: 100%;
        box-sizing: border-box;
    }

    .table-scroll table.is-stacked tr > :first-child {
        font-weight: var(--font-weight-semibold);
    }

    .table-scroll table.is-stacked td[data-label]:not([data-label=""]):not(:first-child)::before {
        content: attr(data-label);
        display: block;

        font-size: 0.75rem;
        font-weight: var(--font-weight-base);
        text-transform: uppercase;
        letter-spacing: 0.04em;
        color: var(--colour-text-secondary);
    }
}

/* THE CORNER BUTTON'S SHARE OF THE BOTTOM OF THE WINDOW. The window menu is pinned
   over the bottom right of every screen -- see .window-menu -- and on a phone that
   corner is content: the last line of a list, the Say button under the scene. This
   is how much room to leave it, worked out from the same numbers that place it: a
   44 pixel button, its offset from the edge, and a gap the same size again. */
:root {
    --window-menu-clearance: calc(44px + var(--space-small) * 2);
}

@media (pointer: coarse) {
    .window-menu__toggle {
        min-width: 44px;
        min-height: 44px;
    }
}

/* THE LAST OF A SCREEN SCROLLS CLEAR OF IT. The content area's own bottom inset was
   shorter than the button, so the end of every long screen stopped underneath it:
   the last row of a list could be read around and not tapped.

   :where() SO A FULL BLEED SCREEN STILL WINS. The scene and the map take every pixel
   and set no padding at all; at the specificity of .page-main alone, their own rules
   outrank this one and nothing of theirs moves. */
@media (max-width: 40rem) {
    :where(body:has(.window-menu__toggle:not([hidden]))) .page-main {
        padding-bottom: calc(var(--window-menu-clearance) + var(--space-small));
    }
}

/* A ROW THAT SCROLLS SIDEWAYS FADES AT THE EDGE WITH MORE BEYOND IT.

   The toolbar's screens and the forum's boards hide their scrollbar -- a strip one
   control tall cannot spare half of itself for one -- and on a phone that left a row
   cut off at the edge with nothing to say it went on. assets/js/scroll_strips.js sets
   the two classes while there is something past that edge, and brings the current
   entry into view when the page opens.

   A MASK, NOT A GRADIENT LAID OVER THE ROW. An overlay has to be the colour of
   whatever is behind the strip, which is the toolbar's surface here and a module's own
   elsewhere, and it sits over the links at the edge and takes their taps. A mask takes
   the pixels away instead: it works on any surface, and what is under it stays live.

   .scroll-strip IS THE SAME THING FOR A MODULE'S OWN ROW of chips or tabs: it scrolls,
   hides its bar, and does not hand a swipe at its end on to the page. */
.scroll-strip {
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}

.scroll-strip::-webkit-scrollbar {
    display: none;
}

.scroll-strip--more-end {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 2.5rem), transparent);
}

.scroll-strip--more-start {
    -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to left, #000 calc(100% - 2.5rem), transparent);
}

.scroll-strip--more-start.scroll-strip--more-end {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(to right, transparent, #000 2.5rem, #000 calc(100% - 2.5rem), transparent);
}

/* THE TOOLBAR GROWS BY FOUR PIXELS UNDER A FINGER, and its links fill it. Forty was
   chosen as enough for a control and too little for a second content area; forty four
   is the smallest thing a thumb can be asked to hit, and the reason for a fixed height
   -- that no module can quietly grow the bar -- still holds at the new number. */
@media (pointer: coarse) {
    .module-toolbar {
        height: 44px;
    }

    .module-toolbar__screen {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }
}

/* THE MAIN ACTION OF A LONG WINDOW STAYS IN REACH, on a phone. A window's body
   scrolls, and the button that finishes the form inside it was at the bottom of that
   scroll: the trade window's Send this offer sat fourteen hundred pixels down, under a
   keyboard, with nothing on screen to say it was there.

   STICKY, SO IT IS ONLY HELD WHEN IT WOULD OTHERWISE BE OUT OF SIGHT. A short form
   draws its row where it always did; a long one keeps the row on the bottom edge of
   the window while the form is on screen, and lets it go with the form. Two forms in
   one window each hold their own row in turn.

   THE LAST ROW OF ITS CONTAINER ONLY, AND ONLY WITH SOMETHING TO PRESS IN IT. A row
   of buttons part way down a form is not what finishes it. A form that saves itself
   keeps its Save button hidden until there is a change to save, and its row pinned
   empty was a bar with nothing in it. And a row whose only button deletes is not the
   thing anybody came to the window to reach: the account editor's Delete this
   account sits at the end of the whole editor, and held there it floated over every
   section above it.

   NOT bottom: 0. A sticky box is held inside its scroller's padding, so 0 left the
   row one inset short of the window's edge with the form showing through beneath it.

   THE SIDES ARE COVERED BY SHADOWS, NOT BY STRETCHING THE ROW. Without them what
   scrolls under the row shows in the inset either side of it. A negative margin would
   have reached that inset too, and pushed the window body into scrolling sideways
   wherever the row sits deeper than one card; a shadow paints there and takes no
   room. */
@media (max-width: 40rem) {
    .modal-panel__body .form-actions:last-child:has(> .button:not([hidden]):not(.is-hidden):not(.button--danger)) {
        position: sticky;
        bottom: calc(var(--container-padding) * -1);
        z-index: 1;

        margin-top: var(--space-medium);
        padding: var(--space-small) 0;

        background-color: var(--colour-card-background);
        border-top: 1px solid var(--colour-border);
        box-shadow:
            calc(var(--container-padding) * -1) 0 0 var(--colour-card-background),
            var(--container-padding) 0 0 var(--colour-card-background);
    }
}

/* A VISITOR'S HEADER ON A PHONE: THE NAME ON ONE ROW, SIGN IN AND CREATE ACCOUNT ON
   THE NEXT. The two links are wider together than a member's menu button, and the bar
   gives way by shrinking the branding -- so at 375 pixels the site's name was cut to
   "General ..." on the one screen a newcomer sees first.

   ONLY FOR A VISITOR. A member's single button fits beside the name, and whether that
   row is still too tight is the stacked header's setting in site edit mode, which an
   administrator chooses with their own name in front of them.

   EACH LINK HALF THE ROW, which is also the biggest target either could be given. */
@media (max-width: 40rem) {
    .site-header__bar:has(.site-header__sign-in) {
        flex-wrap: wrap;
        row-gap: var(--space-small);
    }

    .site-header__bar:has(.site-header__sign-in) .site-header__branding {
        flex-basis: 100%;
    }

    /* THE SCROLLBAR GUTTER KEEPS A ROW ALIGNED, and in a wrapped bar it would wrap
       onto a row of its own: nothing to see, and a gap's worth of height. */
    .site-header__bar:has(.site-header__sign-in) .site-header__scrollbar-gutter {
        display: none;
    }

    /* ORDER AND MARGINS BACK TO NOTHING. The menu's position setting puts it ahead of
       the branding or pushes it to one end; on a row of its own under the name, it is
       simply the second row. */
    .site-header__menu:has(> .site-header__sign-in) {
        order: 0;
        width: 100%;
        max-width: none;
        height: auto;
        margin: 0;
    }

    .site-header__menu > .site-header__sign-in,
    .site-header__menu > .site-header__create-account {
        flex: 1 1 0;
        justify-content: center;
    }
}

@media (pointer: coarse) {
    .site-header__sign-in,
    .site-header__create-account {
        min-height: 44px;
    }
}

/* THE TILES OR LIST SWITCH, UNDER A FINGER. Two small capitals in a thin outline
   were 25 pixels tall -- the smallest thing left on the shop. The words keep their
   size; the halves grow round them. */
@media (pointer: coarse) {
    .list-view-toggle__choice {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
    }
}

/* A CATEGORY TREE'S ROWS, UNDER A FINGER. Each row is wide but was 28 pixels tall,
   stacked one on another, so a thumb meant for Hard cheese opened Soft cheese. The
   tree grows longer; that is the cheaper mistake. */
@media (pointer: coarse) {
    .tree-row__open {
        min-height: 44px;
    }
}
