/* ============================================================================
   Web fonts: Source Sans Pro (ODS's family) and JetBrains Mono, self-hosted (fonts/README.md). A page that measures
   throughput must not depend on a third party at load time. */
@font-face {
  font-family: "Source Sans Pro";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/source-sans-pro-v23-latin-400.woff2") format("woff2");
}

@font-face {
  font-family: "Source Sans Pro";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/source-sans-pro-v23-latin-600.woff2") format("woff2");
}

/* 700 is ODS's heading weight. The headings come from the Text component rather
   than a token, and its presets all carry 700, so the page has to be able to serve
   it. Same v23 latin static as the other two. */
@font-face {
  font-family: "Source Sans Pro";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/source-sans-pro-v23-latin-700.woff2") format("woff2");
}

/* One variable file covers every weight we use: 400 for commands, hostnames and
   table cells, 600 for the selected server's host. Source Code Pro needed one
   static file per weight. */
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-v24-latin-400-800.woff2") format("woff2");
}

/* ============================================================================
   OVHcloud Design System (ODS) tokens: the canonical light-theme values, copied verbatim from the ODS docs and
   never redefined per theme. They are the brand and not a palette choice. */
:root {
  --ods-brand: #000e9c;
  --ods-text: #4d5592;
  --ods-heading: #00185e;
  --ods-text-disabled: #808080;

  --ods-anchor: #0050d7;
  --ods-anchor-hover: #002dbe;
  --ods-anchor-visited: #000e9c;

  --ods-bg: #fff;
  --ods-bg-readonly: #f2f2f2;
  --ods-bg-disabled: #e6e6e6;
  --ods-border-disabled: #cccccc;
  --ods-input-border: #b3b3b3;

  --ods-critical: #bf0020;

  --ods-outline-color: #000e9c;
  --ods-outline-width: 2px;
  --ods-outline-style: solid;
  --ods-outline-offset: 2px;

  --ods-shadow: 0 2px 8px rgba(0, 14, 156, 0.2);

  --ods-radius: 8px;
  --ods-border-width: 1px;
  --ods-gap: 8px;
  --ods-duration: 300ms;

  --ods-font: "Source Sans Pro", "Trebuchet MS", arial, "Segoe UI", sans-serif;
  --ods-font-code: "Source Code Pro", arial;
}

/* ----------------------------------------------------------------------------
   Page tokens, light, mapped onto ODS values. Only these switch per theme. Two are derived (--surface-sunken reuses
   the readonly grey, --border the input border), because ODS documents no equivalent. */
:root {
  --surface: var(--ods-bg);
  --surface-sunken: var(--ods-bg-readonly);
  --surface-raised: var(--ods-bg);
  --surface-hover: #f7f7fa;
  --surface-code: var(--ods-bg-readonly);

  --text: var(--ods-text);
  --text-heading: var(--ods-heading);
  --text-muted: #6a719e;

  --link: var(--ods-anchor);
  --link-hover: var(--ods-anchor-hover);

  --border: var(--ods-border-disabled);
  --border-strong: var(--ods-input-border);
  /* neutral-100 is the value ODS draws table cell borders and the neutral tag stroke
     with, one step quieter than border-disabled (neutral-200). */
  --border-soft: var(--ods-bg-disabled);

  --header-bg: var(--ods-brand);
  --header-fg: #ffffff;
  --header-hover: rgba(255, 255, 255, 0.14);
  --header-border: rgba(255, 255, 255, 0.35);
  --logo-filter: brightness(0) invert(1);

  --accent-soft: #e8ecff;
  --accent-soft-border: #c3cdff;

  --elevation: var(--ods-shadow);
  --elevation-hover: 0 4px 16px rgba(0, 14, 156, 0.28);

        /* One deliberate deviation, declared in the page block so the ODS block stays verbatim: everything
           monospace is JetBrains Mono, which stays legible at the 11-13px those blocks run at, and one variable
           file replaces a static face per weight. */
  --ods-font-code: "JetBrains Mono", ui-monospace, "SF Mono", menlo, monospace;
}

/* ----------------------------------------------------------------------------
   Page tokens, dark. This is an EXTRAPOLATION and not ODS, since no dark theme is documented: the brand hue
   (234deg) is kept and the blues are lifted in lightness to hold contrast. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface: #141829;
    --surface-sunken: #0c0f1c;
    --surface-raised: #1b2039;
    --surface-hover: #222846;
    --surface-code: #0c0f1c;

    --text: #c5cae6;
    --text-heading: #eef0fa;
    --text-muted: #9298bd;

    --link: #8aacff;
    --link-hover: #b3c8ff;

    --border: #2c3357;
    --border-strong: #3d4676;
    --border-soft: #262c52;

    --header-bg: var(--ods-brand);
    --header-fg: #ffffff;
    --header-hover: rgba(255, 255, 255, 0.14);
    --header-border: rgba(255, 255, 255, 0.35);
    --logo-filter: brightness(0) invert(1);

    --accent-soft: #1d2450;
    --accent-soft-border: #313b73;

    --elevation: 0 2px 8px rgba(0, 0, 0, 0.5);
    --elevation-hover: 0 4px 16px rgba(0, 0, 0, 0.6);
  }
}

:root[data-theme="dark"] {
  --surface: #141829;
  --surface-sunken: #0c0f1c;
  --surface-raised: #1b2039;
  --surface-hover: #222846;
  --surface-code: #0c0f1c;

  --text: #c5cae6;
  --text-heading: #eef0fa;
  --text-muted: #9298bd;

  --link: #8aacff;
  --link-hover: #b3c8ff;

  --border: #2c3357;
  --border-strong: #3d4676;
  --border-soft: #262c52;

  --header-bg: var(--ods-brand);
  --header-fg: #ffffff;
  --header-hover: rgba(255, 255, 255, 0.14);
  --header-border: rgba(255, 255, 255, 0.35);
  --logo-filter: brightness(0) invert(1);

  --accent-soft: #1d2450;
  --accent-soft-border: #313b73;

  --elevation: 0 2px 8px rgba(0, 0, 0, 0.5);
  --elevation-hover: 0 4px 16px rgba(0, 0, 0, 0.6);
}

/* Light forced: restate the light mapping so the toggle wins in both ways. */
:root[data-theme="light"] {
  --surface: var(--ods-bg);
  --surface-sunken: var(--ods-bg-readonly);
  --surface-raised: var(--ods-bg);
  --surface-hover: #f7f7fa;
  --surface-code: var(--ods-bg-readonly);

  --text: var(--ods-text);
  --text-heading: var(--ods-heading);
  --text-muted: #6a719e;

  --link: var(--ods-anchor);
  --link-hover: var(--ods-anchor-hover);

  --border: var(--ods-border-disabled);
  --border-strong: var(--ods-input-border);
  --border-soft: var(--ods-bg-disabled);

  --accent-soft: #e8ecff;
  --accent-soft-border: #c3cdff;

  --elevation: var(--ods-shadow);
  --elevation-hover: 0 4px 16px rgba(0, 14, 156, 0.28);
}

