/* ==========================================================================
   SENSIFLY
   The original palette - green #57bc90 and blue #77c9d4, brightened for a dark
   ground - and the Jura display face, on a layout that works on a phone.
   ========================================================================== */

@font-face {
    font-family: 'Jura';
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: local('Jura'), url('../include/Jura-Regular.otf') format('opentype');
}

@font-face {
    font-family: 'Jura';
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: local('Jura'), url('../include/Jura-DemiBold.otf') format('opentype');
}

/* --------------------------------------------------------------------------
   Theme
   --------------------------------------------------------------------------
   Colours are only ever referenced through these tokens, so nothing after this
   block names a colour of its own. Gradients, glows and focus rings are tokens
   too, which is what keeps a change of mind here from becoming an edit spread
   across the whole file.

   One theme and one palette. Green is the accent - the original #57bc90,
   lifted to #4fd39b so it holds against a near-black ground - and it names
   what the product is about: headings, the figures a sensor reports, the rail
   under the top bar, a tick beside a plan's line.

   Blue #77c9d4, lifted to #63d3e8, is green's partner rather than a second
   accent. It is the far end of every gradient - the wordmark, the buttons, the
   headline figures - and stands on its own only on the info notice.

   Everything else is neutral. Links, field labels, the sorted column and
   anything under the pointer are three weights of grey, which is what leaves
   the green meaning something when it does appear. Grey and not white: on a
   ground this dark, white is glare rather than emphasis.

   Buttons are all one colour. .secondary and .danger still say what they are
   in the markup, but neither is drawn differently - a delete reads as a delete
   from its label and the sentence above it.
   -------------------------------------------------------------------------- */

:root {
    /* The accent, and the one colour on the page that means anything by
       itself. --green-ink is the same green as text, where a heading wants
       less weight than a rule or a button. */
    --green:       #4fd39b;
    --green-ink:   #7fe3c0;
    --green-dark:  #6ae0ad;

    /* Green's partner in every gradient, and the info notice's border. */
    --blue:        #63d3e8;

    /* Three weights of neutral text: the body, a step up for a link, a field
       label or the sorted column, and the brightest for whatever is under the
       pointer. Grey rather than white - against a ground this dark, white runs
       to around 18:1, which is far past legible and into glare. These land
       between 10:1 and 15:1, which keeps the steps between them readable
       without the page having to shout. */
    --ink:         #bcc9ce;
    --ink-strong:  #d4dee1;
    --ink-hover:   #e8f0f2;
    --muted:       #8ba3ab;
    --line:        #282a2b;
    --panel:       #171919;
    --page:        #0d0e0f;
    --danger:      #ff8a78;
    --warn:        #e8b45c;

    --topbar:      rgba(20, 21, 22, .82);
    --input-bg:    #121313;
    --code-bg:     #1d1e20;
    --pre-bg:      #0b0c0d;
    --pre-ink:     #bccdd4;
    --secret-ink:  #7fe0b0;

    /* Syntax colouring in the code examples, all of it on --pre-bg. No new
       hues: a string is the green the headings use, a keyword is the blue at
       the far end of every gradient, a number is the warning amber, and a name
       being called is the brightest neutral. Only the comment is its own
       value - a grey dimmer than --muted, because a comment is the one part of
       an example a reader can skip. */
    --code-comment: #6e8791;
    --code-meta:    #8ba3ab;
    --code-keyword: #63d3e8;
    --code-string:  #7fe3c0;
    --code-number:  #e8b45c;
    --code-fn:      #d4dee1;
    --row-hover:   #1f2021;
    --nav-hover:   #242627;
    --grid-line:   rgba(119, 201, 212, .10);

    /* Status, kept clear of the brand on purpose. Success is --ok rather than
       --green, so "saved" reads as saved without borrowing the accent. */
    --ok:          #4fd39b;
    --ok-ink:      #6fe0b4;
    --notice-success-bg: #0e2a2172;
    --notice-error-bg:   #331a1772;
    --notice-error-ink:  #ffb3a6;
    --notice-info-bg:    #0e273172;
    --notice-warn-bg:    #2e250f72;
    --notice-warn-ink:   #f0cd8a;

    /* The payload bit map. One colour per parameter, held for as long as that
       parameter exists: a device's temperature is the same colour in every
       payload it appears in, and reordering the fields repaints nothing. The
       slot is the parameter's place in the device's own list, so it follows
       the entity rather than its rank in the message.

       Sixteen, because that is what the largest plan sells per device. They
       were generated in OKLCH inside the band this dark ground wants
       (L 0.48-0.67, C >= 0.1) and then searched for an order, rather than
       picked by eye - the first four are as far apart as the set allows, since
       most devices map two or three, and every touching pair clears the
       colour-blindness gate with room to spare (worst adjacent CVD dE 11.3
       against a target of 8; worst adjacent normal-vision dE 18.1).

       Sixteen categorical colours cannot all be far apart - the wheel is only
       so big at one lightness - so colour never carries identity alone here:
       every block is labelled with its parameter's name, and the table below
       repeats the name beside the same swatch. Three of the slots also sit
       just under 3:1 against the panel, which is the same obligation and the
       same answer. */
    --bit-1:  #aa3527; --bit-2:  #83419f; --bit-3:  #cc7200; --bit-4:  #09a0a0;
    --bit-5:  #565bc0; --bit-6:  #a83a71; --bit-7:  #30a556; --bit-8:  #0478a4;
    --bit-9:  #999000; --bit-10: #bf65b8; --bit-11: #537600; --bit-12: #847be5;
    --bit-13: #008a63; --bit-14: #0395db; --bit-15: #876200; --bit-16: #d95e6d;

    /* Bits nothing reads. Drawn, not left blank: a gap in a mapping is
       something to notice, and an empty space reads as the picture ending. */
    --bit-gap:         #131415; --bit-gap-ink:     #5d757e;
    --bit-byte-rule:   rgba(119, 201, 212, .34);
    --bit-bit-rule:    rgba(119, 201, 212, .10);
    /* Text on a block. The slots span a lightness band rather than sitting at
       one step, so one ink cannot serve them all; near-white with a shadow
       reads on every one of the sixteen. */
    --bit-ink:         #ffffff;

    --badge-bg:     #212223;
    --badge-green:  #113228; --badge-green-ink: #6fe0b4;
    --badge-blue:   #0f2c36; --badge-blue-ink:  #7ad9ec;
    --badge-red:    #331c19; --badge-red-ink:   #ffab9c;
    --badge-amber:  #2f2712; --badge-amber-ink: #eec983;

    --on-accent:        #03130d;
    --footer-bg:        #0b0c0d;
    --footer-ink:       #86a0a8;
    --footer-link-hover:#d8ecf1;
    --footer-dim:       #5d757e;

    /* Green to blue, the two brand colours. The hover pair is a clear step
       brighter rather than a shade: a button has to answer the pointer plainly
       enough to be read out of the corner of an eye. */
    --accent-grad:  linear-gradient(135deg, #4fd39b 0%, #63d3e8 100%);
    --btn-bg:       linear-gradient(135deg, #4fd39b 0%, #57c6e0 100%);
    --btn-bg-hover: linear-gradient(135deg, #7cebbd 0%, #86e2f5 100%);

    /* The glow under a lifted button, and the smaller one left while it is
       held down. */
    --btn-glow:     0 6px 18px rgba(79, 211, 155, .32);
    --btn-glow-held: 0 1px 5px rgba(79, 211, 155, .24);

    /* Movement answers the pointer at once where a colour this large is better
       arriving slowly, so the lift runs on its own, quicker clock. */
    --lift-ease:    .16s ease;

    /* Two soft washes of the brand colours, green on the left and blue on the
       right, so the background is not a flat slab. Faint on purpose: the
       ground is a neutral grey, with only a trace of the cool tint the text
       greys carry, and washes any stronger than this tint it blue-green again.
       Fixed, so they do not slide around while scrolling. */
    --page-glow:
        radial-gradient(1100px 620px at 12% -12%, rgba(79, 211, 155, .05), transparent 62%),
        radial-gradient(980px 540px at 92% -4%,  rgba(99, 211, 232, .04), transparent 58%);
    --panel-bg:         var(--panel);
    --panel-glow:       0 1px 2px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .04);
    --panel-hover-glow: 0 6px 22px rgba(79, 211, 155, .13);
    --focus-ring:       0 0 0 3px rgba(99, 211, 232, .28);
    --chart-fill:       rgba(79, 211, 155, .14);

    /* The chart library (js/sensichart) reads these off the element a chart
       is drawn in, since a canvas cannot use a variable itself. Aliases where
       a token already says it, so a chart changes when the theme does. The
       crosshair is the strongest ink at a third: a line to read against, not
       one that competes with the series under it. */
    --sc-text:        var(--muted);
    --sc-strong:      var(--ink-strong);
    --sc-grid:        var(--grid-line);
    --sc-axis:        var(--line);
    --sc-crosshair:   rgba(212, 222, 225, .35);
    --sc-select:      rgba(119, 201, 212, .22);
    --sc-select-edge: var(--blue);
    --sc-surface:     var(--panel);
    --sc-mask:        rgba(0, 0, 0, .45);
    --sc-font:        12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    --radius:     8px;
    --shadow:     0 1px 3px rgba(0, 0, 0, .5);

    /* Duration and easing for a control arriving at its hover state, and
       leaving it again. `ease` so it starts quickly and settles rather than
       running at a constant rate - which is what keeps a fade this long from
       reading as a lag: most of the colour has arrived early on, and the tail
       of it is what takes the time. */
    --hover-ease: .5s ease;

    color-scheme: dark;
}

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

body {
    margin: 0;
    background-color: var(--page);
    /* Fixed, so the washes stay put instead of sliding while scrolling. */
    background-image: var(--page-glow);
    background-attachment: fixed;
    background-repeat: no-repeat;
    color: var(--ink);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 16px;
    line-height: 1.55;
}

h1, h2, h3, .brand, .footer-brand {
    font-family: 'Jura', 'Segoe UI', sans-serif;
    font-weight: 600;
    color: var(--green-ink);
    line-height: 1.25;
}

h1 { font-size: 1.75rem; margin: 0 0 .5rem; }
h2 { font-size: 1.3rem;  margin: 2rem 0 .75rem; }
h3 { font-size: 1.05rem; margin: 1.5rem 0 .5rem; }

/* A level below h3, for a section inside a section. The same face, one step
   smaller, and in the text colour rather than the heading green, so it reads
   as belonging to the h3 above it rather than as a sibling of it. */
h4 {
    font-family: 'Jura', 'Segoe UI', sans-serif;
    font-weight: 600;
    font-size: .95rem;
    line-height: 1.25;
    color: var(--ink-strong);
    margin: 1.25rem 0 .4rem;
}

a { color: var(--ink-strong); }
a:hover { color: var(--ink-hover); }

code, pre, .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: .875em;
}

code {
    background: var(--code-bg);
    padding: .1em .35em;
    border-radius: 3px;
}

pre {
    background: var(--pre-bg);
    color: var(--pre-ink);
    padding: 1rem;
    border-radius: var(--radius);
    overflow-x: auto;
    line-height: 1.5;
}

pre code { background: none; padding: 0; color: inherit; }

/* --- the code example's corner button and line numbers ------------------
   The footer's script adds .code-box, and only then: without it a <pre> is
   exactly as above. The box stops scrolling and its <code> scrolls instead,
   so the copy button - pinned to the box - stays in the top right corner
   while a long line is scrolled underneath it. The right padding is room for
   the button, so the end of the widest line can be scrolled clear of it. */
pre.code-box {
    position: relative;
    padding: 0;
    overflow: hidden;
}

pre.code-box > code {
    display: block;
    padding: 1rem 3.25rem 1rem 1rem;
    overflow-x: auto;
}

/* No button, so no room kept for one. */
pre.code-box.no-copy > code { padding-right: 1rem; }

.code-copy {
    position: absolute;
    top: .55rem;
    right: .55rem;
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--pre-bg);
    color: var(--muted);
    cursor: pointer;
    user-select: none;
    transition: color .15s, border-color .15s;
}
.code-copy svg { width: 1rem; height: 1rem; }
.code-copy:hover { color: var(--ink-hover); border-color: var(--green); }
.code-copy:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.code-copy.copied { color: var(--green); border-color: var(--green); }

/* Numbers from a counter, in ::before: generated content is never copied or
   selected, so neither the button nor a reader's own selection picks them
   up. The gutter is as wide as the largest number, set by the script. */
code.numbered { counter-reset: line; }
code.numbered .line { counter-increment: line; }
code.numbered .line::before {
    content: counter(line);
    display: inline-block;
    width: calc(var(--line-digits, 1) * 1ch);
    margin-right: 1.5ch;
    text-align: right;
    color: var(--code-comment);
    opacity: .6;
    user-select: none;
    -webkit-user-select: none;
}

/* --- the right a request needs -----------------------------------------
   A pill per right, each in its own colour so a page of requests can be
   scanned for what a key must carry: read the calm blue, write the brand
   green, manage the amber that says it can change things. Any key and no key
   are neutral - they ask for nothing in particular. */
.right-badge {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    padding: .12rem .55rem .12rem .45rem;
    border: 1px solid transparent;
    border-radius: 999px;
    font-size: .75rem;
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: .02em;
    white-space: nowrap;
    background: var(--badge-bg);
    color: var(--muted);
    border-color: var(--line);
}
.right-badge svg { width: .9rem; height: .9rem; flex: none; }
.right-read   { background: var(--badge-blue);  color: var(--badge-blue-ink);  border-color: transparent; }
.right-write  { background: var(--badge-green); color: var(--badge-green-ink); border-color: transparent; }
.right-manage { background: var(--badge-amber); color: var(--badge-amber-ink); border-color: transparent; }

/* "Request" and its badge, together on the left of the strip's row. */
.request-meta {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin-bottom: .35rem;
}
.request-meta .example-heading { margin: 0; }
/* Above a tabbed request, where it has a row to itself. */
.request-meta + .codetabs { margin-top: .2rem; }

.visually-hidden,
/* The copy button's message, for a screen reader only. */
.sr-status {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

/* The tokens Sensifly\Highlight emits. Unscoped, because a highlighted block
   is a highlighted block wherever it is put - the class names are the contract
   between the lexer and this file, and a token it has no rule for is simply
   the code colour, which is what an unhighlighted example already looks
   like. */
.tok-comment { color: var(--code-comment); }
.tok-meta    { color: var(--code-meta); }
.tok-keyword { color: var(--code-keyword); }
.tok-string  { color: var(--code-string); }
.tok-number  { color: var(--code-number); }
.tok-fn      { color: var(--code-fn); }

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

.topbar {
    display: flex;
    align-items: center;
    gap: 1rem;
    background: var(--topbar);
    backdrop-filter: saturate(160%) blur(10px);
    -webkit-backdrop-filter: saturate(160%) blur(10px);
    /* The solid colour is the floor: the animated bar in .topbar::after lies
       over this border, so if the animation never runs the line is still
       there. See "the accent rail" below. */
    border-bottom: 3px solid var(--green);
    padding: .65rem 1.25rem;
    position: sticky;
    top: 0;
    z-index: 50;
    box-shadow: var(--shadow);
}

.brand {
    font-size: 1.3rem;
    letter-spacing: .22em;
    text-decoration: none;
    white-space: nowrap;
}

/* The hamburger, on small screens only. It and the nav each take the free
   space to their left, so whichever of the two is showing sits hard right. */
.menu-toggle {
    display: none;
    margin-left: auto;
    background: none;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font-size: 1.2rem;
    line-height: 1;
    padding: .4rem .6rem;
    cursor: pointer;
    color: var(--ink);
}

.mainnav { margin-left: auto; order: 3; }

.mainnav ul {
    display: flex;
    gap: .25rem;
    list-style: none;
    margin: 0;
    padding: 0;
    flex-wrap: wrap;
}

.mainnav a {
    display: block;
    padding: .45rem .75rem;
    border-radius: var(--radius);
    text-decoration: none;
    color: var(--muted);
    font-size: .93rem;
    white-space: nowrap;
}

.mainnav a:hover   { background: var(--nav-hover); color: var(--ink-hover); }
.mainnav a.active  { background: var(--accent-grad); color: var(--on-accent); }
.mainnav a.cta     { background: var(--accent-grad); color: var(--on-accent); }
.mainnav a.cta:hover { background: var(--btn-bg-hover); color: var(--on-accent); }

@media (max-width: 800px) {
    .menu-toggle { display: block; order: 3; }
    .mainnav {
        display: none;
        width: 100%;
        margin: .5rem 0 0;
        order: 4;
    }
    .mainnav.open { display: block; }
    .mainnav ul { flex-direction: column; }
    .mainnav a { padding: .6rem .5rem; }
    .topbar { flex-wrap: wrap; }
}

/* --- layout ------------------------------------------------------------- */

/* --content-top is the room between the top bar and the first thing on a
   page, the same on every page - so moving between them, nothing jumps -
   and where a side menu sits and sticks (see "a side menu"). --content-x is
   the padding at the sides, which a docked side menu reaches out over. */
.content {
    --content-top: 1.75rem;
    --content-x: 1.25rem;
    max-width: 1080px;
    margin: 0 auto;
    padding: var(--content-top) var(--content-x) 4rem;
}

.content.narrow { max-width: 480px; }

/* A tool rather than text (Page::setWide): the charts page, where a plot is
   better for every pixel of width and nothing on it is a line of prose. */
.content.wide {
    --content-x: 1rem;
    max-width: 1760px;
    /* Little padding at the sides and the foot of a workspace that is sized
       to the window: what the reading column spends on air, a chart spends
       on plot. Not at the top, which is every page's. */
    padding: var(--content-top) var(--content-x) 1rem;
}

/* A page with a side menu (Page::addSideMenu()) runs the whole width of the
   window, so that its menu can sit against the window's left edge - see "a
   side menu" - with nothing between the two however wide the window is. */
.content.with-side { max-width: none; }

.panel {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1.5rem;
    margin-bottom: 1.25rem;
    box-shadow: var(--panel-glow);
}

.panel > :first-child { margin-top: 0; }
.panel > :last-child  { margin-bottom: 0; }

/* --- contents ------------------------------------------------------------ */

/* A long page's contents - the documentation's, the downloads' - built by
   Page::contents() from its own headings: the menu down its left, laid out,
   docked and put away as the dashboards', the charts' and the devices' menus
   are (see "a side menu"), with the page's own column beside it
   (Page::withContents()). */

/* The page beside the contents keeps to a reading width - prose as wide as
   the window is unreadable - starting where every page beside a side menu
   starts, against the gutter. */
.contents-layout > .side-main {
    width: 100%;
    max-width: 1040px;
}

.toc-list,
.toc-list ol {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Each entry a row, as a dashboard or a device is in its menu: the whole row
   is the link, lit under the pointer. A section's own sub-sections hang under
   it on a hairline, as a device's parameters do under the device. */
.toc-list a {
    display: block;
    padding: .38rem .5rem .38rem .7rem;
    border-radius: var(--radius);
    color: var(--ink);
    font-size: .86rem;
    line-height: 1.35;
    text-decoration: none;
    /* "GET /{username}/{device}/{parameter}/latest" has nowhere to break. */
    overflow-wrap: anywhere;
}

.toc-list > li > a { font-weight: 600; }

.toc-list ol {
    margin: .1rem 0 .45rem .7rem;
    padding-left: .3rem;
    border-left: 1px solid var(--line);
}

.toc-list ol a {
    padding-top: .25rem;
    padding-bottom: .25rem;
    font-size: .8rem;
    color: var(--muted);
}

.toc-list a:hover {
    background: var(--input-bg);
    color: var(--ink-hover);
}

/* The section being read, marked as the open dashboard is in its menu: a
   step brighter, with a bar of the accent down its edge. */
.toc-list a.current {
    background: var(--input-bg);
    color: var(--ink-hover);
    box-shadow: inset 3px 0 0 var(--green);
}

.toc-list a:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.toc-list a.current:focus-visible { box-shadow: inset 3px 0 0 var(--green), var(--focus-ring); }

/* Folded to its heading below the width where the menu sits above the page,
   as the devices menu is - all of it at once is a long way to scroll before
   the page starts. Only with script, which is what unfolds it: without, the
   contents are simply there, and the button that would do nothing is not. */
.toc-fold {
    display: none;
    margin-left: auto;
}

@media (max-width: 900px) {
    .js .toc-fold { display: inline-block; }
    .js .toc:not(.is-unfolded) .toc-list { display: none; }
    .js .toc:not(.is-unfolded) .side-head { margin-bottom: 0; }
}

/* A payload's full address in the device page's list. A URL has no spaces to
   break at, and a table cell will not shrink below its longest word, so it
   is allowed to break anywhere rather than pushing the table off the screen. */
.payload-url { word-break: break-all; }

/* A jump to a heading would otherwise park it under the sticky top bar. */
h2[id], h3[id], h4[id], .panel[id] { scroll-margin-top: 5rem; }

.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

.lead { color: var(--muted); margin: 0 0 1.5rem; }

.grid {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}

/* --- the documentation page ---------------------------------------------- */

/* A panel there can hold half a dozen sub-sections - one per endpoint - and
   an h3 on its own was left doing all the work of saying where the previous
   one stopped. A hairline above it draws that boundary, which also tells an
   h3 apart from the h4s under it at a glance. Only direct children, so the
   h3 that labels a language block inside an example is untouched. */
.docs .panel > h3 {
    margin-top: 2.75rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--line);
}

/* An h3 straight under the panel's h2 would draw the same boundary twice, so
   there it keeps the plain spacing. Where intro prose comes between the two,
   the rule is wanted: it is what says the introduction has ended. */
.docs .panel > h2 + h3 {
    margin-top: 1.5rem;
    padding-top: 0;
    border-top: 0;
}

/* --- notices ------------------------------------------------------------ */

.notice {
    padding: .8rem 1rem;
    border-radius: var(--radius);
    margin-bottom: 1rem;
    border-left: 4px solid;
    background: var(--panel);
    border-color: var(--blue);
}

.notice.success { border-color: var(--ok); background: var(--notice-success-bg); }
.notice.error   { border-color: var(--danger); background: var(--notice-error-bg); color: var(--notice-error-ink); }
.notice.info    { border-color: var(--blue); background: var(--notice-info-bg); }
.notice.warning { border-color: var(--warn); background: var(--notice-warn-bg); color: var(--notice-warn-ink); }

/* --- forms -------------------------------------------------------------- */

.field { margin-bottom: 1rem; }

.field label {
    display: block;
    font-size: .875rem;
    font-weight: 600;
    margin-bottom: .3rem;
    color: var(--ink-strong);
}

.field .hint {
    display: block;
    font-size: .8rem;
    color: var(--muted);
    margin-top: .3rem;
}

/* A parameter's unit (lib/views/_measurement.php): a list of the units that
   go with its measurement, and a box for one the list does not have. The list
   needs the script to follow the measurement, so with none the box is the
   whole field; with one, the box shows only for "Other...". */
html:not(.js) .unit-choice { display: none; }
.js .unit-own.unit-listed { display: none; }
.js .unit-choice + .unit-own { margin-top: .4rem; }

input[type=text], input[type=email], input[type=password],
input[type=number], input[type=search], select, textarea {
    width: 100%;
    padding: .6rem .7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    font: inherit;
    font-size: .95rem;
    background: var(--input-bg);
    color: var(--ink);
}

input:focus, select:focus, textarea:focus {
    outline: none;
    border-color: var(--green);
    box-shadow: var(--focus-ring);
}

input.invalid, .field.invalid input, .field.invalid textarea { border-color: var(--danger); }

.field-error {
    display: block;
    color: var(--danger);
    font-size: .8rem;
    margin-top: .3rem;
}

.checkbox {
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    font-size: .9rem;
}

.checkbox input { width: auto; margin-top: .25rem; }

.form-row {
    display: grid;
    gap: 1rem;
    grid-template-columns: 1fr 1fr;
}

@media (max-width: 560px) {
    .form-row { grid-template-columns: 1fr; }
}

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

.btn {
    display: inline-block;
    padding: .6rem 1.1rem;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--btn-bg);
    color: var(--on-accent);
    font: inherit;
    font-size: .95rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    text-align: center;
    transition:
        color var(--hover-ease),
        transform var(--lift-ease),
        box-shadow var(--lift-ease);
}

.btn:hover { color: var(--on-accent); }

/* Definite on purpose. Three things move at once: the fill brightens, the
   button lifts off the page, and it takes a glow the colour of the accent
   underneath - so the answer reads whether the pointer is being watched or
   not. The fill fades on --hover-ease and the other two on --lift-ease,
   because a colour that size is better arriving slowly while movement is
   worse for any delay at all. */
.btn:hover,
.btn:focus-visible {
    transform: translateY(-2px);
    box-shadow: var(--btn-glow);
}

/* Back down under the press, so the click has somewhere to land. Quicker
   again: this one has to keep up with the finger. */
.btn:active {
    transform: translateY(0);
    box-shadow: var(--btn-glow-held);
    transition-duration: .05s;
}

/* Every button shades, and both of a button's states are gradients. Gradients
   do not interpolate - a transition between them simply lands on the second
   one, with nothing in between - so the hover state is laid over the base
   instead and the overlay's opacity is what fades.

   -1px reaches out over the border, which the background paints under, so the
   fade covers the whole button rather than stopping a hair inside it. The
   isolation is what keeps the negative z-index inside the button: behind the
   label, above the button's own background. */
.btn {
    position: relative;
    isolation: isolate;
}

.btn::after {
    content: "";
    position: absolute;
    inset: -1px;
    z-index: -1;
    border-radius: inherit;
    background: var(--btn-bg-hover);
    opacity: 0;
    transition: opacity var(--hover-ease);
}

.btn:hover::after,
.btn:focus-visible::after { opacity: 1; }

/* .secondary and .danger stay on the markup - they are what a reader of the
   template learns the intent from, and what a later change would style again -
   but neither takes a colour of its own. Every button is the one gradient. */

.btn.small { padding: .35rem .7rem; font-size: .85rem; }
.btn.block { display: block; width: 100%; }
.btn[disabled], .btn.disabled { opacity: .5; pointer-events: none; }

/* The display above outranks the browser's own [hidden] { display: none }, and
   several buttons are rendered hidden and shown by script - the row's Delete,
   which needs a dialog that only script can open, and New group, which has
   nothing to post without one. */
.btn[hidden] { display: none; }

/* The chips and tabs. Unlike .btn these stay flat until they are the chosen
   one, and a chosen chip takes the accent gradient - so a plain transition is
   all their states need.

   Deliberately named properties rather than `all`: transitioning everything
   catches layout properties too, and a control that eases its own width while
   the pointer crosses it is worse than one that snaps. */
.range,
.mainnav a,
.menu-toggle,
.sortable th .sort,
.device-group > summary,
.format-switch .format-btn,
.codetabs-bar .codetab-btn {
    transition:
        background-color var(--hover-ease),
        border-color var(--hover-ease),
        color var(--hover-ease),
        opacity var(--hover-ease),
        box-shadow var(--hover-ease);
}

/* A button repeated down a table is not a call to action - there is one per
   row, and forty filled pills is a page shouting the same word forty times. So
   in a table they are outlines, in the same ink and on the same hairline as the
   inputs and the row pills beside them, and the fill arrives only under the
   pointer. The gradient is the one every other button uses, fading in on the
   ::after overlay that is already there.

   No lift, either. A button that rises 2px is fine on its own at the foot of a
   form; in a dense table it is a row that jumps as the pointer crosses it, and
   with one in every row the whole list twitches. The colour does the answering
   instead. Buttons outside a table keep the lift. */
.data .btn {
    transition:
        color var(--hover-ease),
        background-color var(--hover-ease),
        border-color var(--hover-ease);
    background: var(--input-bg);
    border-color: var(--line);
    color: var(--ink-strong);
    font-weight: 500;
}

.data .btn:hover,
.data .btn:focus-visible {
    transform: none;
    box-shadow: none;
    border-color: transparent;
    color: var(--on-accent);
}

.data .btn:active {
    transform: none;
    box-shadow: none;
}

/* Revoke is the one that has to keep saying what it is: it is the only button
   in a table that cannot be undone, so it wears the danger colour cold rather
   than waiting to be pointed at, and fills with it rather than with the accent. */
.data .btn.danger {
    border-color: var(--danger);
    color: var(--danger);
}

.data .btn.danger:hover,
.data .btn.danger:focus-visible {
    background: var(--danger);
    border-color: var(--danger);
    color: var(--on-accent);
}

/* ...which means the accent overlay must not paint over it. */
.data .btn.danger::after { display: none; }

/* The grip fades in with the row it sits on. */
.drag-grip { transition: opacity var(--hover-ease); }

.btn-row {
    display: flex;
    gap: .5rem;
    flex-wrap: wrap;
    align-items: center;
}

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

.table-wrap { overflow-x: auto; }

/* The documentation's credential loop. Drawn in SVG so it can take its
   colours from the tokens; scrolls sideways on a phone rather than shrinking
   its labels past reading. Each credential keeps one colour throughout: the
   key green, the access token blue, the refresh token amber. */
.flow-wrap { overflow-x: auto; margin: 1rem 0; }
.flow { display: block; width: 100%; min-width: 560px; max-width: 760px; }
.flow-box { fill: var(--code-bg); stroke: var(--line); stroke-width: 1.5; }
.flow-box.key     { stroke: var(--green); }
.flow-box.access  { stroke: var(--blue); }
.flow-box.refresh { stroke: var(--warn); }
.flow-box.bad     { stroke: var(--danger); }
.flow-title { fill: var(--ink-strong); font-size: 15px; font-weight: 600; }
.flow-sub   { fill: var(--muted); font-size: 12px; }
.flow-note  { fill: var(--muted); font-size: 12px; }
.flow-line  { fill: none; stroke: var(--muted); stroke-width: 1.5; marker-end: url(#flow-arrow); }
.flow-head  { fill: var(--muted); }
.flow-line.retry { stroke-dasharray: 5 4; }
.flow-line.bad   { stroke: var(--danger); marker-end: url(#flow-arrow-bad); }
.flow-head.bad   { fill: var(--danger); }
.flow-note.access  { fill: var(--blue); }
.flow-note.refresh { fill: var(--warn); }
.flow-note.bad, .flow-sub.bad { fill: var(--danger); }

/* The sequence: two parties with a lifeline each, requests as solid arrows
   and responses dashed, in phases divided by a rule. A credential named in a
   label is coloured as it is everywhere else: key green, access token blue,
   refresh token amber. */
.flow-actor    { fill: var(--code-bg); stroke: var(--muted); stroke-width: 1.5; }
.flow-lifeline { fill: none; stroke: var(--line); stroke-width: 1.5; stroke-dasharray: 3 5; }
.flow-divider  { fill: none; stroke: var(--line); stroke-width: 1; }
.flow-phase    { fill: var(--ink-strong); font-size: 12px; font-weight: 600; letter-spacing: .02em; }
.flow-aside    { fill: var(--muted); font-size: 11px; font-style: italic; }
.flow-line.reply { stroke-dasharray: 5 4; }
.flow tspan.key     { fill: var(--green); }
.flow tspan.access  { fill: var(--blue); }
.flow tspan.refresh { fill: var(--warn); }

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

table.data th, table.data td {
    text-align: left;
    padding: .6rem .75rem;
    border-bottom: 1px solid var(--line);
    vertical-align: middle;
}

table.data th {
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--muted);
    font-weight: 600;
    white-space: nowrap;
}

table.data tbody tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--row-hover); }
table.data td.numeric, table.data th.numeric { text-align: right; font-variant-numeric: tabular-nums; }

/* The bare .right utility loses to `table.data td`, which is the more specific
   selector, so an action cell asking for it stayed left. Repeat it at the
   table's own weight rather than reaching for !important. */
table.data td.right, table.data th.right { text-align: right; }

/* --- stat tiles --------------------------------------------------------- */

.stat {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1.1rem 1.25rem;
    box-shadow: var(--panel-glow);
}

.stat .stat-label {
    font-size: .78rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--muted);
    margin-bottom: .3rem;
}

.stat .stat-value {
    font-family: 'Jura', sans-serif;
    font-size: 1.9rem;
    font-weight: 600;
    color: var(--green);
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.stat .stat-value small { font-size: .95rem; color: var(--muted); font-weight: 400; }
.stat .stat-note { font-size: .8rem; color: var(--muted); margin-top: .3rem; }

/* --- sensor cards ------------------------------------------------------- */

.sensor-card {
    display: block;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1.1rem 1.25rem;
    text-decoration: none;
    color: inherit;
    box-shadow: var(--panel-glow);
    transition: border-color .15s, transform .15s, box-shadow .15s;
}

.sensor-card:hover {
    border-color: var(--green);
    transform: translateY(-1px);
    box-shadow: var(--panel-hover-glow);
    color: inherit;
}

.sensor-card .sensor-name {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: 1.05rem;
    color: var(--green-ink);
    margin-bottom: .1rem;
}

.sensor-card .sensor-slug {
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: .78rem;
    color: var(--muted);
    margin-bottom: .75rem;
}

.sensor-card .sensor-reading {
    font-family: 'Jura', sans-serif;
    font-size: 2rem;
    font-weight: 600;
    color: var(--green);
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

.sensor-card .sensor-reading .unit { font-size: 1rem; color: var(--muted); }
.sensor-card .sensor-when { font-size: .8rem; color: var(--muted); margin-top: .25rem; }

/* --- badges ------------------------------------------------------------- */

.badge {
    display: inline-block;
    padding: .15rem .5rem;
    border-radius: 999px;
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .03em;
    text-transform: uppercase;
    background: var(--badge-bg);
    color: var(--muted);
    white-space: nowrap;
}

.badge.green  { background: var(--badge-green); color: var(--badge-green-ink); }
.badge.blue   { background: var(--badge-blue); color: var(--badge-blue-ink); }
.badge.grey   { background: var(--badge-bg); color: var(--muted); }
.badge.red    { background: var(--badge-red); color: var(--badge-red-ink); }
.badge.amber  { background: var(--badge-amber); color: var(--badge-amber-ink); }

/* A badge whose text is data rather than a label - an id, a slug - so it keeps
   the case and the face it was written in. Every other badge says a word about
   a thing and reads better shouted; `loft-01` shown as LOFT-01 is not the id
   any more, and it is the one thing on the row a reader may retype. */
.badge.mono {
    text-transform: none;
    letter-spacing: 0;
    font-weight: 500;
}

/* --- secret reveal ------------------------------------------------------ */

.secret {
    background: var(--pre-bg);
    color: var(--secret-ink);
    padding: .9rem 1rem;
    border-radius: var(--radius);
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: .85rem;
    word-break: break-all;
    margin: .75rem 0;
}

.copy-row {
    display: flex;
    gap: .5rem;
    align-items: stretch;
    flex-wrap: wrap;
}

.copy-row input {
    flex: 1 1 260px;
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: .82rem;
}

/* --- charts ------------------------------------------------------------- */

.chart-box {
    position: relative;
    height: 380px;
    width: 100%;
}

.chart-box.short { height: 220px; }

/* The chart inside a box that also holds other things - the parameter page's
   box is the focus target for its arrow keys - fills it exactly. */
.chart-host {
    position: absolute;
    inset: 0;
}

.chart-controls {
    display: flex;
    gap: .4rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
    align-items: center;
}

/* The small pill button. It began life beside a chart, but the same shape is
   wanted anywhere a control sits next to something rather than under it - the
   devices list collapses its groups with a pair - so it is a class in its
   own right rather than one nested
   under .chart-controls.

   Deliberately no display of its own: several of these are toggled with the
   hidden attribute, which a display declaration here would quietly defeat. */
.range {
    padding: .3rem .7rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--input-bg);
    font-size: .82rem;
    cursor: pointer;
    color: var(--muted);
}

.range:hover {
    border-color: var(--green);
    color: var(--ink-hover);
}

.range:focus-visible {
    outline: none;
    border-color: var(--green);
    box-shadow: var(--focus-ring);
}

.range.active {
    background: var(--accent-grad);
    border-color: transparent;
    color: var(--on-accent);
}

/* A pill that is an icon - the charts page's zoom - as tall as one with a
   word in it beside it: the glyph the height of the word's line. Its own
   display, so its own [hidden] guard. */
.range-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: .3rem .55rem;
}

.range-icon svg {
    display: block;
    width: 1.2em;
    height: 1.2em;
}

.range-icon[hidden] { display: none; }