/* ============================================================================ */
* { margin: 0; padding: 0; box-sizing: border-box; }

/* Two columns: a fixed sidebar and the content. minmax(0, 1fr) rather than a bare
   1fr, because a wide child (a long command, a wide SVG) refuses to shrink and
   pushes the whole page into a horizontal scroll. */
body {
  font-family: var(--ods-font);
  background: var(--surface-sunken);
  color: var(--text);
  line-height: 1.5;
  min-height: 100vh;
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
}

.content { min-width: 0; }

/* Horizontal gutter of the content column. It is a token and not a literal, because the column's padding has to
   stay in step with the cards inside it: the hero and the .panel below are both cards, and the gutter moves them
   together. One value, one place to change, including the narrow-screen override below. */
:root { --gutter: 24px; }

/* The column pays the gutter, not the blocks inside it. It used to sit on .section and .footer only, which left
   .hero, itself a card with no side gutter, running edge to edge while the .panel below was inset by 24px. The two
   cards shared neither width nor edges, and under 1200px the hero touched the viewport while the test did not. */
main { padding: 0 var(--gutter); }

/* ODS: focus is a 2px solid brand outline at 2px offset on every interactive
   element, and the mixin adds nothing else, so there is no radius of its own because
   modern browsers already follow each element's corners. Never removed. */
a:focus-visible,
button:focus-visible,
summary:focus-visible {
  outline: var(--ods-outline-width) var(--ods-outline-style) var(--ods-outline-color);
  outline-offset: var(--ods-outline-offset);
}

:root[data-theme="dark"] a:focus-visible,
:root[data-theme="dark"] button:focus-visible,
:root[data-theme="dark"] summary:focus-visible { outline-color: #8aacff; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) a:focus-visible,
  :root:not([data-theme="light"]) button:focus-visible,
  :root:not([data-theme="light"]) summary:focus-visible { outline-color: #8aacff; }
}

.sidebar a:focus-visible,
.sidebar button:focus-visible,
.sidebar summary:focus-visible { outline-color: #ffffff; }

/* ODS's Text component defines the heading presets and no font-size token. This page
   climbs levels 2/4/6 (28/20/16px) to keep the hero and section titles compact.
   line-height stays ours (1.25, budgeted against the frame below). */
h1, h2, h3 { color: var(--text-heading); font-weight: 700; line-height: 1.25; }
h1 { font-size: 28px; }
h2 { font-size: 20px; }
h3 { font-size: 16px; }

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

code { font-family: var(--ods-font-code); }

/* Bypass block (WCAG 2.4.1). The sidebar's logo, theme toggle, filter, ten servers and the closest-server button
   come first in the tab order, so on a desktop the test itself is fourteen tabs away. Off screen until it takes
   focus, which is the only moment it is useful, and wearing the chrome pair so it reads over whatever it lands on. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
}
.skip-link:focus {
  left: 8px;
  top: 8px;
  padding: 8px 14px;
  background: var(--header-bg, #0b1030);
  color: #ffffff;
  border-radius: calc(var(--ods-radius) / 2);
  font-weight: 600;
  text-decoration: none;
  outline: var(--ods-outline-width) solid #8aacff;
  outline-offset: var(--ods-outline-offset);
}
/* The skip target is a container and not a control: it takes focus (tabindex -1)
   without painting a ring around the whole page. */
main:focus { outline: none; }

/* ============================================================================ */
.sidebar {
  background: var(--ods-brand);
  color: #ffffff;
  padding: 20px 16px;
  display: flex;
  flex-direction: column;
  gap: 24px;
  /* Its own scroll: the fleet list is long and must not drag the page with it. */
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
    /* The sidebar itself does not scroll: the logo, theme button and section links
       stay put and only the fleet list moves. Otherwise anything below the ten servers
       falls under the fold on a shorter window. */
  overflow: hidden;
}

.sidebar-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* Logo and page name as one block, so the theme toggle is always the last thing in the row and `space-between` pins
   it to the sidebar's right edge. */
.sidebar-identity {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  min-width: 0;
}

.brand { display: block; }
.logo { height: 30px; filter: var(--logo-filter); display: block; }

/* Small subtitle under the OVHcloud logo, in the sidebar. Its own margin and padding are gone, because it is now a
   child of .sidebar-identity and the 6px gap there spaces it from the logo. The old -16px margin only existed to
   cancel the sidebar's 24px gap when it was a sibling. */
.sidebar-subtitle {
  font-size: 13px;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.8);
  margin: 0;
  padding: 0;
}

.side-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.65);
  margin-bottom: 8px;
  padding: 0 10px;
}