/* Nothing to do yet - zoomed all the way out, or nothing on the chart - so
   greyed, and not lit by the pointer as if it would do something. */
.range-icon:disabled,
.range-icon:disabled:hover {
    opacity: .4;
    border-color: var(--line);
    color: var(--muted);
    cursor: default;
}

/* The zoom buttons, kept together as one control. */
.chart-zoom {
    display: inline-flex;
    gap: .25rem;
}

/* The explicit from/to row, revealed by the "Dates..." button.
   The [hidden] guard is not optional: a display declaration in a class rule
   outranks the browser's own [hidden] { display: none }, so without it the
   row would be visible from the start. */
.chart-range[hidden] { display: none; }

.chart-range {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
    font-size: .85rem;
}

.chart-range label { color: var(--muted); }

.chart-range input[type="date"] {
    width: auto;
    padding: .3rem .5rem;
    font-size: .82rem;
}

.chart-range #range-error { color: var(--danger); }

/* Expanded: the chart panel becomes the window. Fixed rather than the
   Fullscreen API, so it keeps the page's own theme and chrome instead of
   dropping onto the browser's black backdrop, and so Escape and the toggle
   behave the same way everywhere. */
.chart-panel.expanded {
    position: fixed;
    inset: 0;
    z-index: 200;
    margin: 0;
    border-radius: 0;
    display: flex;
    flex-direction: column;
    /* The panel itself never scrolls. Anything that has to give way is either
       hidden below or capped and scrolled in its own right, so the chart is
       always the thing filling what is left. */
    overflow: hidden;
    padding: .85rem 1rem 1rem;
}

/* A chart takes whatever height its box is given. Every wrapper between the
   panel and that box has to grow too,
   or the height stops at the first one that does not - which is what leaves a
   full-window panel with a 380px chart and a field of empty space under it. */
.chart-panel.expanded .chart-box {
    flex: 1 1 auto;
    min-height: 0;
}

.chart-panel.expanded .chart-box {
    height: auto;
    /* Below this the plot is not worth looking at; the panel scrolls instead,
       which is the right failure for two charts on a short window. */
    min-height: 180px;
}

/* The explanatory prose has been read by the time anyone expands the panel,
   and on the comparison panel it is worth about a third of the window. The
   heading stays - a panel covering the whole screen should still say what it
   is - but it stops taking a heading's worth of margin with it. */
.chart-panel.expanded .chart-note { display: none; }

.chart-panel.expanded > h2 {
    font-size: 1rem;
    margin: 0 0 .5rem;
}

.chart-panel.expanded .chart-controls { margin-bottom: .6rem; }

/* Nothing behind the panel should scroll while it is covering the page. */
body.chart-expanded {
    overflow: hidden;
}

.chart-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--muted);
    font-size: .95rem;
    text-align: center;
    padding: 1rem;
}

/* The guard is not optional, and it goes after the rule it guards rather than
   before it: the display above outranks the browser's own
   [hidden] { display: none }, and only source order settles it for readers
   that weigh an attribute selector no higher than a class. Without this the
   devices list announces "Nothing matches that search" before anything has
   been searched for. */
.chart-empty[hidden] { display: none; }

/* --- adverts ------------------------------------------------------------ */

/* An advert on a public page (Sensifly\Ads). Labelled as one, and given room
   before it loads, so the page does not jump when it fills in. What is drawn
   inside is Google's. */
.ad-slot {
    margin: 1.5rem 0;
    min-height: 120px;
}
.ad-label {
    display: block;
    margin-bottom: .35rem;
    color: var(--muted);
    font-size: .72rem;
    letter-spacing: .06em;
    text-transform: uppercase;
}

/* --- cookie notice ------------------------------------------------------ */

/* Along the bottom rather than across the middle: the notice is an
   acknowledgement, so it should be readable without being in the way of the
   page it is describing. */
.cookie-note {
    position: fixed;
    left: 1rem;
    right: 1rem;
    bottom: 1rem;
    z-index: 300;
    max-width: 44rem;
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: .85rem 1rem;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
    font-size: .85rem;
}

.cookie-note[hidden] { display: none; }

.cookie-note p {
    margin: 0;
    flex: 1 1 18rem;
    color: var(--muted);
}

/* Clear of the expanded chart, which covers the window. */
body.chart-expanded .cookie-note { display: none; }

/* --- the dashboard ------------------------------------------------------ */

/* Pages of sections of tiles the account arranges itself. The layout lives
   in JavaScript and is written onto each section's grid as `grid-column` and
   `grid-row` spans, so this file describes what things look like rather than
   where they are.

   A tile carries three colours as custom properties, set from its own colour
   or its section's: --tile-color for what is drawn, --tile-ink for numbers
   (the same colour unless it is too dark to read on the panel, when it is the
   body ink), and --tile-soft, a wash of it for fills. */

/* --- a side menu, beside a page --------------------------------------------- */

/* The dashboards, the charts, the devices and a long page's contents each
   keep a menu down the left: a panel of its own, apart from the page it is
   the menu for, sticking under the top bar and scrolling by itself when it
   is longer than the window.

   Between the two runs a gutter holding the one control that puts the menu
   away - it slides off to the left and the page takes the width - and brings
   it back. The same on every page with one, so it is learned once; the
   script is Page::addSideMenu()'s, and remembers the choice per page. */
.side-layout {
    --side-width: 16rem;
    --side-gutter: 2.25rem;
    display: grid;
    grid-template-columns: var(--side-width) var(--side-gutter) minmax(0, 1fr);
    align-items: start;
}

.side-layout[hidden] { display: none; }
.side-layout.side-hidden { grid-template-columns: 0 var(--side-gutter) minmax(0, 1fr); }

/* Its own width, whatever its column is doing: put away, the column closes
   to nothing while the panel slides out past the left edge whole, rather
   than being squeezed narrower on its way. */
/* It sticks where it starts - the top bar's height and the page's top
   padding down - so it is in the same place on every page and does not
   shift as the page scrolls. The bar is as tall as its content, so its
   height is measured into --topbar-h (Page::addSideMenu()'s script). */
.side-panel {
    position: sticky;
    top: calc(var(--topbar-h, 3.85rem) + var(--content-top, 1.75rem));
    width: var(--side-width);
    max-height: calc(100vh - var(--topbar-h, 3.85rem) - var(--content-top, 1.75rem) - 1rem);
    overflow-y: auto;
    padding: .9rem .7rem .8rem;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--panel-glow);
}

/* Faded as it goes, and out of the tab order once it has: visibility holds
   off until the slide has finished on the way out, and is back at once on
   the way in. */
.side-layout.side-hidden .side-panel {
    transform: translateX(calc(-100% - 2rem));
    opacity: 0;
    visibility: hidden;
}

.side-main { min-width: 0; }

/* Moving only once the page is drawn, so a menu put away last time is away
   from the first frame rather than sliding out as the page arrives. */
.side-layout.is-ready { transition: grid-template-columns .3s ease; }

.side-layout.is-ready .side-panel {
    transition: transform .3s ease, opacity .3s ease, visibility .3s;
}

/* The full height of the row, so the tab in it can stick under the top bar
   all the way down a long page. */
.side-gutter { align-self: stretch; }

/* A tab on the menu's right edge, sideways: the panel's own colour, flat
   against the menu and rounded on the outside, so it reads as part of the
   menu rather than as a button between two panels. It overlaps the menu's
   border by a pixel, so where the two meet there is one edge, not two.

   "Hide" while the menu is out; the menu's name while it is away. */
.side-toggle {
    position: sticky;
    /* A margin's worth below the menu's top, as it is drawn. */
    top: calc(var(--topbar-h, 3.85rem) + var(--content-top, 1.75rem) + 1rem);
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .45rem;
    margin: 1rem 0 0 -1px;
    padding: .65rem .3rem .75rem;
    border: 1px solid var(--line);
    border-left: 0;
    border-radius: 0 var(--radius) var(--radius) 0;
    background: var(--panel);
    color: var(--muted);
    font: inherit;
    font-size: .76rem;
    letter-spacing: .06em;
    cursor: pointer;
}

/* Read upwards, as a book's spine is. */
.side-toggle-label {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    white-space: nowrap;
}

.side-toggle svg {
    flex: none;
    width: .8rem;
    height: .8rem;
    transition: transform .3s ease;
}

/* Pointing the way the menu will go: left to put it away, right to bring it
   back. */
.side-layout.side-hidden .side-toggle svg { transform: rotate(180deg); }

/* With the menu away the tab is left against the edge of the window, flat to
   it as it was to the menu. */
.side-layout.side-hidden .side-toggle { margin-left: 0; }

/* Docked: the menu against the window's left edge, with nothing between -
   the layout reaches out over the page's padding on that side, and the menu
   has no border or rounded corners where it meets the edge. Only while the
   two sit side by side; stacked, the menu is a panel like any other. */
@media (min-width: 901px) {
    .with-side .side-layout { margin-left: calc(-1 * var(--content-x, 1rem)); }

    .with-side .side-panel {
        border-left: 0;
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
    }
}

.side-toggle:hover {
    border-color: var(--green);
    color: var(--ink-hover);
}

.side-toggle:focus-visible {
    outline: none;
    border-color: var(--green);
    color: var(--ink-hover);
    box-shadow: var(--focus-ring);
}

/* The heading at the top of a side menu, and of each part of one. */
.side-head {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    margin-bottom: .65rem;
    padding: 0 .3rem;
}

.side-title {
    margin: 0;
    font-size: .92rem;
}

.side-panel input[type="search"] {
    margin-bottom: .6rem;
    padding: .4rem .6rem;
    font-size: .85rem;
}

@media (prefers-reduced-motion: reduce) {
    .side-layout.is-ready,
    .side-layout.is-ready .side-panel,
    .side-toggle svg { transition: none; }
}

/* Too narrow to sit side by side: the menu above the page, and nothing to
   put away - each page folds its menu its own way down there. */
@media (max-width: 900px) {
    .side-layout,
    .side-layout.side-hidden {
        grid-template-columns: minmax(0, 1fr);
        row-gap: 1rem;
    }
    .side-gutter { display: none; }
    .side-panel,
    .side-layout.side-hidden .side-panel {
        position: static;
        width: auto;
        max-height: none;
        overflow: visible;
        transform: none;
        opacity: 1;
        visibility: visible;
    }
}

/* --- a list of named things in a side menu --------------------------------- */

/* The dashboards, and the saved charts: rows of names to open, drag into
   order, and rename or delete from a ... menu (js/sidelist.js, which draws
   both, so they cannot come to differ). */

.side-list {
    list-style: none;
    margin: 0 0 .75rem;
    padding: 0;
}

/* A row is the grip, the link and its ... button. The row rather than the
   link takes the hover and the mark for the open one, so the button reads as
   part of the dashboard it belongs to. The open one is marked as the page's
   contents mark where the reader is: a step brighter, and a bar of the accent
   down its edge.

   No text selection: the whole row is a drag handle under a mouse, and a
   drag that selects the names it passes over looks like a different gesture. */
.side-item {
    position: relative;
    display: flex;
    align-items: center;
    margin-bottom: .15rem;
    border-radius: var(--radius);
    -webkit-user-select: none;
    user-select: none;
}

.side-item:hover,
.side-item:focus-within { background: var(--input-bg); }

.side-item.is-current {
    background: var(--input-bg);
    box-shadow: inset 3px 0 0 var(--green);
}

/* In the link's left padding, out of the name's way. Faint until the row is
   pointed at, as the devices list's is - there, but not a row of dots down
   the menu competing with the names. */
.side-item-grip {
    position: absolute;
    left: .45rem;
    top: 50%;
    margin-top: -7.5px;
    touch-action: none;
}

.side-item:hover .side-item-grip { opacity: .85; }

/* Under a finger the grip is the only handle, so it answers to more than the
   dots: an invisible margin round them, which is still the grip to a press. */
.side-item-grip::before {
    content: "";
    position: absolute;
    inset: -12px -7px;
}

.side-item-link {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: .5rem;
    padding: .45rem .35rem .45rem 1.35rem;
    border-radius: var(--radius);
    color: var(--ink);
    text-decoration: none;
    -webkit-user-drag: none;
}

.side-item:hover .side-item-link { color: var(--ink-hover); }

.side-item-link:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.side-item.is-current .side-item-link {
    color: var(--ink-hover);
    font-weight: 600;
}

.side-item-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.side-item-count {
    flex: none;
    font-size: .78rem;
    font-weight: 400;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* Always there on the open dashboard and under a finger; on the others when
   the row is pointed at or tabbed into. Transparent rather than removed when
   it is not shown, so the names do not shift when it is. */
.side-item-more {
    flex: none;
    display: grid;
    place-items: center;
    width: 1.65rem;
    height: 1.65rem;
    margin-right: .25rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 6px;
    background: none;
    color: var(--muted);
    cursor: pointer;
    opacity: 0;
}

.side-item-more svg {
    width: 1rem;
    height: 1rem;
    fill: currentColor;
}

.side-item:hover .side-item-more,
.side-item:focus-within .side-item-more,
.side-item.is-current .side-item-more,
.side-item-more[aria-expanded="true"] { opacity: 1; }

.side-item-more:hover,
.side-item-more[aria-expanded="true"] {
    border-color: var(--line);
    background: var(--panel);
    color: var(--ink-hover);
}

.side-item-more:focus-visible {
    outline: none;
    opacity: 1;
    box-shadow: var(--focus-ring);
}

@media (hover: none) {
    .side-item-more { opacity: 1; }
}

/* Being dragged: lifted off the list, with the accent round it. The others
   close up behind it as it goes. */
.side-list.is-sorting,
.side-list.is-sorting .side-item-link { cursor: grabbing; }

.side-item.is-dragging {
    z-index: 1;
    background: var(--panel);
    box-shadow: 0 0 0 1px var(--green), 0 6px 18px rgba(0, 0, 0, .45);
}

.side-item.is-dragging .side-item-grip { opacity: 1; }
.side-item.is-dragging .side-item-more { opacity: 0; }

/* A name being typed, in the dashboard's own place or at the foot of the
   list, with the address it will have underneath. */
.side-item.is-naming {
    display: block;
    padding: .3rem .3rem .45rem;
    background: var(--input-bg);
    box-shadow: inset 3px 0 0 var(--green);
}

.side-item-input[type=text] {
    padding: .35rem .5rem;
    font-size: .9rem;
}

.side-item-hint {
    display: block;
    margin-top: .3rem;
    padding: 0 .2rem;
    font-size: .74rem;
    color: var(--muted);
    overflow-wrap: anywhere;
}

.side-new { margin-left: .2rem; }

/* At the limit. Still pointable, so its title can say which limit. */
.side-new[disabled],
.side-new[disabled]:hover {
    opacity: .5;
    border-color: var(--line);
    color: var(--muted);
    cursor: default;
}

/* --- a dashboard's options ---------------------------------------------- */

/* Fixed to the window by the script, under the ... button it opened from. */
.side-options {
    position: fixed;
    z-index: 60;
    min-width: 10.5rem;
    max-width: 17rem;
    padding: .3rem;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, .5);
}

.side-option {
    display: block;
    width: 100%;
    padding: .45rem .65rem;
    border: 0;
    border-radius: 6px;
    background: none;
    font: inherit;
    font-size: .88rem;
    text-align: left;
    color: var(--ink);
    cursor: pointer;
}

.side-option:hover,
.side-option:focus-visible {
    outline: none;
    background: var(--input-bg);
    color: var(--ink-hover);
}

.side-option.is-danger { color: var(--danger); }

.side-option[disabled],
.side-option[disabled]:hover {
    background: none;
    color: var(--muted);
    opacity: .55;
    cursor: default;
}

.side-option-rule {
    height: 1px;
    margin: .3rem .25rem;
    background: var(--line);
}

/* Delete, asked about in the same box. */
.side-options-text {
    margin: .35rem .45rem .3rem;
    font-size: .9rem;
    color: var(--ink-strong);
    overflow-wrap: anywhere;
}

.side-options-note {
    margin: 0 .45rem .7rem;
    font-size: .78rem;
    color: var(--muted);
}

.side-options-actions {
    display: flex;
    justify-content: flex-end;
    gap: .35rem;
    padding: 0 .25rem .25rem;
}

.range.is-danger {
    border-color: var(--danger);
    color: var(--danger);
}

.range.is-danger:hover,
.range.is-danger:focus-visible {
    outline: none;
    background: var(--danger);
    border-color: var(--danger);
    color: var(--on-accent);
}

/* --- the open dashboard ------------------------------------------------- */

.dash-head {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
    margin-bottom: .85rem;
}

.dash-title {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
}

.dash-tools {
    margin-left: auto;
    display: flex;
    gap: .35rem;
}

/* Too narrow for two columns: the dashboards become a row of chips above
   the open one, as wide as their names. */
@media (max-width: 900px) {
    .side-list { display: flex; flex-wrap: wrap; gap: .35rem; }
    .side-item {
        margin: 0;
        border: 1px solid var(--line);
        border-radius: 999px;
    }
    .side-item.is-current { border-color: var(--green); box-shadow: none; }
    .side-item-grip { display: none; }
    .side-item-link {
        flex: none;
        max-width: 12rem;
        padding: .3rem .75rem;
    }
    /* The ... only on the open chip, or one being tabbed through: on all of
       them a short row of names becomes a row of buttons. */
    .side-item.is-current .side-item-link { padding-right: .25rem; }
    .side-item-more { width: 1.45rem; height: 1.45rem; }
    .side-item:not(.is-current):not(:focus-within) .side-item-more:not([aria-expanded="true"]) { display: none; }
    .side-item.is-naming { flex-basis: 100%; border-radius: var(--radius); box-shadow: none; }
    .side-new { margin: 0; }
}

.dash-empty { padding: 1.5rem 0; }
.dash-add   { margin-top: .7rem; }

/* --- a section ---------------------------------------------------------- */

.dash-section { margin-bottom: 1.4rem; }
.dash-section:last-of-type { margin-bottom: 0; }

/* The rule under the heading fades out from the section's colour, so the
   colour says which band this is without a slab of it across the page. */
.dash-section-head {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex-wrap: wrap;
    margin: 0 0 .6rem;
    padding-bottom: .3rem;
    border-bottom: 1px solid;
    border-image: linear-gradient(90deg, var(--section-color, var(--green)), var(--line) 45%, transparent) 1;
}

.dash-section.is-collapsed .dash-section-head { margin-bottom: 0; }