.servers {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  /* Each region is its own collapsible group now, so the scrollable box is the whole
     nav and not the former single flat ul. */
  overflow-y: auto;
}

.server-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* One details per region; see the template's speedtest_site_regions loop. */
.region {
  display: block;
}

.region-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  padding: 8px 10px 4px;
  cursor: pointer;
  border-radius: calc(var(--ods-radius) / 2);
  user-select: none;
  transition: color var(--ods-duration);
  /* The twisty is the inline SVG chevron, so the native marker goes: both the
     standards way and the WebKit way. */
  list-style: none;
}

.region-label::-webkit-details-marker { display: none; }

.region-label:hover { color: rgba(255, 255, 255, 0.85); }

.region-chev {
  flex: none;
  transition: transform var(--ods-duration);
}

.region[open] > .region-label .region-chev { transform: rotate(90deg); }

/* A datacenter split into availability zones (type=3az in the inventory): one row that */
.dc-label {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px;
  border-radius: calc(var(--ods-radius) / 2);
  color: #ffffff;
  cursor: pointer;
  user-select: none;
  transition: background var(--ods-duration);
}

.dc-label::-webkit-details-marker { display: none; }

.dc-label:hover { background: var(--header-hover); }

.dc-chev {
  margin-left: auto;
  flex: none;
  transition: transform var(--ods-duration);
}

.dc[open] > .dc-label .dc-chev { transform: rotate(90deg); }

.dc > .server-list {
  margin-left: 10px;
  padding-left: 8px;
  border-left: 1px solid rgba(255, 255, 255, 0.12);
}

.server {
  display: block;
  padding: 7px 10px;
  border-radius: calc(var(--ods-radius) / 2);
  text-decoration: none;
  color: #ffffff;
  transition: background var(--ods-duration);
}

.server:hover { background: var(--header-hover); text-decoration: none; color: #ffffff; }

/* The server being viewed. aria-current carries the same meaning for screen readers,
   so the styling is keyed on it rather than on a spare class. */
.server[aria-current="page"] {
  background: rgba(255, 255, 255, 0.18);
  font-weight: 600;
}

.server-site { display: block; font-size: 14px; }

/* National flag rendered after the site name (see speedtest_site_flags in
   roles/speedtest/defaults/main.yml). */
.server-flag { margin-left: 4px; }

/* The row shows the region name and the capacity only: a 34-character public name cannot fit this column, and
   wrapping it read as two different machines. */

/* The server's access-link capacity, from link_capacity in the inventory, a static fact from the order. Rendered
   only when the host declares a value: no fact, no chip, never a wrong one. */
.server { position: relative; padding-right: 70px; }
.server-cap {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  box-sizing: border-box;
  min-height: 18px;
  padding: 3px 6px;
  line-height: 1;
  font-family: var(--ods-font-code);
  font-size: 11px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--header-border);
  border-radius: calc(var(--ods-radius) / 2);
  white-space: nowrap;
}

.theme-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: var(--ods-border-width) solid var(--header-border);
  border-radius: calc(var(--ods-radius) / 2);
  color: #ffffff;
  font-family: var(--ods-font);
  font-size: 13px;
  padding: 6px;
  cursor: pointer;
  line-height: 0;
  transition: background var(--ods-duration);
}

.theme-toggle .icon {
  display: inline-block;
}

.theme-toggle:hover { background: var(--header-hover); }

/* ============================================================================ */
.hero {
  max-width: 1200px;
  margin: 32px auto 0;
  padding: 24px;
  background: var(--surface-raised);
  border-radius: var(--ods-radius);
  box-shadow: var(--elevation);
}

.subtitle { color: var(--text-muted); font-size: 15px; margin-top: 8px; }
/* The two hero paragraphs read as one block: the second follows the first at a
   hair's distance instead of the full paragraph margin. */
.subtitle + .subtitle { margin-top: 2px; }
.subtitle code { color: var(--text); font-size: 14px; }
/* The city in the hero's "you're currently connected to X" line. It replaced a
   <code> hostname there, since a sentence is no place for a 40-character FQDN (§10),
   so it keeps the emphasis the monospace gave it: weight and ink, not a font. */
.subtitle strong { color: var(--text); font-weight: 600; }

/* One compact row inside the hero. It sits above the test, so every pixel it takes
   comes out of the iframe's height budget below: keep it to a single line on a
   normal window. */
.connbar {
  margin-top: 16px;
  padding-top: 14px;
  border-top: var(--ods-border-width) solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 6px 28px;
}

/* `position: relative` is the containing block for the item's floating tooltip
   (.conn-tip). The tip is positioned against the item, so it lines up with the label
   it belongs to and never adds to the item's height. */
.conn-item { display: flex; align-items: baseline; gap: 8px; position: relative; }

.connbar dt {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  white-space: nowrap;
}

.connbar dd {
  margin: 0;
  font-family: var(--ods-font-code);
  font-size: 13px;
  color: var(--text);
  /* An IPv6 address plus a cipher name has no space to break on. */
  overflow-wrap: anywhere;
}

/* The "Fallback to IPv4" pill, shown only while the visitor is on IPv6: a plain link to this server's A-only
   ipv4.<dc> alias, the only way to force a browser back onto IPv4. Deliberately tiny, and it sits with the "IP
   version" value it belongs to rather than as an item of its own. */