.dash-section-title {
    margin: 0;
    font-size: 1rem;
    line-height: 1.3;
}

.dash-section-toggle {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .2rem .45rem .2rem .15rem;
    border: 0;
    border-radius: 4px;
    background: none;
    font: inherit;
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: 1rem;
    color: var(--ink-strong);
    cursor: pointer;
}

.dash-section-toggle:hover { color: var(--ink-hover); background: var(--row-hover); }

.dash-chevron {
    width: 1rem;
    height: 1rem;
    fill: none;
    stroke: var(--muted);
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform .2s ease;
}

.dash-section.is-collapsed .dash-chevron { transform: rotate(-90deg); }

.dash-section-swatch {
    flex: none;
    width: .7rem;
    height: .7rem;
    border-radius: 3px;
    background: var(--section-color, var(--green));
}

.dash-section-name.is-untitled {
    color: var(--muted);
    font-style: italic;
    font-weight: 400;
}

.dash-section-count { margin-right: auto; }

.dash-section-tools {
    display: flex;
    align-items: center;
    gap: .25rem;
}

/* The grid. minmax(0, 1fr) rather than 1fr, so no tile's content can widen
   its column: the layout says how wide a tile is, not what is in it. */
.dash-grid {
    display: grid;
    grid-template-columns: repeat(var(--dash-columns, 24), minmax(0, 1fr));
    grid-auto-rows: var(--dash-row, 32px);
    gap: var(--dash-gap, 10px);
    align-content: start;
}

/* --- a tile ------------------------------------------------------------- */

/* A size container, so what is inside can be sized by the tile rather than
   by the window: a value tile made big draws a big number. */
.dash-tile {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    min-width: 0;
    overflow: hidden;
    padding: .65rem .8rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    box-shadow: var(--panel-glow);
    text-decoration: none;
    color: inherit;
    container-type: size;
    /* Transform is not in the list: a dragged tile is repositioned by the grid
       rather than moved, and easing that would have it drifting after the
       pointer instead of under it. */
    transition:
        border-color var(--hover-ease),
        box-shadow var(--hover-ease);
}

/* Nothing in a tile gives way except what is meant to: the heading, the
   number and the lines of figures keep their height, and only the parts
   declared below as flex: 1 1 auto - the chart, the gauge, the big number -
   shrink when the tile is short. Otherwise a short tile squeezes its number
   to a sliver rather than its chart. */
.dash-tile > * { flex-shrink: 0; }

/* The tile's colour as a line along its top: every tile wears it, including
   the ones whose picture is only a number. */
.dash-tile::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 2px;
    background: var(--tile-color, var(--green));
    opacity: .8;
}

a.dash-tile:hover {
    border-color: var(--tile-color, var(--green));
    box-shadow: var(--panel-hover-glow);
    color: inherit;
}

.dash-tile.is-paused { opacity: .62; }

.dash-tile-head {
    flex: none;
    display: flex;
    align-items: flex-start;
    gap: .5rem;
}

.dash-tile-titles {
    flex: 1 1 auto;
    min-width: 0;
}

.dash-tile-name {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: .95rem;
    line-height: 1.2;
    color: var(--ink-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-tile-device {
    font-size: .72rem;
    color: var(--muted);
    margin-bottom: .2rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-tile-aside {
    flex: none;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: .2rem;
}

.dash-tile-badge {
    padding: .05rem .45rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: .64rem;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}

.dash-tile-value,
.dash-tile-now {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
    color: var(--tile-ink, var(--green));
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-tile-value { font-size: 1.7rem; }
.dash-tile-now   { font-size: .95rem; }
.dash-tile-value.is-compact { font-size: 1.25rem; }

/* A reading outside the expected range. The one red on the dashboard, and
   only ever on the number - the arc or bar beside it stays the tile's own. */
.dash-tile-value.is-out { color: var(--danger); }

.dash-tile-value .unit,
.dash-tile-now .unit,
.dash-tile-foot .unit,
.dash-recent .unit {
    margin-left: .2rem;
    font-size: max(.72rem, .5em);
    font-weight: 500;
    color: var(--muted);
}

/* The value tile: the number fills what the heading leaves, sized off the
   tile in both directions so it neither overflows a short wide tile nor
   rattles about in a tall narrow one. */
.dash-big {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
}

.dash-big .dash-tile-value { font-size: clamp(1.5rem, min(38cqh, 17cqw), 6rem); }

.dash-tile-note {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: .25rem;
    text-align: center;
    line-height: 1.4;
    color: var(--muted);
}

.dash-tile-foot {
    flex: none;
    margin-top: auto;
    padding-top: .3rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Only shown on a tile whose stretch of time has stopped moving, so it costs
   nothing on a live one. */
.dash-tile-when {
    flex: none;
    margin-top: .2rem;
    font-size: .68rem;
    text-align: right;
}

.dash-tile-when[hidden] { display: none; }

/* The end labels under a scale: low on the left, high on the right. */
.dash-ends {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    font-size: .68rem;
    font-variant-numeric: tabular-nums;
}

/* --- charts in a tile --------------------------------------------------- */

/* The chart fills whatever the tile has left, so a taller tile draws a taller
   chart rather than the same one with space under it. */
.dash-chart-box {
    flex: 1 1 auto;
    position: relative;
    display: flex;
    min-height: 34px;
    margin-top: .35rem;
}

.dash-chart {
    position: absolute;
    inset: 0;
}

/* --- the gauge, dial and ring ------------------------------------------- */

.dash-gauge {
    position: relative;
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 60px;
    margin-top: .2rem;
}

.dash-gauge svg {
    width: 100%;
    height: 100%;
    max-height: 11rem;
    overflow: visible;
}

.dash-arc-track,
.dash-arc-fill {
    fill: none;
    stroke-width: 9;
    stroke-linecap: round;
}

.dash-arc-track { stroke: var(--line); }
.dash-arc-fill  { stroke: var(--tile-color, var(--green)); }

.dash-arc-track.is-thin,
.dash-arc-fill.is-thin { stroke-width: 5; }

/* Both of these sit in the arc's gap rather than beside it: the reading in the
   middle where the needle of a real one would point, and the two ends under
   the tips of the arc. */
.dash-gauge-value {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 14%;
    text-align: center;
    font-size: clamp(1.2rem, 13cqw, 2.4rem);
}

.dash-gauge .dash-ends {
    position: absolute;
    left: 4%;
    right: 4%;
    bottom: 0;
}

.dash-dial {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    margin-top: .3rem;
}

.dash-dial svg {
    position: absolute;
    inset: 0 0 1.1rem;
    width: 100%;
    height: calc(100% - 1.1rem);
    overflow: visible;
}

.dash-dial .dash-ends {
    position: absolute;
    left: 2%;
    right: 2%;
    bottom: 0;
}

.dash-dial-tick { stroke: var(--muted); stroke-width: 1; }
.dash-dial-tick.is-major { stroke: var(--ink); stroke-width: 1.6; }

.dash-dial-needle {
    stroke: var(--ink-strong);
    stroke-width: 2.4;
    stroke-linecap: round;
}

.dash-dial-hub { fill: var(--ink-strong); }

.dash-dial-value {
    flex: none;
    text-align: center;
    font-size: clamp(1.1rem, 11cqw, 2rem);
}

.dash-ring {
    position: relative;
    flex: 1 1 auto;
    min-height: 60px;
    margin-top: .3rem;
}

.dash-ring svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.dash-ring-track,
.dash-ring-fill {
    fill: none;
    stroke-width: 8;
}

.dash-ring-track { stroke: var(--line); }

.dash-ring-fill {
    stroke: var(--tile-color, var(--green));
    stroke-linecap: round;
}

.dash-ring-centre {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
}

.dash-ring-value { font-size: clamp(1rem, min(11cqw, 12cqh), 2rem); }
.dash-ring-share { font-size: .68rem; }

/* --- the bar meter and the tank ----------------------------------------- */

.dash-meter {
    margin-top: auto;
    padding-top: .35rem;
}

.dash-meter-track {
    position: relative;
    height: .6rem;
    overflow: hidden;
    border-radius: 999px;
    background: var(--line);
}

.dash-meter-fill {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-radius: inherit;
    background: var(--tile-color, var(--green));
}

/* Quarter marks, cut into the bar in the page's own colour. */
.dash-meter-tick {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--page);
    opacity: .6;
}

.dash-meter .dash-ends { margin-top: .2rem; }

.dash-tank-wrap {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: stretch;
    gap: .8rem;
    margin-top: .4rem;
}

.dash-tank {
    position: relative;
    flex: none;
    width: clamp(2.2rem, 34cqw, 4.5rem);
    overflow: hidden;
    border: 2px solid var(--line);
    border-radius: 10px;
    background: var(--input-bg);
}

.dash-tank-fill {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--tile-color, var(--green));
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, .22);
}

.dash-tank-mark {
    position: absolute;
    right: 0;
    width: 32%;
    height: 1px;
    background: rgba(232, 240, 242, .3);
}

.dash-tank-read {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: .15rem;
}

.dash-tank-share {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: 1.15rem;
    color: var(--ink-strong);
}

/* --- change, low/mean/high, status, recent ------------------------------ */

.dash-delta {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: .2rem .5rem;
    margin-top: .35rem;
    font-variant-numeric: tabular-nums;
}

.dash-delta-arrow { font-size: .8rem; color: var(--tile-ink, var(--green)); }
.dash-delta.is-flat .dash-delta-arrow { color: var(--muted); }
.dash-delta-amount { font-weight: 600; color: var(--ink-strong); }
.dash-delta-pct { font-size: .85rem; color: var(--muted); }
.dash-delta-since { margin-top: auto; padding-top: .3rem; }

.dash-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: .5rem;
    margin-top: .45rem;
}

.dash-stat {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.dash-stat-label {
    font-size: .66rem;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
}

.dash-stat-value {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: clamp(1rem, 7cqw, 1.6rem);
    font-variant-numeric: tabular-nums;
    color: var(--ink-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-stat:nth-child(2) .dash-stat-value { color: var(--tile-ink, var(--green)); }

/* The span from lowest to highest, with a tick at the mean and a ring at the
   latest reading. */
.dash-strip {
    position: relative;
    height: 14px;
    margin: .6rem .35rem 0;
}

.dash-strip-track {
    position: absolute;
    left: 0;
    right: 0;
    top: 5px;
    height: 4px;
    border-radius: 2px;
    background: linear-gradient(90deg, var(--tile-soft, var(--line)), var(--tile-color, var(--green)));
}

.dash-strip-mean {
    position: absolute;
    top: 1px;
    width: 2px;
    height: 12px;
    margin-left: -1px;
    background: var(--ink);
}

.dash-strip-now {
    position: absolute;
    top: 2px;
    width: 10px;
    height: 10px;
    margin-left: -5px;
    border: 2px solid var(--ink-strong);
    border-radius: 50%;
    background: var(--panel);
}

.dash-status {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    align-items: center;
    gap: .75rem;
    --led: var(--tile-color, var(--green));
}

.dash-status.is-high,
.dash-status.is-low { --led: var(--danger); }

.dash-status.is-quiet,
.dash-status.is-never,
.dash-status.is-paused { --led: var(--muted); }

.dash-led {
    flex: none;
    width: clamp(1rem, 24cqh, 2.2rem);
    aspect-ratio: 1;
    border-radius: 50%;
    background: var(--led);
    box-shadow: 0 0 14px color-mix(in srgb, var(--led) 45%, transparent);
}

/* A light that is off: hollow, and no glow, so it reads as unlit rather than
   as a grey one. */
.dash-status.is-quiet .dash-led,
.dash-status.is-never .dash-led,
.dash-status.is-paused .dash-led {
    background: transparent;
    border: 2px solid var(--led);
    box-shadow: none;
}

.dash-status-text { min-width: 0; }

.dash-status-label {
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: clamp(1rem, 8cqw, 1.35rem);
    line-height: 1.2;
    color: var(--ink-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-status.is-high .dash-status-label,
.dash-status.is-low .dash-status-label { color: var(--danger); }

.dash-status-detail {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.dash-status-detail .unit { margin-left: .15rem; }

.dash-recent-box {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
    margin-top: .35rem;
}

.dash-recent {
    width: 100%;
    border-collapse: collapse;
    font-size: .82rem;
    font-variant-numeric: tabular-nums;
}

.dash-recent th,
.dash-recent td {
    padding: .22rem 0;
    white-space: nowrap;
}

.dash-recent th {
    text-align: left;
    font-weight: 400;
    color: var(--muted);
}

.dash-recent td {
    text-align: right;
    color: var(--ink-strong);
}

.dash-recent tr + tr > * { border-top: 1px solid var(--line); }

.dash-recent tr:first-child td {
    font-weight: 600;
    color: var(--tile-ink, var(--green));
}

/* --- the heatmap and the histogram -------------------------------------- */

.dash-heat {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
    column-gap: .45rem;
    row-gap: .2rem;
    margin-top: .4rem;
}

.dash-heat-days {
    display: grid;
    grid-template-rows: repeat(var(--days, 7), minmax(0, 1fr));
    font-size: .62rem;
    color: var(--muted);
}

.dash-heat-days span {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    overflow: hidden;
    line-height: 1;
    white-space: nowrap;
}

.dash-heat-cells {
    display: grid;
    grid-template-columns: repeat(24, minmax(0, 1fr));
    grid-template-rows: repeat(var(--days, 7), minmax(0, 1fr));
    gap: 2px;
}

.dash-heat-cells i {
    display: block;
    min-height: 2px;
    border-radius: 2px;
}

/* An hour with no readings, drawn rather than left blank: a gap in the
   record is something to notice, and a blank reads as the lowest value. */
.dash-heat-cells i.is-empty { box-shadow: inset 0 0 0 1px var(--line); }

.dash-heat-hours {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    font-size: .62rem;
}

.dash-heat-legend {
    flex: none;
    display: flex;
    align-items: center;
    gap: .45rem;
    margin-top: .3rem;
    font-size: .68rem;
    font-variant-numeric: tabular-nums;
}

.dash-heat-ramp {
    flex: 0 1 8rem;
    height: 6px;
    border-radius: 3px;
}

.dash-hist {
    position: relative;
    flex: 1 1 auto;
    min-height: 30px;
    margin-top: .45rem;
}

.dash-hist svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.dash-hist rect {
    fill: var(--tile-color, var(--green));
    opacity: .85;
}

.dash-hist rect:hover { opacity: 1; }

.dash-hist-now {
    stroke: var(--ink-strong);
    stroke-width: 1.5;
    stroke-dasharray: 3 2;
    vector-effect: non-scaling-stroke;
}

.dash-tile-histogram .dash-ends { margin-top: .2rem; }

/* --- arranging ---------------------------------------------------------- */

.dash-panel.is-editing .dash-tile {
    cursor: grab;
    border-style: dashed;
    /* Room for the controls along the bottom, so the reading is not under
       them. */
    padding-bottom: 2.3rem;
    /* The pointer drives the drag, so the browser must not also read a swipe
       on the same finger as a scroll. */
    touch-action: none;
    user-select: none;
}

.dash-panel.is-editing .dash-tile.is-dragging {
    cursor: grabbing;
    border-style: solid;
    border-color: var(--tile-color, var(--green));
    box-shadow: var(--btn-glow);
    z-index: 5;
}

/* An empty section while arranging is somewhere to drop a tile, so it has
   room to be dropped into and says so. */
.dash-panel.is-editing .dash-grid:empty {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 6rem;
    border: 1px dashed var(--line);
    border-radius: var(--radius);
}

.dash-panel.is-editing .dash-grid:empty::before {
    content: "Drag tiles here, or add one";
    font-size: .85rem;
    color: var(--muted);
}

/* Along the bottom rather than over the top right. A four-column tile is
   about 200px wide and the controls need most of that, so anywhere else on
   the tile they sit on the parameter's own name. They stop short of the
   corner, which is the resize grip's. */
.dash-tile-controls {
    position: absolute;
    left: .3rem;
    right: 1.35rem;
    bottom: .3rem;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: .2rem;
}

.dash-tile-controls .dash-text-btn { flex: 1 1 auto; min-width: 0; }

.dash-tile-btn {
    width: 1.5rem;
    height: 1.5rem;
    padding: 0;
    line-height: 1;
    font-size: .95rem;
    border: 1px solid var(--line);
    border-radius: 4px;
    background: var(--input-bg);
    color: var(--ink);
    cursor: pointer;
}

.dash-tile-btn.dash-text-btn {
    width: auto;
    padding: 0 .55rem;
    font: inherit;
    font-size: .74rem;
}

.dash-tile-btn:hover:not(:disabled) { border-color: var(--green); color: var(--ink-hover); }
.dash-tile-btn:disabled { opacity: .4; cursor: default; }
.dash-tile-remove:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); }

/* Two short diagonal strokes in the corner, the drawn convention for "this
   corner drags". */
.dash-tile-resize {
    position: absolute;
    right: 2px;
    bottom: 2px;
    z-index: 3;
    width: 16px;
    height: 16px;
    cursor: nwse-resize;
    touch-action: none;
    background:
        linear-gradient(135deg, transparent 52%, var(--muted) 52%, var(--muted) 60%, transparent 60%,
                                transparent 72%, var(--muted) 72%, var(--muted) 80%, transparent 80%);
}

.dash-page-tools {
    display: flex;
    gap: .5rem;
    margin-top: 1.1rem;
    padding-top: .85rem;
    border-top: 1px dashed var(--line);
}

/* --- the live poll ------------------------------------------------------ */

/* The footer under the tiles: whatever the dashboard has just been told on the
   left, and how often it is asking on the right. */
.dash-foot {
    display: flex;
    align-items: baseline;
    gap: 1rem;
    margin-top: .8rem;
}

.dash-rate {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: .4rem;
    white-space: nowrap;
}

/* The display above outranks the browser's [hidden], and the rate is hidden
   whenever the dashboard on screen has nothing on it to keep up to date. */
.dash-rate[hidden] { display: none; }

/* Green while the clock is running, and the muted grey of any other disabled
   thing on the page once it stops. The dot is the whole of the signal - the
   text beside it says the rate, and a second colour saying the same thing
   twice would make the rate look like a warning when it grows. */
.dash-rate-dot {
    width: .45rem;
    height: .45rem;
    border-radius: 50%;
    background: var(--ok);
    flex: none;
}

.dash-rate.is-paused .dash-rate-dot { background: var(--muted); }

/* A tile whose number has just changed. Short, and on the border rather than
   the background: a tile that flashes its whole face every few seconds is a
   distraction, whereas an edge that lights and fades is noticed only if you
   were looking at it. */
@keyframes dash-fresh {
    from { border-color: var(--tile-color, var(--green)); }
    to   { border-color: var(--line); }
}

.dash-tile.is-fresh { animation: dash-fresh 1.4s ease-out; }

/* --- the dialog --------------------------------------------------------- */

/* Choices on the left, the tile as it will look on the right - kept in view
   while the choices scroll, since watching it change is the point of it. */
.dash-sheet { width: min(72rem, calc(100vw - 2rem)); }

.dash-sheet-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem);
    gap: 1.5rem;
    align-items: start;
}

.dash-sheet-side {
    position: sticky;
    top: 0;
}

.dash-sheet-narrow { max-width: 34rem; }

.dash-sheet-remove { margin-right: auto; }

.dash-field {
    min-width: 0;
    margin: 0 0 1.35rem;
    padding: 0;
    border: 0;
}

.dash-field[hidden] { display: none; }

.dash-field legend {
    margin-bottom: .5rem;
    padding: 0;
    font-family: 'Jura', sans-serif;
    font-weight: 600;
    font-size: .95rem;
    color: var(--ink-strong);
}

.dash-field-help { margin: .45rem 0 0; }

.dash-section-input { width: 100%; }

/* The parameter list: one bordered box, a heading per device that stays put
   while its parameters scroll under it, and rows on two straight edges - the
   name on the left, the reading on the right. */
.dash-param-search { width: 100%; margin-bottom: .5rem; }

.dash-param-list {
    max-height: 17rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--input-bg);
}

.dash-param-group + .dash-param-group { border-top: 1px solid var(--line); }

.dash-param-device {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: baseline;
    gap: .5rem;
    padding: .45rem .75rem .35rem;
    border-bottom: 1px solid var(--line);
    background: var(--panel);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
}

.dash-param-devgroup {
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
}

.dash-param {
    display: grid;
    grid-template-columns: 1rem minmax(0, 1fr) auto auto;
    align-items: center;
    gap: .65rem;
    width: 100%;
    padding: .45rem .75rem;
    border: 0;
    background: none;
    font: inherit;
    font-size: .9rem;
    color: var(--ink);
    text-align: left;
    cursor: pointer;
}

.dash-param + .dash-param { box-shadow: inset 0 1px 0 var(--line); }
.dash-param:hover { background: var(--row-hover); color: var(--ink-hover); }

.dash-param:focus-visible {
    outline: none;
    box-shadow: inset 0 0 0 2px var(--blue);
}

.dash-param.is-chosen {
    background: var(--nav-hover);
    color: var(--ink-hover);
}

/* A radio dot, since one parameter is chosen at a time. */
.dash-param-tick {
    width: 1rem;
    height: 1rem;
    border: 1px solid var(--muted);
    border-radius: 50%;
}

.dash-param.is-chosen .dash-param-tick {
    border-color: var(--green);
    background: var(--green);
    box-shadow: inset 0 0 0 3px var(--nav-hover);
}

.dash-param-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dash-param-on {
    grid-column: 3;
    padding: 0 .45rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: .68rem;
    color: var(--muted);
    white-space: nowrap;
}

.dash-param-reading {
    grid-column: 4;
    min-width: 5.5rem;
    text-align: right;
    font-size: .82rem;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    white-space: nowrap;
}

.dash-param-empty { margin: 0; padding: .75rem; }

/* The styles, as a grid of drawn cards under their group headings. */
.dash-style-group {
    margin: .1rem 0 .4rem;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
}

.dash-style-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
    gap: .45rem;
    margin-bottom: .8rem;
}

.dash-style {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3rem;
    padding: .55rem .4rem .45rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--input-bg);
    font: inherit;
    font-size: .78rem;
    line-height: 1.2;
    color: var(--ink);
    text-align: center;
    cursor: pointer;
}

.dash-style:hover { border-color: var(--muted); color: var(--ink-hover); }

.dash-style.is-chosen {
    border-color: var(--green);
    box-shadow: var(--focus-ring);
    color: var(--ink-hover);
}

.dash-icon {
    width: 3rem;
    height: 2rem;
    color: var(--draft-color, var(--green));
}

.dash-icon .f  { fill: currentColor; }
.dash-icon .s  { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.dash-icon .m  { fill: none; stroke: var(--muted); stroke-width: 1.6; stroke-linecap: round; opacity: .7; }
.dash-icon .mf { fill: var(--muted); opacity: .45; }

.dash-style-about {
    min-height: 2.8em;
    margin: 0;
    color: var(--muted);
}

/* Colour swatches. The first is "whatever the section is", drawn half in that
   colour; the last is any colour at all, and holds the browser's own picker
   under a colour wheel. */
.dash-swatches {
    display: flex;
    flex-wrap: wrap;
    gap: .45rem;
}

.dash-swatch {
    position: relative;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 2px solid var(--panel);
    border-radius: 50%;
    background: var(--swatch);
    box-shadow: 0 0 0 1px var(--line);
    cursor: pointer;
}

.dash-swatch:hover { box-shadow: 0 0 0 1px var(--muted); }
.dash-swatch.is-chosen { box-shadow: 0 0 0 2px var(--ink-hover); }

.dash-swatch.is-auto { background: linear-gradient(135deg, var(--swatch) 50%, var(--input-bg) 50%); }

.dash-swatch.is-custom {
    background:
        linear-gradient(var(--swatch), var(--swatch)),
        conic-gradient(#e66767, #e8b45c, #4fd39b, #63d3e8, #9085e9, #d55181, #e66767);
}

.dash-swatch.is-custom input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    opacity: 0;
    cursor: pointer;
}

.dash-segments {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}

.dash-place {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
}

.dash-place-item {
    display: flex;
    flex-direction: column;
    gap: .25rem;
    flex: 1 1 12rem;
    min-width: 0;
}

.dash-preview-label {
    margin-bottom: .4rem;
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
}

.dash-preview {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    min-height: 10rem;
    overflow: hidden;
    padding: 1rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--page);
}

.dash-preview-empty {
    margin: auto;
    max-width: 15rem;
    text-align: center;
}

.dash-preview-links {
    margin: .6rem 0 0;
    text-align: center;
}

.dash-preview-links[hidden] { display: none; }

@media (max-width: 860px) {
    .dash-sheet-layout { grid-template-columns: minmax(0, 1fr); }
    .dash-sheet-side { position: static; order: -1; }
}

/* --- without script ----------------------------------------------------- */

/* The charts page's list of parameter pages, for a browser without script.
   Gone before the first paint where there is script - the <head> sets .js -
   so it is never on screen for the moment before the workspace replaces it. */
.js #charts-static { display: none; }

.dash-plain {
    list-style: none;
    padding: 0;
    margin: 0;
}

.dash-plain li {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: .4rem 0;
    border-bottom: 1px solid var(--line);
}

.dash-plain li:last-child { border-bottom: 0; }

/* A tile is only so useful at phone width. Below this the grid gives up its
   columns and the tiles stack, each as tall as it was laid out and in the
   order they read - top to bottom, then left to right. Their rows have to be
   let go of as well as their columns, or two tiles that sat side by side
   would land on top of each other. */
@media (max-width: 700px) {
    .dash-grid { grid-template-columns: minmax(0, 1fr); }

    .dash-grid > .dash-tile {
        grid-column: 1 / -1 !important;
        grid-row: auto / span var(--h, 3) !important;
    }
}

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

/* The row of controls above a sortable table: a search, a count of what it
   found, and whatever filters that table has. */
.table-tools {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.table-tools input[type="search"] {
    width: auto;
    flex: 1 1 16rem;
    max-width: 26rem;
    padding: .4rem .6rem;
    font-size: .85rem;
}

.table-tools-spacer { flex: 1 1 auto; }

/* A filter sits at its natural width, not stretched across the row. */
.table-tools select { width: auto; padding: .4rem .6rem; font-size: .85rem; }

/* --- the devices menu ---------------------------------------------------- */

/* Every device, beside the one open (see "a side menu"). The same list,
   drawn the same way, is the charts page's menu, where a device opens to
   parameters to put on the chart rather than to its page - so these rules
   are about a device list wherever it is, and the few that are the devices
   page's own say so. */

.devices-fold { margin-left: auto; }

/* A group is a <details>, so it folds with no script at all and keeps its
   keyboard behaviour for free.

   Each named group wears one of the eight series hues down a rail, by its
   place in the menu, so a long menu reads as sections rather than one run of
   names. Never the only thing saying which group it is - the name is right
   there - and non-text only: the series palette clears 3:1 as a line on this
   panel, which is the bar for a part of the interface, and would not as small
   text. */
.device-group {
    margin-bottom: .3rem;
    border-left: 3px solid var(--group-hue, transparent);
    border-radius: 2px;
}

/* Ungrouped is not a group, so it gets the hairline rather than a hue. */
.device-group.is-ungrouped { border-left-color: var(--line); }

.device-group > summary {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .35rem .3rem .35rem .45rem;
    border-radius: 6px;
    list-style: none;
    cursor: pointer;
    font-size: .85rem;
    font-weight: 600;
}

.device-group > summary::-webkit-details-marker { display: none; }

/* The marker is drawn rather than inherited so it can turn with the state -
   the default triangle differs between browsers and cannot be styled. */
.device-group > summary::before {
    content: "";
    flex: none;
    width: 0;
    height: 0;
    border-left: 5px solid var(--group-hue, var(--muted));
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform .15s;
}

.device-group[open] > summary::before { transform: rotate(90deg); }

.device-group > summary:hover { color: var(--ink-hover); }

.device-group > summary:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.device-group-name {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.device-group-count {
    flex: none;
    font-size: .78rem;
    font-weight: 400;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* A group's + (a device into this group) and x (clear away this empty one),
   at the heading's end. The + is quiet until the heading is pointed at or
   tabbed to - there is one per group, and a column of them is noise. The x is
   only ever there on an empty group, so it is never quiet. */
.device-group-add,
.device-group-remove {
    flex: none;
    display: grid;
    place-items: center;
    width: 1.45rem;
    height: 1.45rem;
    margin-left: auto;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 6px;
    background: none;
    color: var(--muted);
    font: inherit;
    font-size: 1rem;
    font-weight: 400;
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
}

.device-group-add { opacity: 0; }
.device-group-add ~ .device-group-remove { margin-left: 0; }
.device-group-remove[hidden] { display: none; }

.device-group > summary:hover .device-group-add,
.device-group > summary:focus-within .device-group-add { opacity: 1; }

.device-group-add:hover {
    border-color: var(--line);
    background: var(--input-bg);
    color: var(--ink-hover);
}

.device-group-remove:hover {
    border-color: var(--danger);
    color: var(--danger);
}

.device-group-add:focus-visible,
.device-group-remove:focus-visible {
    outline: none;
    opacity: 1;
    box-shadow: var(--focus-ring);
}

@media (hover: none) {
    .device-group-add { opacity: 1; }
}

/* With no named group there is nothing to fold, so the one section has no
   heading and no rail - a list of devices under "Ungrouped" reads like a
   mistake. */
.is-flat .device-group.is-ungrouped { border-left-color: transparent; }
.is-flat .device-group.is-ungrouped > summary { display: none; }

.device-list {
    margin: 0 0 .15rem;
    padding: 0 0 0 .2rem;
    list-style: none;
}

/* A device: the grip, then a link holding its name and count of parameters,
   with when it last reported in its title. No text selection - under a mouse
   the whole row is a drag handle, and a drag that selects the names it passes
   reads as a different gesture. */
.device-item {
    position: relative;
    border-radius: var(--radius);
    -webkit-user-select: none;
    user-select: none;
}

.device-item[hidden] { display: none; }

/* In the link's left padding, out of the name's way, and faint until the row
   is pointed at, as on the dashboards menu. The ::before is a finger's worth
   of grip round the dots: under a finger it is the only handle. */
.device-grip {
    position: absolute;
    left: .4rem;
    top: calc(1.05rem - 7.5px);
    touch-action: none;
}

.device-grip::before {
    content: "";
    position: absolute;
    inset: -10px -7px;
}

.device-item:hover > .device-grip { opacity: .85; }

.device-link {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .4rem .5rem .4rem 1.3rem;
    border-radius: var(--radius);
    color: var(--ink);
    font-size: .9rem;
    text-decoration: none;
    -webkit-user-drag: none;
}

.device-link:hover {
    background: var(--input-bg);
    color: var(--ink-hover);
}

.device-link:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

/* The device on screen: a step brighter, and a bar of the accent down its
   edge, as the open dashboard is marked. */
.device-item.is-current > .device-link {
    background: var(--input-bg);
    color: var(--ink-hover);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--green);
}

.device-item.is-current > .device-link:focus-visible {
    box-shadow: inset 3px 0 0 var(--green), var(--focus-ring);
}

.device-item.is-paused .device-item-name { color: var(--muted); }

/* Over the plan's allowance (Sensifly\Allowance): muted as a paused one is,
   and tagged, since unlike a pause it is on its way to being deleted. */
.device-item.is-suspended .device-item-name { color: var(--muted); }
.device-flag {
    flex: none;
    margin-right: .35rem;
    padding: .05rem .4rem;
    font-size: .68rem;
}

.device-item-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.device-item-count {
    flex: none;
    font-size: .76rem;
    font-weight: 400;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* The open device's parameters, hung under it on a hairline, each with its
   latest reading. */
.device-params {
    margin: .1rem 0 .45rem 1.3rem;
    padding: 0 0 0 .45rem;
    border-left: 1px solid var(--line);
    list-style: none;
}

.device-param,
.device-param-add {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    padding: .28rem .5rem;
    border-radius: 6px;
    color: var(--ink);
    font-size: .84rem;
    text-decoration: none;
}

.device-param-add { color: var(--muted); }

.device-param:hover,
.device-param-add:hover {
    background: var(--input-bg);
    color: var(--ink-hover);
}

.device-param:focus-visible,
.device-param-add:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
}

.device-param[aria-current="page"] {
    background: var(--input-bg);
    color: var(--ink-hover);
    font-weight: 600;
    box-shadow: inset 2px 0 0 var(--green);
}

.device-param.is-paused .device-param-name,
.device-param.is-suspended .device-param-name { color: var(--muted); }

.device-param-name {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.device-param-value {
    flex: none;
    font-size: .78rem;
    font-weight: 400;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.device-empty { padding: .3rem .5rem .45rem 1.3rem; }
.device-empty[hidden] { display: none; }
.device-none { margin: .3rem .3rem .6rem; }

/* Being dragged: the row stays in the menu as the place it will land, dashed
   round, while a label of it follows the pointer. A shut group under the
   pointer lights its heading, since there is no place inside it to show. */
.devices-side.is-sorting,
.devices-side.is-sorting .device-link { cursor: grabbing; }

.device-item.is-dragging {
    outline: 1px dashed var(--green);
    outline-offset: -1px;
}

.device-item.is-dragging > .device-link {
    background: none;
    box-shadow: none;
    opacity: .5;
}

.device-item.is-dragging .device-params { display: none; }
.device-item.is-moving > .device-link { opacity: .6; }

.device-group.is-drop-target > summary {
    background: var(--input-bg);
    box-shadow: inset 0 0 0 1px var(--green);
}

.device-ghost {
    position: fixed;
    z-index: 70;
    padding: .35rem .7rem;
    background: var(--panel);
    border: 1px solid var(--green);
    border-radius: var(--radius);
    box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
    color: var(--ink-hover);
    font-size: .88rem;
    white-space: nowrap;
    pointer-events: none;
}

/* A new group's name, typed where the group will be. */
.device-group-naming {
    margin-bottom: .35rem;
    padding: .3rem;
    background: var(--input-bg);
    border-radius: var(--radius);
    box-shadow: inset 3px 0 0 var(--green);
}

.device-group-naming input[type=text] {
    padding: .35rem .5rem;
    font-size: .88rem;
}

.device-group-naming span {
    display: block;
    margin-top: .3rem;
    padding: 0 .2rem;
}

.devices-side-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
    margin-top: .5rem;
    padding: 0 .2rem;
}

.devices-side-actions a.range {
    display: inline-block;
    text-decoration: none;
}

.devices-side-note {
    margin: .5rem .3rem 0;
    color: var(--muted);
}

.devices-side-note.is-bad { color: var(--danger); }

/* Too narrow for two columns: the menu above the device, folded to its
   heading until asked for, so the device is what arrives on screen. */
@media (max-width: 900px) {
    .devices-side.is-folded .devices-side-body { display: none; }
    .devices-side.is-folded .side-head { margin-bottom: 0; }
}

/* The grip is an affordance, not all of the mechanism - under a mouse the
   whole row is draggable. Drawn rather than set as text, because two glyphs
   squeezed together came out small and lopsided.
   Six dots, and exactly six: the box is 10x15 and the tile is 5x5, so it comes
   out as two columns of three with no remainder. Each dot is centred in its
   own tile with room to spare - a radius of 1.6 inside a 5px cell - so none of
   them meets an edge and none can be clipped. */
.drag-grip {
    flex: none;
    align-self: center;
    width: 10px;
    height: 15px;
    color: var(--muted);
    opacity: .45;
    cursor: grab;
    background-image: radial-gradient(circle at 2.5px 2.5px, currentColor 1.6px, transparent 2.1px);
    background-size: 5px 5px;
    background-repeat: repeat;
}

/* --- sortable columns ---------------------------------------------------- */

/* On the table itself rather than on the devices list that first wanted it:
   the enrolment keys table sorts the same way, and two copies of this would drift.
   A table marked .sortable has a button in each header it can be ordered by.

   The header cell keeps its own padding; the button fills it so the whole cell
   is the target rather than just the word. */
.sortable th[data-sort] { padding: 0; }

.sortable th .sort {
    display: flex;
    align-items: center;
    gap: .35rem;
    width: 100%;
    padding: .6rem .75rem;
    background: none;
    border: 0;
    font: inherit;
    color: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    cursor: pointer;
    text-align: inherit;
}

.sortable th.numeric .sort { justify-content: flex-end; }

.sortable th .sort:hover { color: var(--ink-hover); }

.sortable th .sort:focus-visible {
    outline: none;
    box-shadow: inset var(--focus-ring);
}

/* The arrow. Always in the layout so the heading does not jump sideways when a
   column is sorted, but only inked on the one that is. */
.sortable th .sort::after {
    content: "\25B2";
    font-size: .62em;
    opacity: 0;
}

.sortable th[aria-sort="ascending"]  .sort::after { opacity: .75; }

.sortable th[aria-sort="descending"] .sort::after {
    content: "\25BC";
    opacity: .75;
}

.sortable th[aria-sort="ascending"]  .sort,
.sortable th[aria-sort="descending"] .sort { color: var(--ink-strong); }

/* --- dialogs ------------------------------------------------------------- */

/* showModal() hands focus to the dialog itself when nothing inside claims it,
   and the browser rings the whole box - which is what the payload editor does,
   since the first thing in it is a picture rather than a field. On a control a
   focus ring says where typing will land; around the box it outlines the only
   thing on screen and reads as something being wrong with it. Everything
   inside keeps its own ring, which is where the information is. */
dialog:focus { outline: none; }

/* --- the confirm dialog -------------------------------------------------- */

/* A real <dialog>, so the modality, the focus trap, Escape and the backdrop
   come from the browser. Only the look is set here. */
.confirm {
    width: min(28rem, calc(100vw - 2rem));
    padding: 1.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--ink);
    box-shadow: var(--panel-hover-glow), var(--shadow);
}

.confirm::backdrop {
    background: rgba(13, 14, 15, .55);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
}

.confirm-title {
    margin: 0 0 .75rem;
    font-size: 1.15rem;
    color: var(--green-ink);
    /* A device can be called anything, including something long and unbroken. */
    overflow-wrap: anywhere;
}

.confirm-body { margin: 0 0 1rem; color: var(--muted); font-size: .92rem; }

.confirm-warning { margin: 0 0 1.25rem; font-weight: 600; font-size: .9rem; }

.confirm-actions {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    flex-wrap: wrap;
}

/* --- a panel heading with an action beside it ---------------------------- */
.panel-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

/* --- the sheet: a dialog holding a form ---------------------------------- */

/* The same box as .confirm, sized for a form rather than for a question - a
   confirm is one sentence and two buttons, this is eight fields. The fields
   scroll inside it so the buttons stay on screen on a short one.

   Two positions, because it is opened two ways. Script opens it modally and
   marks it, which is the browser's own centred, backdropped box. Without
   script nothing can call showModal(), so the page renders it with `open`
   instead - and an open dialog is absolutely positioned by default, which
   would drop it on top of whatever is below it. In the flow it reads as the
   panel it replaced, which is exactly what it is when there is no script.

   `relative` rather than `static` because the corner close button is placed
   against the sheet, and needs it to be a containing block in both positions.
   It is in the flow either way. */
.sheet {
    width: min(46rem, calc(100vw - 2rem));
    max-height: calc(100vh - 4rem);
    padding: 1.5rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--ink);
    box-shadow: var(--panel-hover-glow), var(--shadow);
}

.sheet[open] {
    position: relative;
    display: flex;
    flex-direction: column;
    margin: 0 0 1.25rem;
}

.sheet[open].is-modal {
    position: fixed;
    inset: 0;
    margin: auto;
}

/* The sheet takes focus itself when it opens, so the reader is inside it
   without one of its controls being lit up as though they had picked it. It
   is a container rather than something to operate, and the backdrop already
   says where the reader is, so it does not draw a ring of its own. */
.sheet:focus { outline: none; }

.sheet::backdrop {
    background: rgba(13, 14, 15, .55);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
}

.sheet > form {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* Only the fields scroll. The heading says what this is and the buttons are
   how it is finished with, and neither is worth scrolling back for. */
.sheet-body {
    overflow-y: auto;
    min-height: 0;
}

/* The way out, in the corner rather than in the row of buttons at the foot.
   Closing is not one of the things the dialog is for - it is how the reader
   leaves - and in that row it sat beside Save reading as an alternative to it.

   Placed against the sheet rather than laid out in it, so the heading beside
   it keeps the dialog's own padding instead of being pushed into a row, and so
   it stays in the corner while the fields below scroll. Hidden until script
   shows it: with none, a sheet is a panel on the page and there is nothing to
   close. */
.sheet-close {
    position: absolute;
    top: .9rem;
    right: .9rem;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: none;
    color: var(--muted);
    cursor: pointer;
}

.sheet-close svg { width: 1rem; height: 1rem; display: block; }

.sheet-close:hover,
.sheet-close:focus-visible {
    color: var(--ink);
    border-color: var(--line);
    background: var(--row-hover);
}

/* Room for it, so a long title does not run under the X. */
.sheet h2 { padding-right: 2.5rem; }

/* A field the server refused, and what it said about it. The colour is the
   danger one the delete rows use: there is one red on this page and this is
   it. Marked on the wrapper rather than the input so the label reddens with
   the box - the pair is what the reader is looking for, and a red outline on
   its own reads as focus. */
.field.is-invalid > label { color: var(--danger); }

.field.is-invalid input,
.field.is-invalid select,
.field.is-invalid textarea {
    border-color: var(--danger);
}

.field-error {
    display: block;
    margin-top: .35rem;
    font-size: .85rem;
    color: var(--danger);
}

.sheet-actions {
    display: flex;
    justify-content: flex-end;
    gap: .5rem;
    flex-wrap: wrap;
    margin-top: 1.25rem;
}

/* The one sight of a key just created, at the top of the keys dialog. It was a
   panel with a green border when it stood on the page; in here it is the same
   green saying the same thing about a box that is now one thing among several,
   which is why it has a rule rather than the whole border. */
.new-key {
    margin-bottom: 1.5rem;
    padding: 0 0 1.25rem 1rem;
    border-left: 3px solid var(--green);
}

.new-key p { color: var(--muted); }

/* Deleting, at the foot of a dialog that is otherwise about editing. The rule
   above it is the dialog's own hairline in the warning colour: the button
   beside it is the same gradient as every other button here - that is
   deliberate, and stated where .danger is defined - so the boundary is what
   says this half of the dialog does not undo.

   Selected through the dialog because `.sheet > form` above sets a column and
   this form is a row.

   That rule also hands it `min-height: 0`, which is meant for the editing
   form - the one with the scrolling body that has to be allowed to give way.
   This form has nothing that scrolls, so on a dialog tall enough to hit its
   max-height the flex column was squeezing it and cutting the button off at
   the bottom. It keeps its own height instead, and the fields above it give
   way, which is what the scrolling body is there for.

   Padded below as well as above, so the button is not against the dialog's
   edge on a tall one. */
.sheet > .sheet-danger {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    flex-shrink: 0;
    min-height: auto;
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    padding-bottom: .5rem;
    border-top: 1px solid var(--danger);
}

.sheet-danger > div { min-width: min(24rem, 100%); flex: 1; }
.sheet-danger strong { color: var(--danger); }

/* A second subject inside one sheet - the keys, under the device's own fields.
   Ruled off and scrollable like the body above it, so a device with a dozen
   keys does not push Save and Delete out of reach. */
.sheet > .sheet-section {
    flex-shrink: 1;
    min-height: 0;
    overflow-y: auto;
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--line);
}

.sheet > .sheet-section > h3 { margin-top: 0; }

/* An icon on its own, so the padding stops being a label's and becomes a square.
   The svg carries currentColor, which the button's own states already move.

   The glyph is sized to the line box a label would have made - 1.55 is the
   body's line-height, inherited here - so an icon button standing next to a
   worded one is exactly as tall as it. Padding stays the same on all four
   sides, which is what keeps it square. */
.btn.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: .6rem;
    line-height: 1.55;
}