.conn-v4 {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  font-family: var(--ods-font-code);
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-muted);
  text-decoration: none;
  border: var(--ods-border-width) solid var(--border);
  border-radius: 999px;
  background: transparent;
  white-space: nowrap;
  transition: color var(--ods-duration), border-color var(--ods-duration);
}
.conn-v4:hover {
  color: var(--text);
  border-color: var(--text-muted);
}
/* `display: inline-block` above would otherwise beat the `hidden` attribute, because author styles outrank the UA's
   `[hidden] { display: none }`, so a visitor on IPv4 would still see the pill. This restores the attribute's
   effect; the pill is only ever shown for IPv6 visitors. */
.conn-v4[hidden] { display: none; }

/* Same trap as the pill above: `.conn-item` declares `display: flex`, which outranks the UA's `[hidden] { display:
   none }`. The "Protocol" and "Network" items ship hidden and js/main.js reveals them once their values exist, so
   they would sit in the row as empty labels otherwise. */
.conn-item[hidden] { display: none; }

/* The (i) that reveals the reverse DNS or the cipher suite, and its tooltip. The address line used to print the PTR
   between brackets, which doubled the width of the widest item in a row that has a one-line budget. The value is
   now one hover or keyboard focus away and the row stays compact. */
.conn-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  margin-left: 6px;
  padding: 0;
  font-family: var(--ods-font-code);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  color: var(--text-muted);
  border: var(--ods-border-width) solid var(--border);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
  vertical-align: middle;
  transition: color var(--ods-duration), border-color var(--ods-duration);
}
.conn-info:hover,
.conn-info:focus-visible {
  color: var(--text);
  border-color: var(--text-muted);
}
/* Same trap as the link: an explicit `display` outranks the UA's
   `[hidden] { display: none }`, so a visitor with no PTR or no cipher would get an
   (i) that opens an empty tooltip. */
.conn-info[hidden] { display: none; }

.conn-tip {
        /* Floating, below its trigger: the row keeps its one-line budget whether the tip */
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 20;
  padding: 4px 8px;
  font-family: var(--ods-font-code);
  font-size: 12px;
  line-height: 1.4;
  color: var(--text);
  background: var(--surface);
  border: var(--ods-border-width) solid var(--border);
  border-radius: calc(var(--ods-radius) / 2);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
    /* Sized to its content and bounded by the viewport rather than by its parent: a cipher suite is 20+ characters
       with no break opportunity and the containing block is one line wide, so `max-width: 100%` clipped it, and
       with nowrap it ran off the card. It breaks anywhere instead. */
  width: max-content;
  max-width: min(90vw, 32rem);
  overflow-wrap: anywhere;
    /* Out of the pointer's way. The tip overlays whatever sits below the row (the test
       panel) and must not swallow a click meant for the page. It also cannot be hovered
       into: moving off the item closes it, which is what a hover/focus tip should do. */
  pointer-events: none;
}
.conn-tip[hidden] { display: none; }

/* Label for screen readers only: the (i) shows a glyph and not text. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Attribution link for the IP-to-ASN database in the footer. Free ASN databases are licensed on visible
   attribution, so the credit travels with the data as a real link. It renders from speedtest_isp_source_* and is
   present only when a source is configured: no source, no credit, no data. */
.footer-attr {
  margin-top: 6px;
  font-size: 11px;
  color: var(--text-muted);
}
.footer-attr a { color: inherit; text-decoration: underline; }
.footer-attr a:hover { color: var(--text); }

/* ============================================================================ */
/* No side padding of its own: the column owns the gutter (see main above), so this
   box and .hero are the same width and the cards inside them line up. */
.section { max-width: 1200px; margin: 32px auto 0; }
.section > h2 { margin-bottom: 12px; }

.panel {
  background: var(--surface-raised);
  border-radius: var(--ods-radius);
  padding: 16px;
  box-shadow: var(--elevation);
}

/* Kept here rather than in a style="" attribute, because an inline style outranks any stylesheet rule and the
   responsive heights below could never apply. The height is tied to the viewport, since a hard 420px pushed the
   gauge past the fold on a short window, and the subtrahend is the chrome above it. */
.panel iframe {
  display: block;
  width: 100%;
  height: clamp(410px, calc(100vh - 330px), 620px);
  height: clamp(410px, calc(100dvh - 330px), 620px);
  border: none;
  background: var(--surface-raised);
}

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

/* The file cards carry a copyable command, and a 280px track leaves the box too narrow for it: `curl -O
   https://<host>/files/1Mb.dat` broke after the host, so the card was three lines tall for two lines of content. */
.grid--files { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }

/* The one thing the file page never said: -O writes the file to disk, and the 10 GiB
   one is a footgun. Quiet, above the list, where the commands are read. */
.files-hint {
  margin: 0 0 16px;
  font-size: 13px;
  color: var(--text-muted);
}
.files-hint code { font-size: 12px; }

/* Desktop: the "Other tools" cards are always shown and the mobile server <select> is
   hidden, because the filter and region list are the picker there. Both flip inside
   the narrow-width block below. */
.tools-toggle { display: none; }
.server-select-mobile { display: none; }

/* The phone picker: a trigger that stands in for the native control (revealed by js/main.js) plus a panel holding
   the SAME filter and region list the desktop sidebar shows, so one rendering keeps the two lists from drifting.
   display: contents keeps the wrapper out of the desktop layout entirely. */
.server-picker-btn { display: none; }
.server-picker-panel { display: contents; }