/* Exactly the height a label's line box would have been: the button is a flex
   container, so there is no line box to fall back on and the glyph's own height
   is what sets the button's. 1.55 is the inherited line-height, which is what
   makes a cog button and a worded one the same height to the pixel. */
.btn.btn-icon svg {
    display: block;
    width: 1.55em;
    height: 1.55em;
}

.btn.btn-icon.small { padding: .35rem; }

/* The payload editor, which is a bit picture, a mapping table of up to sixteen
   rows and a live decoder. It takes the content column's own width, so the
   picture wraps at the same place in the dialog as it did on the page. */
.sheet.wide { width: min(68rem, calc(100vw - 2rem)); }

/* The flash moved in from the top of the page. It arrives as the dialog's
   first child, above the heading, where the reader is already looking. */
.sheet > .flashes { margin-bottom: 1rem; }
.sheet > .flashes .notice:last-child { margin-bottom: 0; }


/* --- series colours ----------------------------------------------------- */

/* Categorical series colours, in fixed slot order.
 *
 * A parameter keeps its slot for as long as it is on screen, whatever else is
 * selected: colour follows the parameter, not its position in the selection,
 * so unticking one never repaints the others.
 *
 * Validated against this app's own panel colour - #171919 - for lightness,
 * chroma, colour-blind separation between neighbours, and contrast. Deliberately
 * outside the brand pair: these say which parameter, not which product, so a
 * green line here means parameter three and nothing else. The dashboard offers
 * them as its palette for tiles and sections, for the same reason.
 */
:root {
    --series-1: #3987e5;
    --series-2: #d95926;
    --series-3: #199e70;
    --series-4: #c98500;
    --series-5: #d55181;
    --series-6: #008300;
    --series-7: #9085e9;
    --series-8: #e66767;
}

/* The strip under a chart showing the whole range, with the window being
   looked at marked out on it. Its own box, because the library draws into
   whatever it is given and needs that to have a height. */
.chart-overview {
    position: relative;
    height: 58px;
    margin-top: .6rem;
}

.chart-overview[hidden] { display: none; }

@media (max-width: 900px) {
    .chart-panel.expanded .compare-analysis { display: none; }
}

/* The parameter page's analysis switches, under its chart. */
.chart-analysis {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem 1.25rem;
    margin-top: .7rem;
    font-size: .85rem;
}

/* --- the admin area ------------------------------------------------------- */

/* Every account at a glance (/admin) and one in full (/admin/users/...),
   made of the parts the rest of the site uses: stat tiles, panels, the
   sortable table, badges. Only what is the admin area's own is here. */

.admin-totals {
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    margin-bottom: 1.25rem;
}

/* The account's name is what a row is opened by. */
.admin-name { font-weight: 600; }
.admin-table .badge { margin-left: .25rem; }

.admin-back { margin: 0 0 .75rem; font-size: .9rem; }

/* One account's four panels of figures, two by two where there is room. */
.admin-facts-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
    gap: 0 1.25rem;
}

/* Label and figure on one line, the labels in a column of their own. */
.admin-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: .45rem 1.25rem;
    margin: 0;
}

.admin-facts dt { color: var(--muted); font-size: .88rem; }
.admin-facts dd { margin: 0; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* Thirty days of requests, a bar a day, in the series' first colour. */
.admin-spark {
    display: block;
    width: 100%;
    height: 2.5rem;
    margin: 1rem 0 .35rem;
    fill: var(--series-1);
}

/* What can be done: what each is, and its button, a row each. */
.admin-action {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .9rem 0;
    border-top: 1px solid var(--line);
}

.admin-action:first-child { border-top: 0; padding-top: 0; }
.admin-action > div:first-child { max-width: 40rem; }

.admin-action-form {
    display: flex;
    align-items: center;
    gap: .5rem;
    flex: none;
}

.admin-action-form select { width: auto; }

/* What admins have done, newest first. */
.admin-history {
    list-style: none;
    margin: 0;
    padding: 0;
}

.admin-history li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .2rem .6rem;
    padding: .45rem 0;
    border-top: 1px solid var(--line);
}

.admin-history li:first-child { border-top: 0; padding-top: 0; }
.admin-history li > .small:first-child { min-width: 6.5rem; }

@media (max-width: 700px) {
    .admin-action { flex-direction: column; align-items: flex-start; }
}

/* --- the charts page ---------------------------------------------------- */

/* The page is the workspace (see "a side menu"): the devices and what is on
   the chart down the left, the chart on the right, two panels as tall as the
   window leaves below the top bar - the script measures that into
   --charts-height - so the chart and its timeline are on screen together
   without scrolling. Neither panel sticks: between them they are the window. */
.charts-workspace {
    --side-width: 18rem;
    height: var(--charts-height, calc(100vh - 6rem));
    align-items: stretch;
}

/* The menu scrolls on its own, so a long list of devices never pushes what
   is on the chart out of reach. */
.charts-workspace > .side-panel {
    position: relative;
    top: 0;
    display: flex;
    flex-direction: column;
    min-height: 0;
    max-height: none;
}

/* A heading's buttons - Clear, here; Collapse all and Show all, on the
   devices menus - at its far end. Two together stay together there, rather
   than sharing out the room between them. */
.side-head > :is(.range, .side-icon) { margin-left: auto; }
.side-head > :is(.range, .side-icon):not([hidden]) ~ :is(.range, .side-icon) { margin-left: 0; }

/* A heading's button that is an icon - Collapse all - drawn as a menu row's
   ... is, but always there: it is the heading's, not a row's to show on
   hover. Centred on the heading rather than on its baseline, which an icon
   has none of. */
.side-icon {
    flex: none;
    align-self: center;
    display: grid;
    place-items: center;
    width: 1.65rem;
    height: 1.65rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 6px;
    background: none;
    color: var(--muted);
    cursor: pointer;
}

.side-icon svg {
    width: 1rem;
    height: 1rem;
}

/* Lit as the menu's rows are under the pointer: on the panel itself, the
   panel's colour would change nothing but the border. */
.side-icon:hover {
    border-color: var(--line);
    background: var(--input-bg);
    color: var(--ink-hover);
}

.side-icon:focus-visible {
    outline: none;
    color: var(--ink-hover);
    box-shadow: var(--focus-ring);
}

/* The display above would otherwise beat the hidden attribute. */
.side-icon[hidden] { display: none; }

/* The saved charts, at the top of the menu, ruled off from the devices under
   them. The list is the dashboards' (see "a list of named things"); under it,
   Save this chart for one nobody has kept, New chart while a kept one is
   open, and a line saying how the last change went. */
.charts-saved {
    margin-bottom: .9rem;
    padding-bottom: .8rem;
    border-bottom: 1px solid var(--line);
}

.charts-saved .side-list:empty { margin: 0; }

.charts-saved-none { margin: 0 0 .5rem; padding: 0 .3rem; }

/* Gone as soon as the list has anything in it - the first chart's name box
   too, which the list draws and the page's script does not hear about. */
.charts-saved .side-list:not(:empty) + .charts-saved-none { display: none; }

.charts-saved-actions {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}

.charts-saved-actions a.range { text-decoration: none; }

/* Kept in the flow while empty, rather than display: none, so a screen
   reader still hears what is put in it. */
.charts-saved-note { margin: .45rem 0 0; padding: 0 .3rem; }
.charts-saved-note:empty { margin: 0; }

/* The devices, drawn as the devices menu draws them, but a device opens and
   shuts its parameters here rather than going to its page - so it is a
   button, with a marker that turns where the devices menu has its grip - and
   a parameter is put on the chart or taken off rather than opened. */
/* The face, not the size: the size is the devices menu's, from the rules for
   a device row and a parameter, which the shorthand would undo. */
button.device-link,
button.device-param,
button.device-param-add {
    width: 100%;
    border: 0;
    background: none;
    font-family: inherit;
    line-height: inherit;
    text-align: left;
    cursor: pointer;
}

button.device-link { position: relative; }

button.device-link::before {
    content: "";
    position: absolute;
    left: .6rem;
    top: 50%;
    margin-top: -3.5px;
    border-left: 5px solid var(--muted);
    border-top: 3.5px solid transparent;
    border-bottom: 3.5px solid transparent;
    transition: transform .15s;
}

button.device-link[aria-expanded="true"]::before { transform: rotate(90deg); }

/* A device with something on the chart reads a step brighter, so what is
   plotted can be found in a shut list. */
.device-item.has-on > .device-link { color: var(--ink-hover); }
.device-item.has-on > .device-link .device-item-name { font-weight: 600; }

/* Its colour on the chart once it is there; an empty square until then. */
.charts-swatch {
    flex: none;
    align-self: center;
    width: .65rem;
    height: .65rem;
    border: 1px solid var(--line);
    border-radius: 2px;
}

button.device-param[aria-pressed="true"] {
    background: var(--input-bg);
    color: var(--ink-hover);
}

button.device-param[aria-pressed="true"] .charts-swatch { border-color: transparent; }

button.device-param:disabled,
button.device-param-add:disabled {
    opacity: .45;
    cursor: default;
}

.device-param-actions {
    display: flex;
    gap: .2rem;
}

.device-param-actions .device-param-add { width: auto; }

/* What is on the chart, under the devices: a line saying how much, and the
   legend, where each series is taken off. */
.charts-chosen {
    margin-top: .9rem;
    padding-top: .8rem;
    border-top: 1px solid var(--line);
}

.charts-summary { margin: 0 0 .5rem; padding: 0 .3rem; }

/* The legend, in a column this narrow: each series' name on one line with
   the x that takes it off, its line style and focus on the line under. */
.charts-legend .sc-legend-list {
    grid-template-columns: minmax(0, 1fr);
    gap: .15rem;
}

.charts-legend .sc-legend-item {
    flex-wrap: wrap;
    row-gap: .25rem;
    padding: .3rem .35rem;
}

.charts-legend .sc-legend-name { flex: 1 1 calc(100% - 4.6rem); }
.charts-legend .sc-legend-remove { order: 1; }
.charts-legend .sc-legend-item select,
.charts-legend .sc-legend-focus { order: 2; }
.charts-legend .sc-legend-item select { margin-left: 2.35rem; }

.charts-analysis {
    margin-top: 1rem;
    padding-top: .9rem;
    border-top: 1px solid var(--line);
}

.charts-analysis[hidden] { display: none; }
.charts-analysis .side-title { margin: 0 0 .5rem; padding: 0 .3rem; }
.charts-analysis .sc-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* How to get about, folded away at the foot of the panel: read once, then
   only in the way. */
.charts-help {
    margin-top: auto;
    padding-top: 1rem;
    font-size: .85rem;
}

.charts-help summary {
    color: var(--muted);
    cursor: pointer;
}

.charts-help p { margin: .5rem 0 0; }

/* The chart's panel: the toolbar, the chart taking every pixel of height
   left, the timeline, and one line of status. */
.charts-main {
    display: flex;
    flex-direction: column;
    min-height: 0;
    margin-bottom: 0;
    padding: .75rem;
}

.charts-toolbar { margin-bottom: .5rem; }
.charts-main .chart-range { margin-bottom: .5rem; }

.charts-option {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-size: .82rem;
    color: var(--muted);
}

.charts-option select {
    width: auto;
    padding: .25rem .4rem;
    font-size: .82rem;
}

.charts-plot {
    position: relative;
    flex: 1 1 auto;
    min-height: 240px;
}

.charts-main .chart-overview {
    flex: none;
    height: 52px;
    margin-top: .4rem;
}

.charts-status {
    flex: none;
    margin: .35rem 0 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Expanded: the workspace is the window, top bar and all covered. */
.charts-workspace.expanded {
    position: fixed;
    inset: 0;
    z-index: 200;
    height: 100vh;
    height: 100dvh;
    margin: 0;
    padding: .75rem .75rem .75rem 0;
    background: var(--page);
}

/* Too narrow for two columns: the menu above the chart, and the chart a
   fixed share of the window rather than whatever is left of it. */
@media (max-width: 900px) {
    .charts-workspace { height: auto; }
    .charts-plot { flex: none; height: 65vh; }
    .charts-workspace.expanded > .side-panel { display: none; }
    .charts-workspace.expanded { padding-left: .75rem; }
    .charts-workspace.expanded .charts-plot { flex: 1 1 auto; height: auto; }
}

/* --- the chart library's HTML (js/sensichart) --------------------------- */

/* The library positions these and nothing more; how they look is the
   site's. Every colour is a token, like everywhere else. */

.sc-tooltip {
    min-width: 9rem;
    max-width: min(24rem, 90%);
    padding: .45rem .6rem;
    border: 1px solid var(--line);
    border-radius: 6px;
    background: var(--panel);
    box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
    font-size: .78rem;
    line-height: 1.4;
    color: var(--ink);
}

.sc-tip-title {
    margin-bottom: .25rem;
    font-weight: 600;
    color: var(--ink-strong);
    white-space: nowrap;
}

.sc-tip-row {
    display: grid;
    grid-template-columns: .6rem minmax(0, 1fr) auto;
    column-gap: .45rem;
    align-items: center;
}

.sc-tip-swatch {
    width: .6rem;
    height: .6rem;
    border-radius: 2px;
}

.sc-tip-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sc-tip-value {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    color: var(--ink-strong);
}

.sc-tip-when,
.sc-tip-axis {
    grid-column: 2 / -1;
    font-size: .72rem;
    color: var(--muted);
}

.sc-tip-row.is-focused .sc-tip-name { color: var(--ink-hover); }

.sc-tip-more {
    margin-top: .2rem;
    color: var(--muted);
}

/* The legend, which is also each series' controls. */

.sc-legend-tools {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    margin-bottom: .5rem;
}

.sc-legend-tools input[type=search] {
    flex: 1 1 12rem;
    width: auto;
    padding: .3rem .6rem;
    font-size: .82rem;
}

.sc-legend-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr));
    gap: .2rem .9rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.sc-legend-item {
    display: flex;
    align-items: center;
    gap: .4rem;
    min-width: 0;
    padding: .2rem .35rem;
    border-radius: 4px;
    font-size: .82rem;
}

.sc-legend-item[hidden] { display: none; }
.sc-legend-item.is-selected { background: var(--code-bg); }
.sc-legend-item.is-dimmed { opacity: .55; }

.sc-legend-item.is-hidden .sc-legend-label {
    color: var(--muted);
    text-decoration: line-through;
}

.sc-legend-show { flex: none; margin: 0; accent-color: var(--green); }

/* The colour well, made to look like the swatch it replaces. */
.sc-legend-colour {
    flex: none;
    width: 1.05rem;
    height: 1.05rem;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 3px;
    background: none;
    cursor: pointer;
}

.sc-legend-colour::-webkit-color-swatch-wrapper { padding: 0; }
.sc-legend-colour::-webkit-color-swatch { border: 0; border-radius: 2px; }
.sc-legend-colour::-moz-color-swatch { border: 0; border-radius: 2px; }

.sc-legend-swatch {
    flex: none;
    width: .7rem;
    height: .7rem;
    border-radius: 2px;
}

.sc-legend-name {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    align-items: flex-start;
    min-width: 0;
    padding: 0 .2rem;
    border: 0;
    background: none;
    font: inherit;
    text-align: left;
    color: var(--ink);
    cursor: pointer;
}

.sc-legend-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sc-legend-name:hover .sc-legend-label,
.sc-legend-name[aria-pressed="true"] .sc-legend-label { color: var(--ink-hover); }

.sc-legend-name:focus-visible {
    outline: none;
    border-radius: 3px;
    box-shadow: var(--focus-ring);
}

.sc-legend-axis {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .72rem;
    color: var(--muted);
}

.sc-legend-item select {
    flex: none;
    width: auto;
    padding: .1rem .3rem;
    border-radius: 4px;
    font-size: .75rem;
}

/* The library's buttons, in the shape of the site's small pill button. A
   rule of their own rather than a second selector on .range, which the page
   tests expect to open its own rule. */
.sc-legend-bulk,
.sc-legend-focus {
    flex: none;
    padding: .2rem .6rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--input-bg);
    font: inherit;
    font-size: .76rem;
    color: var(--muted);
    cursor: pointer;
}

.sc-legend-bulk:hover,
.sc-legend-focus:hover {
    border-color: var(--green);
    color: var(--ink-hover);
}

/* Taking a series off the chart, where the page allows it from the legend. */
.sc-legend-remove {
    flex: none;
    padding: 0 .2rem;
    border: 0;
    border-radius: 3px;
    background: none;
    font-size: 1.05rem;
    line-height: 1;
    color: var(--muted);
    cursor: pointer;
}

.sc-legend-remove:hover { color: var(--danger); }

.sc-legend-remove:focus-visible {
    outline: none;
    color: var(--danger);
    box-shadow: var(--focus-ring);
}

.sc-legend-focus[aria-pressed="true"] {
    background: var(--accent-grad);
    border-color: transparent;
    color: var(--on-accent);
}

/* Numbers about a series. */

.sc-analysis-title {
    margin: 0 0 .15rem;
    font-size: 1rem;
}

.sc-analysis-scope {
    margin: 0 0 .7rem;
    font-size: .8rem;
    color: var(--muted);
}

.sc-analysis-empty {
    margin: 0;
    font-size: .9rem;
    color: var(--muted);
}

.sc-metrics {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    gap: .6rem 1rem;
    margin: 0 0 .8rem;
}

.sc-metric dt {
    font-size: .7rem;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--muted);
}

.sc-metric dd {
    margin: 0;
    font-size: .95rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-strong);
}

.sc-metric-note {
    display: block;
    font-size: .72rem;
    color: var(--muted);
}

.sc-analysis-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .5rem 1rem;
    margin-bottom: .8rem;
    font-size: .85rem;
}

.sc-check {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--ink);
    cursor: pointer;
}

.sc-check input[type=checkbox] { margin: 0; accent-color: var(--green); }

.sc-check input[type=number] {
    width: 4.5rem;
    padding: .15rem .4rem;
    font-size: .85rem;
}

.sc-analysis [hidden] { display: none; }

/* The charts' right-click menu: a panel at the pointer, a heading over each
   thing it can act on, and plain rows to pick from. */
.sc-menu {
    padding: .3rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--panel);
    box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
    font-size: .85rem;
}

.sc-menu-heading {
    max-width: 20rem;
    padding: .35rem .55rem .2rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: .75rem;
    color: var(--muted);
}

.sc-menu-kind {
    margin-right: .4rem;
    font-size: .65rem;
    letter-spacing: .06em;
    text-transform: uppercase;
}

.sc-menu-item {
    padding: .35rem .55rem;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--ink);
}

.sc-menu-item:hover:not([disabled]),
.sc-menu-item:focus-visible {
    outline: none;
    background: var(--nav-hover);
    color: var(--ink-hover);
}

.sc-menu-item[disabled] { opacity: .45; }

.sc-menu-separator {
    height: 1px;
    margin: .3rem .2rem;
    background: var(--line);
}

/* Stacked rows: the grip that moves a row and the edge that resizes it. The
   grip is quiet until it is wanted; the edge is invisible until the pointer
   or the keyboard finds it, then shows as a line the width of the row. */
.sc-row-grip {
    border-radius: 3px;
    color: var(--muted);
}

.sc-row-grip:hover,
.sc-row-grip.is-dragging { color: var(--ink-hover); }

.sc-row-grip:focus-visible {
    outline: none;
    color: var(--ink-hover);
    box-shadow: var(--focus-ring);
}

.sc-row-resize::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 3px;
    height: 2px;
    border-radius: 1px;
    background: transparent;
    transition: background-color .15s ease;
}

.sc-row-resize:hover::after,
.sc-row-resize:focus-visible::after,
.sc-row-resize.is-dragging::after { background: var(--blue); }

.sc-row-resize:focus-visible { outline: none; }

/* --- the accent rail ---------------------------------------------------- */

/* The green-to-blue line under the header and over the footer. It used to be
   a border-image, which cannot be animated - a gradient in a border paints
   once and has no background-position to move. So the line is drawn again as
   a pseudo-element lying exactly over that border: the border stays as the
   static fallback, and this is what drifts.

   The ramp runs green -> blue -> green so the tile ends on the colour it
   started with, and sliding it by exactly one tile width loops with no seam. */

.topbar::after,
.sitefooter::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    height: 3px;
    background-image: linear-gradient(90deg, var(--green), var(--blue), var(--green));
    background-size: 200% 100%;
    animation: accent-drift 14s linear infinite;
    pointer-events: none;
}

/* -3px puts each one over its own border rather than beside it, so the layout
   is unchanged from when the border was doing the drawing. */
.topbar::after     { bottom: -3px; }
.sitefooter::before { top: -3px; }

/* Negative, deliberately. A background-position percentage resolves against
   (container width - image width), and the image here is twice the container,
   so that bracket is negative: a *positive* percentage would slide the ramp
   leftwards. -200% is one whole tile to the right, which both travels the way
   we want and lands exactly on the next repeat, so the loop has no seam. */
@keyframes accent-drift {
    from { background-position:    0% 0; }
    to   { background-position: -200% 0; }
}

/* The sparkle's track. It exists to clip: the spark starts and ends outside
   the bar, and clipping on the header itself would cut off the open mobile
   menu, so the overflow is confined to this 3px strip. Built by the script in
   Page.php, so no page has to carry the markup. */
.accent-rail {
    position: absolute;
    left: 0;
    right: 0;
    height: 3px;
    overflow: hidden;
    pointer-events: none;
}

.topbar     > .accent-rail { bottom: -3px; }
.sitefooter > .accent-rail { top: -3px; }

.accent-spark {
    position: absolute;
    top: 0;
    left: 0;
    height: 3px;
    width: 110px;
    opacity: 0;
    /* Bright in the middle, nothing at the ends, so it reads as a highlight
       travelling along the line rather than a block sliding over it. */
    background: linear-gradient(90deg,
        rgba(255, 255, 255, 0) 0%,
        rgba(255, 255, 255, .35) 35%,
        rgba(255, 255, 255, .95) 50%,
        rgba(255, 255, 255, .35) 65%,
        rgba(255, 255, 255, 0) 100%);
}

/* Readers who ask for less motion get the line, and none of the movement. */
@media (prefers-reduced-motion: reduce) {
    .topbar::after,
    .sitefooter::before {
        animation: none;
        background-image: linear-gradient(90deg, var(--green), var(--blue));
    }
    .accent-rail { display: none; }
    .dash-tile.is-fresh { animation: none; }

    /* Hover states arrive at once. The states themselves are unchanged - only
       the travel between them goes. */
    .btn,
    .btn::after,
    .range,
    .mainnav a,
    .menu-toggle,
    .sortable th .sort,
    .device-group > summary,
    .drag-grip,
    .format-switch .format-btn,
    .codetabs-bar .codetab-btn {
        transition: none;
    }

    /* The lift is movement rather than a state, so it goes altogether instead
       of arriving instantly. The brightened fill and the glow stay - they say
       the same thing without anything travelling. */
    .btn:hover,
    .btn:focus-visible,
    .btn:active {
        transform: none;
    }
}

/* --- tabbed code examples ----------------------------------------------- */

/* The page-level example-format switch. Deliberately a pill row rather than
   the tab strip below: it is a preference, not a tab attached to a block of
   code, and it borrows the shape the chart range buttons already use. */

.format-switch {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
}

.format-switch-label {
    font-size: .8rem;
    color: var(--muted);
    margin-right: .15rem;
}

.format-switch .format-btn {
    padding: .3rem .8rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--input-bg);
    font: inherit;
    font-size: .82rem;
    color: var(--muted);
    cursor: pointer;
    transition: background .15s, color .15s;
}

/* Reserve the bold width, so switching does not shuffle the row. */
.format-switch .format-btn::after {
    content: attr(data-label);
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 600;
}

.format-switch .format-btn:hover {
    border-color: var(--green);
    color: var(--ink-hover);
}

.format-switch .format-btn[aria-pressed="true"] {
    background: var(--accent-grad);
    border-color: transparent;
    color: var(--on-accent);
    font-weight: 600;
}

.format-switch .format-btn:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: 2px;
}

/* The subheading over an example - Request or Response: set like the other
   small labels on the page, and close to the box it names, so it reads as
   that box's title rather than the start of a new section. */
.example-heading {
    margin: 1.1rem 0 .4rem;
    font-family: 'Jura', 'Segoe UI', sans-serif;
    font-size: .78rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

.example-heading + pre { margin-top: 0; }

/* In a request's strip row it shares the line with the language buttons, and
   the row's own gap does the spacing. Its bottom margin is the strip's, so the
   two sit on one line. */
.codetabs-head > .example-heading {
    margin: 0 0 .35rem;
    align-self: center;
}

/* --- the language switch on an example ---------------------------------- */

/* The same pill strip as .format-switch, on its own classes rather than
   sharing them: that one's script sweeps every .format-btn on the page and
   sets it from the chosen tab label, which would leave these three pressed
   false every time somebody moved between Json and Binary. */
.lang-switch {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .35rem;
    /* Sat above a panel once and needed the room; in the tab row the row's own
       gap is the spacing, and a margin here would push the tabs out of line. */
    margin-bottom: .35rem;
}

.lang-switch[hidden] { display: none; }

.lang-switch-label {
    font-size: .8rem;
    color: var(--muted);
    margin-right: .15rem;
}

.lang-btn {
    padding: .3rem .8rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--input-bg);
    font: inherit;
    font-size: .82rem;
    color: var(--muted);
    cursor: pointer;
    transition: background .15s, color .15s, border-color .15s;
}

/* Reserve the bold width, so switching does not shuffle the row. */
.lang-btn::after {
    content: attr(data-lang);
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 600;
}

.lang-btn:hover {
    border-color: var(--green);
    color: var(--ink-hover);
}

.lang-btn[aria-pressed="true"] {
    background: var(--accent-grad);
    border-color: transparent;
    color: var(--on-accent);
    font-weight: 600;
}

.lang-btn:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: 2px;
}

.lang-block[hidden] { display: none; }

.lang-heading {
    font-size: .95rem;
    margin: 1.5rem 0 .5rem;
}

.lang-block:first-of-type .lang-heading { margin-top: 0; }

/* Once the strip is working it says which language this is, so the heading
   inside the block would only say it twice. */
.lang-set.is-switched .lang-heading { display: none; }

/* An example that exists in more than one form - a single value or a binary
   payload, say - with a strip of tabs sitting on the code block so only the
   form the reader picked is shown. */

.codetabs { margin: 0 0 1rem; }

/* The tab row: what is being sent on the left, and whatever the page put
   beside it - the language strip, here - pushed to the other end. They are two
   different questions about the same example, which is why they share a line
   rather than stacking into two strips a reader has to tell apart. Narrow
   enough and the aside wraps under the tabs, still to the right. */
.codetabs-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .5rem 1rem;
}

.codetabs-head > :last-child:not(.codetabs-bar) { margin-left: auto; }

.codetabs-bar {
    display: flex;
    flex-wrap: wrap;
    gap: .2rem;
}

.codetabs-bar .codetab-btn {
    position: relative;
    padding: .4rem 1rem;
    border: 1px solid var(--line);
    border-bottom: 0;
    border-radius: var(--radius) var(--radius) 0 0;
    background: var(--code-bg);
    font: inherit;
    font-size: .82rem;
    font-weight: 400;
    line-height: 1.4;
    letter-spacing: .02em;
    color: var(--muted);
    cursor: pointer;
    transition: background .15s, color .15s;
}

/* The selected tab is bold, which would make it wider and nudge the tabs after
   it every time the reader switched. Reserving the bold width in every state
   holds the strip still - the label is already on the button as data-label. */
.codetabs-bar .codetab-btn::after {
    content: attr(data-label);
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 600;
}

.codetabs-bar .codetab-btn:hover {
    background: var(--nav-hover);
    color: var(--ink-hover);
}

/* The selected tab takes the code block's own colours, so the two read as one
   piece rather than a control sitting above an unrelated box. Colour alone
   would be thin - in dark the block is barely darker than the panel behind it
   - so the choice is also carried by weight and by the accent bar below. */
.codetabs-bar .codetab-btn[aria-selected="true"] {
    background: var(--pre-bg);
    border-color: var(--pre-bg);
    color: var(--pre-ink);
    font-weight: 600;
}

/* The brand gradient across the top of the selected tab, the same mark the
   nav and the primary buttons use. Drawn inside the tab so switching moves no
   layout. */
.codetabs-bar .codetab-btn[aria-selected="true"]::before {
    content: "";
    position: absolute;
    top: -1px;
    left: -1px;
    right: -1px;
    height: 3px;
    background: var(--accent-grad);
    border-radius: var(--radius) var(--radius) 0 0;
}

.codetabs-bar .codetab-btn:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: -2px;
}

.codetab-panel > *:first-child { margin-top: 0; }
.codetab-panel > *:last-child  { margin-bottom: 0; }

/* Square the top of the block the strip sits on, so it reads as joined to
   whichever tab is selected rather than only to the first one. Applies only
   when the panel opens with the code block - some lead with a settings table. */