.card {
  background: var(--surface-raised);
  border-radius: var(--ods-radius);
  padding: 20px;
  text-decoration: none;
  color: inherit;
  box-shadow: var(--elevation);
  transition: box-shadow var(--ods-duration), background var(--ods-duration);
        /* A column, so a card's blocks stack in a predictable order. It is no longer used to pin the last block to
           the bottom: the three cards share a height, and pushing the shorter cards' last block down opened a hole
           that read as two disconnected halves. Unused height stays at the bottom edge. */
  display: flex;
  flex-direction: column;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.card p { font-size: 14px; color: var(--text-muted); }

/* ODS badge, sm: min-height 20px, 12px text with line-height 1, radius calc(radius/2) = 4px and no stroke, because
   ODS badges are borderless chips. The colour pair stays on our page tokens (--accent-soft/--link, the
   'information'-badge idea) rather than the raw information-100/900 pair, so the dark extrapolation keeps working. */
.badge {
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  min-height: 20px;
  line-height: 1;
  font-size: 12px;
  font-weight: 600;
  padding: 4px;
  border-radius: calc(var(--ods-radius) / 2);
  background: var(--accent-soft);
  color: var(--link);
  white-space: nowrap;
}

.cmd {
  margin-top: 12px;
  font-family: var(--ods-font-code);
  font-size: 13px;
  background: var(--surface-code);
  padding: 6px 6px 6px 12px;
  border-radius: calc(var(--ods-radius) / 2);
  color: var(--text);
  border: var(--ods-border-width) solid var(--border);
  display: flex;
        /* Centre the command in the box: the box is as tall as its tallest child (the 38px
           copy button on touch), so a top-aligned command left a band of empty box under a
           one-line command. Centred, it still reads correctly when it wraps. */
  align-items: center;
  gap: 8px;
}

/* The command text wraps instead of being clipped or scrollable, so the full command
   is always readable. min-width:0 lets the flex child shrink inside the grid track, so
   a long URL never widens the page. */
.cmd-text {
  flex: 1 1 auto;
  min-width: 0;
    /* break-word and not `anywhere`: break only at a natural boundary, a space or a
       URL's `/`, and never mid-token. `anywhere` is what split the curl command as
       `…/fi` / `les/100Mb.dat`, which read as two fragments. */
  overflow-wrap: break-word;
  white-space: normal;
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

.copy-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: var(--ods-border-width) solid var(--border);
  border-radius: calc(var(--ods-radius) / 2);
  background: var(--surface-sunken);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--ods-duration), border-color var(--ods-duration), background var(--ods-duration);
}

.copy-btn:hover {
  color: var(--link);
  border-color: var(--link);
  background: var(--surface-hover);
}

.copy-btn .icon-check { display: none; }

/* Confirmed: the copy button flips to a check. The green is ODS success-500
   (#2b8000) and not an invented one. */
.copy-btn.copied {
  color: #2b8000;
  border-color: #2b8000;
  background: var(--surface-hover);
}

.copy-btn.copied .icon-copy { display: none; }
.copy-btn.copied .icon-check { display: block; }

/* The way back from /files/ to the speed test. An ODS-style ghost button, the same
   shape as .closest-btn in the sidebar, on the page's own tokens because this one sits
   on a card rather than on the navy bar. */
.back-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: calc(var(--ods-radius) / 2);
  background: none;
  color: var(--link);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: background var(--ods-duration), border-color var(--ods-duration);
}
.back-btn:hover {
  background: var(--surface-hover);
  border-color: var(--link);
  text-decoration: none;
}
.back-btn .icon-back { flex: none; }

/* The download card's pointer to the rest of the sizes. Quiet, like .ports: a footnote
   to the command above it, not a second description. */
.card .card-more {
  margin-top: 12px;
  font-size: 13px;
  color: var(--text-muted);
}

.card .ports {
  margin-top: 6px;
        /* One step below the card description (14px) and the tags (12px): a footnote and
           not a headline. The selector is `.card .ports` and not `.ports`, or `.card p`
           wins the cascade and leaves this at 14px. */
  font-size: 11px;
  color: var(--text-muted);
  font-family: var(--ods-font-code);
}

.tags { margin-top: 12px; display: flex; flex-wrap: wrap; gap: var(--ods-gap); }

/* ODS tag, md: 24px tall, 14px/1 text, the 16px radius and twice the theme border
   width, since the tag is defined by its 2px stroke. Keywords off themed page tokens
   rather than ODS's light-theme hard values, and the font stays the code face. */
.tag {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 1px 8px;
  font-family: var(--ods-font-code);
  font-size: 14px;
  line-height: 1;
  background: transparent;
  border: 2px solid var(--border-soft);
  border-radius: calc(var(--ods-radius) * 2);
  color: var(--text);
}

/* ============================================================================ */
/* Nothing in these pages is a <table> any more, so the old table styles are gone. If
   one comes back, the ODS Table spec is the reference
   (packages/ods-react/src/style/_table.scss). */
.footer {
  max-width: 1200px;
  margin: 40px auto 32px;
  color: var(--text-muted);
  font-size: 13px;
}

/* A URL has no break opportunity, so a 41-character one overflows the body below
   about 344px and gives the whole page a horizontal scrollbar. */
.footer code { font-size: 13px; overflow-wrap: anywhere; }

/* ============================================================================ */

/* Below this a 248px column costs more than it gives: the sidebar unfolds into a bar
   across the top and the fleet list becomes a horizontal scroller, so ten servers do
   not push the content off the screen. */