.codetab-panel > pre:first-child {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

.codetab-panel pre { margin-bottom: 0; }
.codetab-panel pre ~ * { margin-top: .9rem; }

/* The device page's examples put the code inside a language block, so the
   panel's first child is that block rather than the <pre> the rule above
   squares off. Once the strip is working the visible block is all the panel
   holds and its heading is hidden, so the box sits on the tabs exactly as a
   bare one does - a top margin here would be a gap between a tab and the block
   it is drawn as part of. Without script there is no strip and no tab row to
   join, and the headings are back, which is why this is on .is-switched. */
.codetab-panel .lang-set.is-switched .lang-block > pre {
    margin-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* --- pricing ------------------------------------------------------------ */

.plan {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 1.5rem;
    display: flex;
    flex-direction: column;
    box-shadow: var(--panel-glow);
}

.plan.current { border-color: var(--green); border-width: 2px; }

.plan .plan-name {
    font-family: 'Jura', sans-serif;
    font-size: 1.2rem;
    font-weight: 600;
    color: var(--green-ink);
}

.plan .plan-price {
    font-family: 'Jura', sans-serif;
    font-size: 2.1rem;
    font-weight: 600;
    color: var(--green);
    margin: .4rem 0;
}

.plan .plan-price small { font-size: .9rem; color: var(--muted); font-weight: 400; }

.plan ul {
    list-style: none;
    padding: 0;
    margin: .75rem 0 1.25rem;
    font-size: .9rem;
    color: var(--muted);
    flex: 1;
}

.plan li { padding: .25rem 0; }
.plan li::before { content: '\2713'; color: var(--green); margin-right: .5rem; font-weight: 700; }

/* A plan card is linked to from the calculator's answer, and lands clear of
   the sticky top bar. */
.plan[id] { scroll-margin-top: 5rem; }

/* --- the token calculator (the tokens page) --------------------------------- */

/* What a setup would use, and the plan it needs. The questions in as many
   columns as fit; the answer as three figures, a sentence, and every plan
   held up against it. */
.calc-inputs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(10.5rem, 1fr));
    gap: 0 1rem;
}

.calc-inputs[hidden],
.calc-profile[hidden],
.calc-result[hidden] { display: none; }

/* The profile: a block per device - its name, how many of it, what it comes
   to - and a row per parameter, each at its own rate. */
.calc-profile { margin-bottom: 1.25rem; }

.calc-profile-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .5rem .75rem;
    margin-bottom: .75rem;
}

.calc-subtitle { margin: 0; font-size: 1rem; }
.calc-spacer { flex: 1 1 auto; }

.calc-devices {
    display: grid;
    gap: .75rem;
    margin-bottom: .75rem;
}

.calc-device {
    padding: .75rem .85rem .8rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--input-bg);
}

.calc-device-head,
.calc-param {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
}

.calc-device-head { margin-bottom: .5rem; }
.calc-device-head > input[type=text] { flex: 1 1 12rem; width: auto; font-weight: 600; }

.calc-copies {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    color: var(--muted);
}

.calc-copies input { width: 5.5rem; }

/* What it comes to, at the row's end. */
.calc-device-total,
.calc-param-total {
    margin-left: auto;
    color: var(--muted);
    font-size: .85rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.calc-params {
    list-style: none;
    margin: 0 0 .5rem;
    padding: 0 0 0 .75rem;
    border-left: 1px solid var(--line);
}

.calc-param { padding: .25rem 0; }
.calc-param > input[type=text] { flex: 1 1 10rem; width: auto; }
.calc-param > input[type=number] { width: 5.5rem; }
.calc-param > select { width: auto; }
.calc-param-every { color: var(--muted); font-size: .9rem; }
.calc-param-none { padding: .25rem 0; }

.calc-device input,
.calc-device select { padding: .4rem .55rem; font-size: .9rem; background: var(--panel); }

/* On a phone the name has the line to itself, and how often and what it
   comes to share the next. */
@media (max-width: 560px) {
    .calc-param > input[type=text] { flex-basis: 100%; }
    .calc-param > input[type=number] { width: 4.5rem; }
}
.calc-figures { margin: .25rem 0 1rem; }

/* The plan, as a link to its card below - in the figure's own size. */
.calc-plan a { color: inherit; text-decoration: none; }
.calc-plan a:hover { text-decoration: underline; }

.calc-advice { margin: 0 0 1rem; }

.calc-plans .is-suggested td {
    background: var(--input-bg);
    color: var(--ink-hover);
}

.calc-plans .is-suggested td:first-child {
    box-shadow: inset 3px 0 0 var(--green);
    font-weight: 600;
}

/* What a plan is short of, in the colour that says so - and the word for it
   in the Fits column, so the colour is never the only sign. */
.calc-plans td.is-short { color: var(--danger); }
.calc-plans td.is-yes { color: var(--green-ink); font-weight: 600; }
.calc-plans td.is-no { color: var(--muted); }

/* --- misc --------------------------------------------------------------- */

.empty-state {
    text-align: center;
    padding: 3rem 1.5rem;
    color: var(--muted);
}

.empty-state h2 { margin-top: 0; }

.muted   { color: var(--muted); }
.small   { font-size: .85rem; }
.nowrap  { white-space: nowrap; }
.right   { text-align: right; }
.mt0     { margin-top: 0; }
.mb0     { margin-bottom: 0; }

hr {
    border: none;
    border-top: 1px solid var(--line);
    margin: 1.75rem 0;
}

/* --- footer ------------------------------------------------------------- */

.sitefooter {
    position: relative;
    border-top: 3px solid var(--green);
    background: var(--footer-bg);
    color: var(--footer-ink);
    font-size: .85rem;
}

.footer-inner {
    max-width: 1080px;
    margin: 0 auto;
    padding: 1.5rem 1.25rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.footer-brand {
    color: var(--green);
    letter-spacing: .22em;
    font-size: 1rem;
}

/* The mark beside the name, top and bottom. The name is filled with the
   accent gradient by clipping to text, which leaves its colour transparent;
   the mark takes currentColor, so it needs its own colour or it vanishes. */
.brand,
.footer-brand {
    display: inline-flex;
    align-items: center;
    gap: .55em;
}

.brand-mark {
    flex: none;
    width: 1.75em;
    height: 1.75em;
    color: var(--green);
}

.footer-brand .brand-mark {
    width: 1.6em;
    height: 1.6em;
}

.sitefooter nav {
    display: flex;
    gap: 1rem;
    flex-wrap: wrap;
    margin-left: auto;
}

.sitefooter a { color: var(--footer-ink); text-decoration: none; }
.sitefooter a:hover { color: var(--footer-link-hover); }
.footer-copy { color: var(--footer-dim); }

@media (max-width: 560px) {
    .sitefooter nav { margin-left: 0; }
}

/* --- accent gradient text ----------------------------------------------- */
/* Last in the file on purpose: these selectors tie with the base rules for
   .brand, .sensor-reading and .plan-price, so source order is what decides. */

/* Text filled with the accent gradient: the wordmark and every headline
   figure. Green at the start, blue by the end, so the numbers carry both brand
   colours rather than only the green. Falls back to a flat colour anywhere
   background-clip: text is unsupported, so the words are never invisible. */
.gradient-text,
.brand,
.footer-brand,
.stat .stat-value,
.sensor-card .sensor-reading,
.plan .plan-price {
    color: var(--green);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
    .gradient-text,
    .brand,
    .footer-brand,
    .stat .stat-value,
    .sensor-card .sensor-reading,
    .plan .plan-price {
        background: var(--accent-grad);
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
    }

    /* Child spans carry their own colour, so they opt back out of the fill. */
    .stat .stat-value small,
    .sensor-card .sensor-reading .unit,
    .plan .plan-price small {
        -webkit-background-clip: initial;
        background-clip: initial;
        background: none;
    }
}


/* --- binary payloads ------------------------------------------------------
   One editor and a list, rather than a panel per payload: a device may define
   as many messages as it has parameters, and sixteen open forms is not a page
   anyone can read. Picking one from the list is an ordinary link, so the
   editor's contents and the address bar stay the same thing. */

.payload-map input[type="number"] { width: 100%; }

/* The row the editor above is showing. A tint rather than a rule, so the list
   still reads as one table. */
.data tr.is-editing > td { background: var(--row-hover); }

/* Edit and Delete sit on one line at the end of a row. The delete is a form
   rather than a link because it changes something, and a form is a block. */
form.inline { display: inline; }

/* --- the bit map ----------------------------------------------------------
   Every bit of the message, in proportion: a 16-bit field is twice the width
   of an 8-bit one, so the picture answers "does this match my struct" at a
   glance in a way a column of offsets cannot.

   Wrapped at eight bytes a line. A pro plan allows sixty-four bytes, which on
   one line puts a 1-bit flag at a thousandth of the width - too narrow to
   label and too narrow to catch with a pointer. Eight bytes keeps a single bit
   at about 1.5% of the line, which is a target, and it reads like a hex dump.

   Sized with flex-grow rather than a grid of bit cells: --bits is the block's
   width in bits and does the proportioning, and the byte and bit rules are
   repeating backgrounds underneath, so they stay put however the blocks above
   them fall. */

.bitmap {
    margin: 0 0 1rem;
}

.bitmap-line {
    margin: 0 0 .55rem;
}

.bitmap-line:last-of-type { margin-bottom: 0; }

.bitmap-ruler,
.bitmap-track,
.bitmap-bits {
    display: flex;
    gap: 0;
    /* --fill is this line's share of a full one, so a message whose last line
       is two bytes draws those two bytes a quarter of the way across rather
       than stretched over the whole width. One bit is the same size on every
       line, which is the only way the picture can be read down as well as
       across. */
    width: calc(100% * var(--fill, 1));
}

.bitmap-ruler {
    margin-bottom: .15rem;
}

/* Every strip - the ruler, the blocks, the bit ticks - is sized the same way:
   an exact share of the line, calc'd from the bits it covers.

   Not flex-grow. With `flex: <bits> 1 0` an item's width is its padding plus a
   proportional share of what is left, so a row of blocks with padding drifts
   away from a row of ticks without it, by more at every boundary - which is
   exactly the misalignment between the tick marks and the block edges. An
   explicit basis and no growing makes the arithmetic the same for all three
   strips, so a boundary drawn in one lands on the boundary drawn in another. */
.bitmap-byte-label {
    flex: 0 0 calc(var(--bits, 8) * 100% / var(--line-bits, 64));
    box-sizing: border-box;
    min-width: 0;
    font-size: .62rem;
    letter-spacing: .03em;
    color: var(--muted);
    text-align: left;
    padding-left: .2rem;
    border-left: 1px solid var(--bit-byte-rule);
    overflow: hidden;
    white-space: nowrap;
}

.bitmap-track {
    border-radius: 5px;
    overflow: hidden;
    min-height: 2.5rem;
    /* A hairline every bit and a brighter one every byte, drawn under the
       blocks so a field that straddles a boundary visibly crosses one. */
    background:
        repeating-linear-gradient(
            to right,
            var(--bit-byte-rule) 0 1px,
            transparent 1px calc(100% / var(--bytes, 8))
        ),
        repeating-linear-gradient(
            to right,
            var(--bit-bit-rule) 0 1px,
            transparent 1px calc(100% / var(--line-bits, 64))
        ),
        var(--pre-bg);
}

.bitmap-block {
    flex: 0 0 calc(var(--bits, 1) * 100% / var(--line-bits, 64));
    box-sizing: border-box;
    min-width: 0;
    /* No horizontal padding: it would be added to the block's share rather
       than taken out of it, and the edges would stop landing on their bits.
       The labels carry their own inset instead. */
    padding: .3rem 0;
    text-align: center;
    overflow: hidden;
    color: var(--bit-ink);
    /* A hairline of the surface between neighbours, so two blocks never read
       as one however close their colours land. */
    box-shadow: inset -1px 0 0 var(--panel);
    position: relative;
}

.bitmap-block.is-field {
    background: var(--bit-colour, var(--bit-1));
    /* The block colours span a lightness band, so the label gets a shadow
       rather than a second ink token per slot. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
}

/* A gap is hatched rather than flat, so it reads as absence rather than as a
   field that happens to have no name. */
.bitmap-block.is-gap {
    color: var(--bit-gap-ink);
    background:
        repeating-linear-gradient(135deg, var(--bit-gap) 0 4px, #1a1b1c 4px 8px);
}

/* A field cut by a line break keeps a square edge on the cut side, so the run
   visibly carries on rather than ending twice. */
.bitmap-block.continues { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.bitmap-block.continued { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.bitmap-block.continued::before {
    content: '\2026';
    position: absolute;
    left: .15rem;
    top: 50%;
    transform: translateY(-50%);
    opacity: .7;
    font-size: .6rem;
}

.bitmap-label {
    display: block;
    padding: 0 .2rem;
    font-size: .68rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.bitmap-span {
    display: block;
    padding: 0 .2rem;
    font-size: .6rem;
    opacity: .82;
    white-space: nowrap;
}

/* A narrow block - a 1-bit flag - has room for neither line, so it keeps the
   colour and gives up the text. The title attribute and the table still name
   it. */
.bitmap-block { container-type: inline-size; }

@container (max-width: 4rem) { .bitmap-span { display: none; } }
@container (max-width: 2rem) { .bitmap-label { display: none; } }

/* Draggable: a field can be picked up in the map and dropped somewhere else in
   the order, which is the same move as dragging its row in the table. */
.bitmap-block.is-field[draggable="true"] { cursor: grab; }
.bitmap-block.is-field.dragging { opacity: .4; cursor: grabbing; }
.bitmap-block.drop-before { box-shadow: inset 3px 0 0 var(--green), inset -1px 0 0 var(--panel); }
.bitmap-block.drop-after  { box-shadow: inset -3px 0 0 var(--green); }

/* The bit scale under the track. One cell per bit, so a tick lines up with
   every boundary in the track above it and a field's edges can be counted off
   rather than estimated. Numbered every four: sixty-four numbers on a line is
   unreadable, and every eight leaves the nibble boundaries - where a packed
   field very often lands - to be guessed at. */

.bitmap-bits {
    margin-top: .1rem;
    height: .85rem;
}

.bitmap-bit {
    flex: 0 0 calc(100% / var(--line-bits, 64));
    box-sizing: border-box;
    min-width: 0;
    position: relative;
    border-left: 1px solid var(--bit-bit-rule);
    height: .25rem;
}

.bitmap-bit.is-nibble { border-left-color: var(--bit-byte-rule); height: .4rem; }
.bitmap-bit.is-byte   { border-left-color: var(--bit-byte-rule); height: .55rem; }

/* The number hangs off its tick rather than sitting in the cell, because a
   cell is 1/64th of the line and the number is wider than that. */
.bitmap-bit i {
    position: absolute;
    top: .45rem;
    left: 0;
    transform: translateX(-50%);
    font-style: normal;
    font-size: .56rem;
    line-height: 1;
    color: var(--muted);
}

.bitmap-bit.is-byte i { color: var(--ink); }

/* The first tick of a line sits at x=0, so a number centred on it would have
   half of itself outside the box and get clipped - which on lines after the
   first means the bit the line starts at is the one number that cannot be
   read. Pin it to the left edge instead. */
.bitmap-bit:first-child i { transform: none; }

.bitmap-empty {
    display: block;
    padding: .6rem 0;
}

.bitmap-key {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    margin-left: .5rem;
    flex-wrap: wrap;
}

.bitmap-swatch {
    display: inline-block;
    width: .7rem;
    height: .7rem;
    border-radius: 2px;
    vertical-align: -1px;
    flex: none;
}

.bitmap-swatch.is-field { background: var(--bit-colour, var(--bit-1)); }
.bitmap-swatch.is-gap {
    background: repeating-linear-gradient(135deg, var(--bit-gap) 0 3px, #1a1b1c 3px 6px);
}

/* --- the structure table --------------------------------------------------
   The same colours as the map, so a block and its row are found from each
   other, and a handle to reorder by. Order is position: the fields are laid
   end to end in the order the rows are in, so moving a row moves everything
   after it. */

.payload-map td { vertical-align: middle; }
.payload-map input[type="number"] { width: 100%; }

.payload-map tr.is-mapped .bitmap-swatch { visibility: visible; }
.payload-map .bitmap-swatch { visibility: hidden; }

.payload-grip {
    cursor: grab;
    color: var(--muted);
    padding: 0 .25rem;
    user-select: none;
    touch-action: none;
}

.payload-grip:hover { color: var(--ink-hover); }
.payload-map tr.dragging { opacity: .45; }
.payload-map tr.drop-before > td { box-shadow: inset 0 3px 0 var(--green); }
.payload-map tr.drop-after  > td { box-shadow: inset 0 -3px 0 var(--green); }

/* The facts table under the list: an address, a length and a byte order. The
   address is a URL with nothing to break on, so it is allowed to wrap mid-word
   rather than push the page sideways on a phone. */


/* Where a field lands, computed rather than typed - so it is text, not an
   input a reader could fight with. */
.payload-at { white-space: nowrap; }

/* --- choosing what stays active (lib/views/active.php) ------------------ */

.active-group { min-width: 0; }
.active-group legend {
    padding: 0 .3rem;
    font-weight: 600;
}
.active-count { margin-left: .5rem; font-weight: 400; }
.active-group input[type="checkbox"]:disabled + label,
.active-group tr:has(input[type="checkbox"]:disabled) { opacity: .6; }
.suspended-notice a { white-space: nowrap; }

/* --- the coming-soon page (lib/views/coming_soon.php) -------------------- */
/* What every visitor sees until the site opens: the wordmark, a line on what
   it is, and where to ask. There is no top bar, since nothing it links to is
   open yet, so the green rail that runs under the bar runs along the top of
   the window instead, and the rest sits in the middle of it. */
.soon-page {
    display: flex;
    min-height: 100vh;
    border-top: 3px solid var(--green);
}

.soon {
    margin: auto;
    max-width: 640px;
    padding: 3rem 16px;
    text-align: center;
}

.soon-brand { font-size: 1.6rem; margin: 0 0 2.5rem; }
.soon-title { font-size: 2.4rem; margin: 0 0 1rem; }
.soon-lead { margin: 0 auto; }

/* --- the contact form (contact.php) -------------------------------------- */
/* The field only a script fills in (see Sensifly\Contact). Moved off the page
   rather than display:none, which the better scripts know to skip; the markup
   keeps it from screen readers and the Tab key. */
.contact-trap {
    position: absolute;
    left: -10000px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}