@media (max-width: 900px) {
  body { grid-template-columns: minmax(0, 1fr); }

  .sidebar {
    position: static;
    height: auto;
    overflow: visible;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
  }

    /* The bar's first row: logo and page name together on the left, theme toggle against
       the right edge. flex-basis 100% is what gives the row the bar's full width; shrink
       wrapped, space-between has nothing to push against. */
  .sidebar-top { flex: 1 1 100%; }
  /* On the bar the two sit side by side, close together, instead of stacked. */
  .sidebar-identity { flex-direction: row; align-items: center; gap: 10px; }
    /* The region labels stay on small screens even though the "Tools"/"Servers" captions
       go: they are the taps that open the collapsed groups there. The subtitle stays too,
       since it names the page next to the logo. */
  .side-label { display: none; }
    /* overflow: visible is load-bearing. The nav scrolls on desktop because the list is long, and a scroll
       container CLIPS the panel that hangs below it: the panel was laid out at the right place and painted nowhere
       (measured: absent from elementsFromPoint, present in the layout). */
  .servers { flex: 1 1 100%; min-width: 0; position: relative; overflow: visible; }

    /* A phone gets a picker instead of the desktop filter and region list. The trigger */
  .server-select-mobile { display: block; margin: 0 0 6px; }
  .server-select-mobile select {
    width: 100%;
    font-family: var(--ods-font);
        font-size: 16px; /* iOS: no auto-zoom on focus */
            /* Two traps. The text was chrome white while the ground read */
    color: var(--header-fg);
    background: var(--header-bg, #0b1030);
    color-scheme: dark;
    border: 1px solid var(--header-border);
    border-radius: calc(var(--ods-radius) / 2);
    padding: 8px 10px;
  }
    /* Engines that paint the popup's own rows from the <option>/<optgroup> elements take
       the same chrome pair. Safari builds its popup natively, ignores both rules, and
       stays legible. */
  .server-select-mobile select option,
  .server-select-mobile select optgroup {
    color: #ffffff;
    background: var(--header-bg, #0b1030);
  }

  /* The trigger carries the chrome pair the select had, so the swap is invisible. The
     chip rides in flow here, where a list row pins it absolute. */
  .server-picker-btn {    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin: 0 0 6px;
    padding: 8px 10px;
    font-family: var(--ods-font);
    font-size: 16px; /* iOS: no auto-zoom on focus */
    color: var(--header-fg);
    background: var(--header-bg, #0b1030);
    border: 1px solid var(--header-border);
    border-radius: calc(var(--ods-radius) / 2);
    text-align: left;
    cursor: pointer;
    transition: background var(--ods-duration);
  }
  .server-picker-btn[hidden] { display: none; }
    /* js/main.js adds picker-on when it wires the trigger, and the native control steps aside there. A class and
       not the [hidden] attribute, because the rule above sets display: block on this label and an author rule beats
       the UA's [hidden]: so `label. */
  .servers.picker-on .server-select-mobile { display: none; }  .server-picker-btn:hover { background: var(--header-hover); }
  .server-picker-btn .server-site { font-size: 15px; }
  .server-picker-btn .server-cap { position: static; transform: none; margin-left: auto; }
  .server-picker-chev { flex: none; }

    /* The panel: the sidebar's own list, dropped under the bar and over the content. Only
       the wrapper turns on, so the filter, the region groups and the rows inside it keep
       every desktop rule, including the filter's behaviour. */
  .servers .server-picker-panel { display: none; }
  .servers.picker-open .server-picker-panel {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% + 6px);
    z-index: 40;
    max-height: min(70vh, 480px);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: 10px;
    background: var(--header-bg, #0b1030);
    border: 1px solid var(--header-border);
    border-radius: var(--ods-radius);
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  }

  h1 { font-size: 22px; }
  h2 { font-size: 18px; }
    /* minmax(0, 1fr) and not a bare 1fr, because a grid item's automatic minimum size is its min-content: long
       content (a command, a table, a URL) must not inflate the single track past the viewport and give phones a
       horizontal scrollbar. body uses minmax(0, 1fr) for the same reason. */
  .grid { grid-template-columns: minmax(0, 1fr); }

  /* The "Other tools" cards (curl, iperf3, network diagnostics) are desktop commands a
     phone cannot run, so they fold behind a small toggle. */
  .tools-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0 0 12px;
    padding: 6px 10px;
        /* Page tokens and not the chrome pair. The toggle sits in the content column (--surface-raised over
           --surface), where the header tokens are the navy sidebar's. */
    border: 1px solid var(--border);
    border-radius: calc(var(--ods-radius) / 2);
    background: var(--surface-raised);
    color: var(--text);
    font-family: inherit;
    font-size: 12px;
    cursor: pointer;
    transition: background var(--ods-duration);
  }
  .tools-toggle:hover { background: var(--surface-hover); }
  .tools-grid:not(.tools-open) { display: none; }
}

/* OpenSpeedTest picks its .Desktop or .Mobile layout from `orientation`, evaluated against the *iframe's* box
   rather than the phone's. A short, full-width frame reads as landscape and would serve the desktop UI crushed onto
   a phone, so the frame is forced portrait here. */
@media (max-width: 600px) {
  .subtitle-lead { display: none; }
  .section { margin-top: 20px; }
  .section > h2 { margin-bottom: 8px; }
  .panel { padding: 12px; }
  .panel iframe {
        /* The floor is about the FOLD and not about the vendor's 300px rule. Upstream hides */
    height: clamp(410px, calc(100dvh - 440px), 620px);
    width: auto;
    max-width: 100%;
    aspect-ratio: 3 / 4;
    margin-inline: auto;
  }
}

@media (max-width: 480px) {
  .sidebar { padding: 10px 12px; }
  .hero { padding: 16px; margin-top: 16px; }
  /* One override moves every block in the column, cards included. */
  :root { --gutter: 12px; }
  h1 { font-size: 20px; }
  .cmd { font-size: 12px; }
}

/* ---------------------------------------------------------------------------
   Short viewports: a window dragged short, or a phone in landscape, has the same content in far less room. */
@media (max-height: 620px) {
  .panel iframe { height: clamp(410px, calc(100dvh - 240px), 480px); }
  .hero { margin-top: 16px; padding: 16px; }
  h1 { font-size: 22px; }
  .section { margin-top: 20px; }
  .section > h2 { margin-bottom: 8px; }
  .panel { padding: 10px; }
  .footer { margin: 24px auto 16px; }
}

@media (max-height: 460px) {
  .panel iframe { height: clamp(330px, calc(100dvh - 200px), 410px); }
  .hero { margin-top: 12px; padding: 12px; }
  h1 { font-size: 20px; }
  .subtitle { font-size: 13px; margin-top: 4px; }
  .connbar { margin-top: 10px; padding-top: 10px; gap: 4px 18px; }
  .section { margin-top: 16px; }
  /* In the stacked layout the bar is in flow and scrolls away on its own. */
  .sidebar { position: static; height: auto; }
}

/* Auto server selection spinner, shown while probing latency to all servers */
.auto-select-spinner {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  z-index: 1000;
}

.spinner-content {
  text-align: center;
}

.spinner-ring {
  width: 48px;
  height: 48px;
  border: 4px solid var(--border);
    /* --ods-anchor and not --ods-brand: the ring spins on --surface, and the brand navy is #000e9c in both skins (a
       brand token, never re-themed), which measured a hair over 1.5:1 on the dark card, an invisible ring. --link
       is the one blue this stylesheet does theme. */
  border-top-color: var(--link);
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto 16px;
}

/* The overlay covers the page for as long as the probe takes, a couple of seconds on the far ends of the fleet, so
   a spinning ring is exactly the motion prefers-reduced-motion asks us to drop. The overlay itself stays, since it
   is also the "do not navigate away yet" signal; it just stops spinning. */
@media (prefers-reduced-motion: reduce) {
  .spinner-ring { animation: none; border-top-color: var(--link); }
  .region-chev { transition: none; }
}

/* --------------------------------------------------------------------------- */
.banner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  max-width: 1200px;
  margin: 0 0 20px;
  padding: 12px 14px;
  border: 1px solid var(--accent-soft-border);
  border-left: 3px solid var(--link);
  border-radius: var(--ods-radius);
  background: var(--accent-soft);
  color: var(--text);
  font-size: 14px;
}

.banner[hidden] { display: none; }
.banner-icon { flex: none; margin-top: 2px; color: var(--link); }
.banner-text { flex: 1 1 auto; }
.banner-text strong { color: var(--text-heading); }
.banner-from { color: var(--text-muted); }
.banner-close {
  flex: none;
  border: 0;
  background: none;
  color: var(--text-muted);
  font-size: 20px;
  line-height: 1;
  padding: 0 4px;
  cursor: pointer;
  border-radius: calc(var(--ods-radius) / 2);
}
.banner-close:hover { color: var(--text); background: var(--surface-hover); }

.server-search {
  width: 100%;
  margin: 6px 0 2px;
  padding: 7px 9px;
    /* ODS form element: 1px neutral stroke and a 32px minimum height (--ods-theme-input-*). The border colour is an
       extension, because this input sits on the brand navy where neutral-300 is invisible, so it takes the chrome
       stroke, and the focus outline colour is the themed link blue for the same reason. */
  min-height: 32px;
  border: 1px solid var(--header-border);
  border-radius: calc(var(--ods-radius) / 2);
  background: rgba(0, 0, 0, 0.18);
  color: #ffffff;
  font-family: inherit;
  font-size: 11.5px;
}

.server-search::placeholder { color: rgba(255, 255, 255, 0.6); }

/* Focus ring, drawn INSIDE the field: the input is width: 100% and its scrolling box (.servers has overflow-y:
   auto, which computes overflow-x to auto as well) clips anything that sticks out, so the usual 2px-offset outline
   was cut on the left and right and read as a doubled edge. */
.server-search:focus-visible {
  outline: var(--ods-outline-width) solid #8aacff;
  outline-offset: calc(-1 * var(--ods-outline-width));
}

/* The filter hides the non-matching <li> and whole regions, and the empties are the
   only feedback a text input can give. Same [hidden] caveat as above. */
.server-list li[hidden],
.region[hidden],
.dc[hidden],
.server-search-empty[hidden] { display: none; }

.server-search-empty {
  margin: 4px 10px 0;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.7);
}

.closest { flex: none; padding-top: 4px; }

.closest-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 7px 10px;
  border: 1px solid var(--header-border);
  border-radius: calc(var(--ods-radius) / 2);
  background: none;
  color: #ffffff;
  font-family: inherit;
  font-size: 12px;
  text-align: left;
  cursor: pointer;
  transition: background var(--ods-duration);
}

/* Rendered hidden until js/main.js runs: with no script the button would be a promise
   nothing keeps. */
.closest-btn[hidden] { display: none; }
.closest-btn:hover:not(:disabled) { background: var(--header-hover); }
/* Disabled while a measurement runs, and it must not look broken there: the radar turning is the whole progress
   signal now, so the control keeps its ink and only the cursor changes. The 0. */
.closest-btn:disabled { opacity: 1; cursor: progress; }
.closest-icon { flex: none; }

/* Measuring: the radar in the button turns, and that is the whole progress signal — the
   card below stays free for the verdict (AGENTS.md §10). It is the button the visitor
   just pressed, so the motion is explained by the gesture that started it. */
.closest-btn.closest-busy { cursor: progress; }
.closest-btn.closest-busy .closest-icon { animation: closest-scan 1.2s linear infinite; }

@keyframes closest-scan { to { transform: rotate(360deg); } }

/* Motion is what prefers-reduced-motion asks us to drop, and the button still reads as
   busy: it is disabled, and the progress is announced in the live region. */
@media (prefers-reduced-motion: reduce) {
  .closest-btn.closest-busy .closest-icon { animation: none; }
}

.closest-result {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 6px 10px 0;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.75);
  min-height: 1em; /* the aria-live line must not move the list when it speaks */
}
.closest-result[hidden] { display: none; }
.closest-result-body { flex: 1 1 auto; min-width: 0; }
.closest-result-text { display: block; min-width: 0; }

/* The two tones are cards: the glyph, the sentence, the latency on its own line and, for an offer, the action
   inside the card. */
.closest-result--ok,
.closest-result--offer {
  padding: 8px 9px;
  border-radius: var(--ods-radius);
  font-weight: 600;
}
.closest-result--ok { background: #d2f2c2; color: #113300; }
.closest-result--offer { background: #ffe3c2; color: #5c3a00; }

/* The card's own glyph, in the card's ink: a target for the offer, a tick for the verdict.
   Both live in the markup and the tone shows one, so the icon needs no script. */
.closest-result-icon { flex: none; display: none; margin-top: 1px; }
.closest-result--ok .closest-result-icon,
.closest-result--offer .closest-result-icon { display: block; }
.closest-result-icon .icon-check { display: none; }
.closest-result--ok .closest-result-icon .icon-check { display: block; }
.closest-result--ok .closest-result-icon .icon-target { display: none; }

/* The sentence, then the numbers under it. The latency is the half a visitor scans for, so
   it gets its own line and its own weight instead of trailing the sentence behind a dash; */
.closest-result-main { min-width: 0; }
.closest-result-detail {
  display: block;
  margin-top: 1px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.closest-result-age { display: block; margin-top: 2px; font-size: 11px; font-weight: 500; opacity: 0.8; }

/* The dismiss control: quiet, in the card's own ink, at the card's top right, round so the
   hover fill reads as a button and not as a smudge. */
.closest-result-close {
  flex: none;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
}
.closest-result-close[hidden] { display: none; }
.closest-result-close:hover { background: rgba(0, 0, 0, 0.1); }

/* The move is the card's own action, filled with the card's ink so the card reads as one
   object instead of a second control floating under it. Still the visitor's decision, and
   still a second click. */
.closest-move {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 7px 10px;
  border: 0;
  border-radius: calc(var(--ods-radius) / 2);
  background: #5c3a00;
  color: #ffe3c2;
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background var(--ods-duration);
}
.closest-move[hidden] { display: none; }
.closest-move:hover { background: #472c00; }

@keyframes spin {
  to { transform: rotate(360deg); }
}

.auto-select-spinner p {
  margin: 0;
  color: var(--text);
  font-size: 14px;
}

/* The overlay is the only thing on screen for a couple of seconds on a generic domain, so it carries the brand. The
   asset is a white wordmark, so it is drawn through its own alpha as a mask and filled with the brand navy (white
   on the dark skins) instead of sitting on a navy pill. */
.spinner-logo {
  display: block;
  width: 100px;
  height: 26px;
  margin: 22px auto 0;
  background-color: var(--ods-brand);
  -webkit-mask: url(/logo_com.png) no-repeat center / contain;
          mask: url(/logo_com.png) no-repeat center / contain;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .spinner-logo { background-color: #ffffff; }
}
:root[data-theme="dark"] .spinner-logo { background-color: #ffffff; }

/* ---------------------------------------------------------------------------
   Touch devices, keyed on the pointer and not the width: an iPad is wide and a foldable can be narrow, but both are
   tapped. These are finger ergonomics a mouse user never notices. */
@media (pointer: coarse) {
  /* 26px is a mouse pointer's target and not a thumb's. The guidelines ask for 40px, and
     the button's light chrome means growing it stays visually quiet. */
  .copy-btn { width: 38px; height: 38px; }

  /* Same 40px-ish floor for everything tapped in the sidebar bar: the strip chips, the
     region disclosures, the theme toggle. */
  .server { padding: 9px 56px 9px 12px; }
  .region-label { padding: 11px 10px 7px; }
  .theme-toggle { padding: 9px; }
  .closest-btn { padding: 10px 12px; font-size: 13px; }
  .closest-move { padding: 10px 12px; font-size: 13px; }

  /* Same 40px-ish floor for the server picker's trigger, the bar's other tap target. */
  .server-picker-btn { padding: 10px 12px; }

    /* The server filter is a form control, and iOS zooms on any focused field under 16px
       and does not zoom back out. The server picker above avoids the same trap. */
  .server-search { font-size: 16px; padding: 9px 10px; }
}

/* ---------------------------------------------------------------------------
   Bar layout again: the two phone bugs, part two. It sits after the desktop rules so
   source order wins, since media queries carry no specificity. */
@media (max-width: 900px) {
    /* The bar is a column, and .closest becomes a column too: the button and the card each
       own a row, so a short verdict cannot end up beside the button that produced it. */
  .closest { flex: 1 1 100%; padding-top: 0; display: flex; flex-direction: column; align-items: flex-start; }
  .closest-btn { width: auto; }
    /* The card stretches, so the sentence wraps inside it, the action under it is a
       full-width tap target, and a long verdict cannot push the bar sideways. */
  .closest-result { align-self: stretch; }
  .closest-result-text { overflow-wrap: anywhere; }
}
