/**
 * Tree IT Systems — design layer over Cassiopeia.
 *
 * Strategy: redefine the custom properties Cassiopeia and Bootstrap already
 * expose, then add a short list of rules for the few places where tokens are
 * not enough. Nothing here fights the parent's markup, so a Joomla update to
 * Cassiopeia cannot break the design.
 *
 * Loaded after the parent's template.css, so these declarations win.
 *
 * @copyright (C) 2026 Tree IT Systems e.K.
 * @license   GNU General Public License version 2 or later
 */

/* ---------------------------------------------------------------- font
   Montserrat, self-hosted: variable weight axis 400–700, Latin + German
   glyphs, one 30 KB file. SIL Open Font License 1.1 (see fonts/OFL.txt).
   Never fonts.googleapis.com — no third-party request, no visitor IP
   leaving the site.                                                      */
@font-face {
  font-family: "Montserrat";
  src: url("../fonts/montserrat-latin-400-700.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0020-007E, U+00A0-00FF, U+0152-0153, U+2013-2014, U+2018-201A,
                 U+201C-201E, U+2020-2022, U+2026, U+2030, U+2039-203A, U+2044,
                 U+20AC, U+2122, U+2192, U+2212;
}

/* metric-matched fallback: same x-height and line box, so the swap shifts
   nothing (CLS stays flat) */
@font-face {
  font-family: "Montserrat Fallback";
  src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
  size-adjust: 101%;
  ascent-override: 96.8%;
  descent-override: 25.1%;
  line-gap-override: 0%;
}

/* ---------------------------------------------------------------- tokens */
:root {
  color-scheme: light dark;

  /* --- the five company colours, verbatim --------------------------------
     red #6f1009 · dark red #570f07 · silver #eceff3 · off-white #f5f5f5 ·
     graphite #3f4445. Red is the first colour, silver the second; everything
     below is derived from these and nothing else. */
  --tree-red: #6f1009;
  --tree-red-dark: #570f07;
  --tree-silver: #eceff3;
  --tree-silver-2: #f5f5f5;
  --tree-graphite: #3f4445;

  /* roles — revised 2026-09-12: in light mode the site uses the **dark red**
     everywhere the red is read as a colour (text, links, glyphs, prices) and
     everywhere it is a filled surface (buttons, the step discs, the map pin).
     #6f1009 is kept for dark mode, where #570f07 is indistinguishable from
     the background. */
  --tree-accent: var(--tree-red-dark);   /* red text, glyphs, focus rings */
  --tree-accent-h: var(--tree-graphite); /* …and what they turn on hover */
  /* ⚠ Links have their own token, because the accent cannot follow them into
     dark mode. Company dark red on black measures **1.75:1** — not a close
     call, three times short of the 4.5:1 a body link needs — and the two
     lighter reds tried instead were rejected on sight ("icons look awful").
     So in dark mode a link is silver, exactly the decision already taken for
     the hosting prices, and the underline carries the "this is a link" signal
     that the colour used to. Light mode is unchanged: dark red. */
  --tree-link: var(--tree-accent);
  --tree-link-h: var(--tree-accent-h);
  --tree-fill: var(--tree-red-dark);     /* red as a filled surface */
  --tree-on-accent: #ffffff;
  --tree-ink: #1f2325;              /* graphite, one step darker for body text */
  --tree-ink-2: var(--tree-graphite);
  --tree-bg: #ffffff;
  --tree-bg-alt: var(--tree-silver);
  --tree-hairline: #d7dbe1;         /* silver, one step darker for hairlines */
  /* An icon badge is now the same object as a numbered step disc: the company
     red filled, the glyph white. Decided 2026-09-12 — "adjust the icon
     colouring to the number colouring". It also settles dark mode, where a
     dark red *glyph* is a smudge but a dark red *pane* is a colour. */
  --tree-ico-bg: var(--tree-fill);     /* the pane behind a glyph */
  --tree-ico-fg: #ffffff;              /* the glyph itself */
  /* A marker or a large figure drawn straight onto the page, with nothing
     behind it to carry a colour. Dark red on white is 13.4:1; on black it is
     1.9:1, and every red that passes there reads as pink or orange — both were
     tried and both were rejected. So on a dark ground these turn white. Red
     where the ground allows it, white where it does not. */
  --tree-mark: var(--tree-red-dark);   /* ticks, rules, years, contact glyphs */
  --tree-display: var(--tree-red-dark); /* the hosting price, the domain endings */
  --tree-logo-disc: var(--tree-red);
  --tree-logo-tree: #ffffff;

  --tree-font: "Montserrat", "Montserrat Fallback", -apple-system,
               BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  --tree-container: 1120px;
  --tree-gutter: clamp(1.25rem, 4vw, 2.5rem);
  --tree-section: clamp(3.5rem, 8vw, 6.5rem);

  /* --- Cassiopeia's own hooks --- */
  --template-bg-light: var(--tree-bg-alt);
  --template-text-dark: var(--tree-ink);
  --template-text-light: #ffffff;
  --template-special-color: var(--tree-accent);
  --template-link-color: var(--tree-link);

  /* --- Bootstrap/Cassiopeia base --- */
  --body-bg: var(--tree-bg);
  --body-color: var(--tree-ink);
  --body-font-family: var(--tree-font);
  --body-font-size: 1.0625rem;
  --body-font-weight: 400;
  --body-line-height: 1.62;
  --link-color: var(--tree-link);
  --link-hover-color: var(--tree-link-h);
  --border-color: var(--tree-hairline);
  --border-color-translucent: var(--tree-hairline);
  --border-radius: 12px;
  --border-radius-sm: 8px;
  --border-radius-lg: 16px;
  --border-radius-xl: 20px;
  --border-radius-xxl: 24px;
  --border-radius-pill: 999px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Dark mode keeps the company red for filled surfaces, where white type
       sits on it, and uses a light tint of it for text and links — #6f1009 as
       link colour on black is 1.6:1 and unreadable. */
    --tree-accent: #d98078;         /* tint of --tree-red, for text and links */
    --tree-accent-h: #e8a19a;
    --tree-link: var(--tree-silver-2);
    --tree-link-h: #ffffff;
    --tree-fill: var(--tree-red);   /* on black the brighter red is the surface */
    --tree-on-accent: #ffffff;
    --tree-ink: var(--tree-silver-2);
    --tree-ink-2: #a8adb3;
    --tree-bg: #000000;
    --tree-bg-alt: #131517;
    --tree-hairline: #2b2f33;
    --tree-ico-bg: var(--tree-fill);
    --tree-ico-fg: #ffffff;
    --tree-mark: #ffffff;
    /* Large red text on black was tried three ways: the brand reds (1.75:1,
       invisible), `#bf3626` (3.77:1, reads orange) and `#b02a1f` (3.20:1, the
       darkest red that clears the floor). Decided 2026-09-13 to use the second
       company colour instead — silver is unambiguous on black and the red
       stays where it belongs, on the light theme. */
    --tree-display: var(--tree-silver);
    --tree-logo-disc: var(--tree-red);
    --tree-logo-tree: var(--tree-silver-2);
    --template-text-dark: var(--tree-ink);
  }
}

:root[data-theme="dark"] {
  --tree-accent: #d98078;
  --tree-accent-h: #e8a19a;
  --tree-link: var(--tree-silver-2);
  --tree-link-h: #ffffff;
  --tree-fill: var(--tree-red);
  --tree-on-accent: #ffffff;
  --tree-ink: var(--tree-silver-2);
  --tree-ink-2: #a8adb3;
  --tree-bg: #000000;
  --tree-bg-alt: #131517;
  --tree-hairline: #2b2f33;
  --tree-ico-bg: var(--tree-fill);
  --tree-ico-fg: #ffffff;
  --tree-mark: #ffffff;
  --tree-display: var(--tree-silver);
  --tree-logo-disc: var(--tree-red);
  --tree-logo-tree: var(--tree-silver-2);
  --template-text-dark: var(--tree-ink);
}

/* the tokens above are consumed here, so both themes resolve as a set */
body {
  background: var(--tree-bg);
  color: var(--tree-ink);
  font-family: var(--tree-font);
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* ---------------------------------------------------------------- type */
h1, h2, h3, h4, .page-header h1, .page-header h2 {
  font-family: var(--tree-font);
  font-weight: 600;
  color: var(--tree-ink);
  text-wrap: balance;
}
h1 { font-size: clamp(2.1rem, 4.6vw, 3.25rem); letter-spacing: -0.035em; line-height: 1.04; }
h2 { font-size: clamp(1.7rem, 3.2vw, 2.5rem); letter-spacing: -0.028em; line-height: 1.1; }
h3 { font-size: 1.3125rem; letter-spacing: -0.015em; line-height: 1.3; }
/* Measure: decided 2026-09-12 to let running text use the column it is in.
   The container is already capped at 1120 px, which is the real limit; a
   second cap on the paragraph left a third of every section empty. Documents
   keep a reading measure (`.tree-legal`), and a lead keeps a short one,
   because those are the two places where a narrow column is the point. */
p { max-width: none; }
a { text-decoration: none; }
.container-header a, .container-footer a, .mod-menu a, .mod-breadcrumbs a { text-decoration: none; }
.container-component a:not(.btn) { text-decoration: underline; text-underline-offset: 3px; }
.container-component .mod-menu a, .container-component .mod-breadcrumbs a { text-decoration: none; }
.mod-menu a:hover, .container-footer a:hover { text-decoration: none; }

/* ---------------------------------------------------------------- layout */
.site-grid,
.container-header > .grid-child,
.container-footer > .grid-child {
  max-width: var(--tree-container);
  margin-inline: auto;
  padding-inline: var(--tree-gutter);
}
/* ⚠ Do not set a gap on .site-grid: Cassiopeia's grid keeps rows for the
   containers that hold no modules, and a row-gap there adds blank space at the
   top of every page (144 px, measured). Space belongs on the sections. */
.site-grid { gap: 0; }
.container-component { padding-block: clamp(2rem, 5vw, 3.5rem) var(--tree-section); }
.container-component > * + * { margin-top: 2rem; }

/* section rhythm inside article bodies */
.container-component h2 { margin-top: clamp(2.5rem, 5vw, 4rem); }
.container-component h2 + p,
.container-component h2 + .row { margin-top: 1.25rem; }
.container-component h3 { margin-top: 0; }
.container-component .lead {
  font-size: var(--fs-lead, clamp(1.09rem, 1.6vw, 1.375rem));
  color: var(--tree-ink-2);
  line-height: 1.5;
  max-width: 62ch;
  margin-top: 1.25rem;
}
.container-component h1 + .lead { margin-top: 1.25rem; }
.container-component .lead + p { margin-top: 2rem; }

/* ---------------------------------------------------------------- header */
/* ⚠ Cassiopeia forces `position: relative !important` on the header below
   992 px, which would drop the sticky bar — and with it the menu toggle — as
   soon as a phone user scrolls. Same media query, same weight, later. */
@media (width <= 991.98px) {
  .container-header { position: sticky !important; }
}
.container-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--tree-bg) 78%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--tree-hairline);
  box-shadow: none;
}
/* Stock Cassiopeia stacks the header: the brand `.grid-child` on one row, the
   `.container-nav` `.grid-child` underneath (its menu carries `flex:1 0 100%`).
   This design wants one row — logo left, module menu right — so the header
   itself becomes the flex row and holds the 1120 px measure, and the two
   grid-children stop constraining their own width. Below 992 px the nav is
   allowed to wrap onto its own full-width line. Markup untouched: what appears
   in the bar is whatever module is published in the `menu` position. */
.container-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 1.5rem;
  padding-inline: max(var(--tree-gutter), (100% - var(--tree-container)) / 2);
}
.container-header > .grid-child {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  min-height: 60px;
  padding-block: 0;
  padding-inline: 0;
  margin-inline: 0;
  max-width: none;
  width: auto;          /* Cassiopeia's .grid-child is width:100% — two of those cannot share a row */
  flex: 0 1 auto;
}
.container-header > .container-nav {
  flex: 0 1 auto;
  margin-left: auto;
  padding-bottom: 0;
}
/* Cassiopeia gives the header menu `flex: 1 0 100%` for its stacked layout. */
.container-header .mod-menu { flex: 0 1 auto; }
@media (width <= 991.98px) {
  .container-header > .container-nav { flex: 1 1 100%; margin-left: 0; }
}
.navbar-brand { margin: 0; }
.navbar-brand a,
.brand-logo {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  color: var(--tree-ink);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-size: 1rem;
}
.navbar-brand img, .brand-logo img, .container-header img.logo {
  height: 34px;
  width: auto;
  max-height: none;
  max-width: none;
}

/* The brand mark is a single monochrome (black) SVG served through the
   template style's Brand → Logo parameter. One <img> element cannot swap its
   src from CSS, so dark mode inverts it to white instead of shipping a second
   file. If a COLOURED logo is ever configured, delete this block — inverting
   a coloured mark produces wrong hues. */
/* `brightness(0) invert(1)` turns any monochrome mark white, whatever colour
   the file uses — cleaner than `invert(1)`, which turned the brand grey into
   a muddy warm grey. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .navbar-brand img,
  :root:not([data-theme="light"]) .brand-logo img,
  :root:not([data-theme="light"]) .container-header img.logo,
  :root:not([data-theme="light"]) .container-footer img.tree-footer-logo { filter: brightness(0) invert(1); }
}
:root[data-theme="dark"] .navbar-brand img,
:root[data-theme="dark"] .brand-logo img,
:root[data-theme="dark"] .container-header img.logo,
:root[data-theme="dark"] .container-footer img.tree-footer-logo { filter: brightness(0) invert(1); }

/* Cassiopeia paints header text with --template-text-light (for its dark
   header); this header is light, so the colour is set explicitly. */
.container-header,
.container-header a,
.container-header .site-title,
.container-header .site-description { color: var(--tree-ink); }
.container-header a:hover { color: var(--tree-ink); }
.navbar-brand .site-title {
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-transform: none;
  margin: 0;
}
.navbar-brand .site-description { display: none; }
.container-header .container-nav { margin-left: auto; }

/* main navigation (mod_menu, core markup untouched) */
/* Cassiopeia paints the header menu white (`.container-header .mod-menu{color:#fff}`)
   and its links inherit that through `currentColor`, because its own header is
   dark. This header is light, so the colour is restored at the same place, with
   the same specificity, later in the cascade. Without this the menu items are
   white on white — invisible. */
.container-header .mod-menu { color: var(--tree-ink); }
.container-header .mod-menu > li > a,
.container-header .mod-menu > li > span,
.container-header .mod-menu .mod-menu__sub a,
.container-header .mod-menu .mod-menu__sub span { color: var(--tree-ink); }
.container-header .mod-menu .mod-menu__sub {
  background-color: var(--tree-bg);
  border: 1px solid var(--tree-hairline);
  border-radius: var(--border-radius-lg, 12px);
  padding: .5rem 0;
}

.container-nav .mod-menu {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  align-items: center;
  justify-content: flex-end;
  margin: 0;
  padding: 0;
  list-style: none;
  border: 0;
}
.container-nav .mod-menu .nav-item { margin: 0; }
.container-nav .mod-menu a {
  padding: 0;
  font-size: .9375rem;
  color: var(--tree-ink);
  opacity: .82;
  border: 0;
  background: none;
  transition: opacity 180ms ease;
}
.container-nav .mod-menu a:hover { opacity: 1; text-decoration: none; }
.container-nav .mod-menu .current > a { opacity: 1; font-weight: 500; }

/* Below 992 px Cassiopeia turns the menu into a full-height stacked column
   (its own rule, `.container-header .mod-menu{flex-direction:column}`), which
   pushes the content of the page down by roughly 300 px on a phone. Same
   specificity, later in the cascade: keep it a row that wraps onto a second
   line. No markup and no JavaScript, so the menu stays whatever the module
   publishes. */
@media (width <= 991.98px) {
  .container-header .mod-menu {
    flex-direction: row;
    flex-wrap: wrap;
    gap: .35rem 1.1rem;
  }
  .container-nav .mod-menu { justify-content: flex-start; }
  .container-header .container-nav nav,
  .container-nav .container-search { margin-top: .5rem; }
  .container-nav { padding-bottom: .5em; }
}

/* -------------------------------------------------- mobile menu (no JS)
   The shell comes from the mod_menu layout override: a button with
   `popovertarget` and a panel with `popover`. The browser does the opening,
   the Esc key, the click-outside dismissal and the focus handling — there is
   no script. The item list inside the panel is core's own output.

   Default state below is "no shell at all": button and close hidden, panel
   transparent to layout (`display: contents`), so the desktop row and any
   browser without popover support show the plain list. */
.tree-navtoggle { display: none; }
.tree-navpanel { display: contents; }

@media (width <= 991.98px) {
  @supports selector(:popover-open) {
    /* the hamburger, drawn in CSS — no icon font, no image */
    .tree-navtoggle {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 44px;
      height: 44px;
      margin-inline-start: auto;
      padding: 0;
      border: 0;
      border-radius: 999px;
      background: none;
      color: var(--tree-ink);
      cursor: pointer;
    }
    .tree-navtoggle__bars,
    .tree-navtoggle__bars::before,
    .tree-navtoggle__bars::after {
      display: block;
      width: 19px;
      height: 1.5px;
      border-radius: 2px;
      background: currentColor;
    }
    .tree-navtoggle__bars { position: relative; }
    .tree-navtoggle__bars::before,
    .tree-navtoggle__bars::after {
      content: "";
      position: absolute;
      left: 0;
    }
    .tree-navtoggle__bars::before { top: -6px; }
    .tree-navtoggle__bars::after { top: 6px; }

    /* the header row keeps one line: logo left, hamburger right */
    .container-header > .container-nav { flex: 0 1 auto; margin-left: auto; }
    .container-nav { padding-bottom: 0; }

    /* the hamburger becomes an X while the panel is open. The panel is a later
       sibling, so :has() on the container is what makes this possible without
       a script. Browsers without :has() simply keep the three bars. */
    .container-nav:has(.tree-navpanel:popover-open) .tree-navtoggle__bars {
      background: transparent;
    }
    .container-nav:has(.tree-navpanel:popover-open) .tree-navtoggle__bars::before {
      top: 0;
      rotate: 45deg;
    }
    .container-nav:has(.tree-navpanel:popover-open) .tree-navtoggle__bars::after {
      top: 0;
      rotate: -45deg;
    }
    .tree-navtoggle__bars::before,
    .tree-navtoggle__bars::after { transition: top .2s ease, rotate .2s ease; }

    /* the panel: everything below the header bar, in the browser's top layer,
       so the logo and the toggle stay visible and usable above it */
    .tree-navpanel {
      display: none;
      position: fixed;
      inset: 60px 0 0 0;
      width: 100%;
      height: calc(100dvh - 60px);
      max-width: none;
      max-height: none;
      margin: 0;
      padding: 1.5rem clamp(1.25rem, 6vw, 2rem) 2rem;
      border: 0;
      overflow-y: auto;
      overscroll-behavior: contain;
      background: var(--tree-bg);
      color: var(--tree-ink);
      opacity: 0;
      translate: 0 -.5rem;
      transition:
        opacity .24s ease,
        translate .24s ease,
        display .24s allow-discrete,
        overlay .24s allow-discrete;
    }
    .tree-navpanel:popover-open {
      display: block;
      opacity: 1;
      translate: 0 0;
    }
    /* the state the panel animates *from* — native, no script */
    @starting-style {
      .tree-navpanel:popover-open { opacity: 0; translate: 0 -.5rem; }
    }
    /* No ::backdrop tint: the panel already covers everything below the bar,
       and a tint would also darken the header, which stays visible. */
    .tree-navpanel::backdrop { background: none; }

    /* the item list inside the panel: stacked, tappable, hairline separated */
    .tree-navpanel .mod-menu {
      flex-direction: column;
      flex-wrap: nowrap;
      align-items: stretch;
      justify-content: flex-start;
      gap: 0;
      width: 100%;
      max-width: 30rem;
    }
    .tree-navpanel .mod-menu > li { border-block-start: 1px solid var(--tree-hairline); }
    .tree-navpanel .mod-menu > li:first-child { border-block-start: 0; }
    .tree-navpanel .mod-menu > li > a,
    .tree-navpanel .mod-menu > li > span {
      display: flex;
      align-items: center;
      min-height: 56px;
      padding-block: .5rem;
      font-size: 1.375rem;
      font-weight: 500;
      letter-spacing: -0.02em;
      opacity: 1;
    }
    .tree-navpanel .mod-menu .current > a { color: var(--tree-accent); }
    /* a parent item keeps its label and core's chevron toggle on one row */
    .tree-navpanel .mod-menu > li.parent {
      display: grid;
      grid-template-columns: 1fr auto;
      align-items: center;
    }
    .tree-navpanel .mod-menu > li.parent > .mod-menu__sub { grid-column: 1 / -1; }
    .tree-navpanel .mod-menu .mod-menu__toggle-sub {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 44px;
      height: 44px;
      padding: 0;
      border: 0;
      background: none;
      color: var(--tree-ink-2);
    }
    .tree-navpanel .mod-menu .mod-menu__sub {
      position: static;
      display: block;
      min-width: 0;
      padding: 0 0 .5rem 1rem;
      border: 0;
      background: none;
      box-shadow: none;
    }
    .tree-navpanel .mod-menu .mod-menu__sub > li > a { min-height: 44px; font-size: 1.0625rem; font-weight: 400; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .tree-navpanel,
  .tree-navpanel::backdrop { transition-duration: 1ms; }
}

/* ---------------------------------------------------------------- buttons
   Three states, all from the company palette: rest, hover/focus, and the
   moment of the click. Red is the primary colour, silver the second, so a
   primary button darkens red → dark red, and a secondary button inverts
   silver → graphite. `:active` adds a 2 % press, which is the only motion
   and is removed under prefers-reduced-motion. */
.btn, button.btn, .btn-primary, .readmore .btn {
  border-radius: 999px;
  padding: .7rem 1.35rem;
  font-size: .9375rem;
  font-weight: 500;
  border: 1px solid transparent;
  box-shadow: none;
  min-height: 44px;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease, scale .1s ease;
}
.btn:active, button.btn:active { scale: .98; }

/* Decided 2026-09-12: the two button families swap into each other's colour.
   A red button turns **graphite** — the dark silver — and a light button turns
   **dark red**. So whichever button you hover, the page answers in the other
   company colour instead of merely darkening.

   Contrast of both hover states, measured: white on #3f4445 = 9.6:1,
   white on #570f07 = 13.4:1. */

/* primary — red, inverting to graphite */
.btn-primary,
.btn-primary:focus {
  background: var(--tree-fill);
  border-color: var(--tree-fill);
  color: #ffffff;
}
.btn-primary:hover,
.btn-primary:focus-visible,
.btn-primary:active {
  background: var(--tree-graphite);
  border-color: var(--tree-graphite);
  color: #ffffff;
}
/* graphite on black is a legible surface, so dark mode keeps the same move */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-primary:hover,
  :root:not([data-theme="light"]) .btn-primary:focus-visible,
  :root:not([data-theme="light"]) .btn-primary:active {
    background: var(--tree-graphite);
    border-color: #5c6365;
    color: #ffffff;
  }
}
:root[data-theme="dark"] .btn-primary:hover,
:root[data-theme="dark"] .btn-primary:focus-visible,
:root[data-theme="dark"] .btn-primary:active {
  background: var(--tree-graphite);
  border-color: #5c6365;
  color: #ffffff;
}

/* secondary — silver or white, inverting to dark red */
.btn-secondary,
.btn-outline-primary,
.btn-secondary:focus,
.btn-outline-primary:focus {
  background: var(--tree-silver);
  border-color: transparent;
  color: var(--tree-graphite);
}
.btn-secondary:hover,
.btn-outline-primary:hover,
.btn-secondary:focus-visible,
.btn-outline-primary:focus-visible,
.btn-secondary:active,
.btn-outline-primary:active {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .btn-secondary,
  :root:not([data-theme="light"]) .btn-outline-primary {
    background: transparent;
    border-color: var(--tree-hairline);
    color: var(--tree-ink);
  }
  :root:not([data-theme="light"]) .btn-secondary:hover,
  :root:not([data-theme="light"]) .btn-outline-primary:hover,
  :root:not([data-theme="light"]) .btn-secondary:focus-visible,
  :root:not([data-theme="light"]) .btn-outline-primary:focus-visible {
    background: var(--tree-red-dark);
    border-color: color-mix(in srgb, var(--tree-red) 60%, var(--tree-silver));
    color: #ffffff;
  }
}
:root[data-theme="dark"] .btn-secondary,
:root[data-theme="dark"] .btn-outline-primary {
  background: transparent;
  border-color: var(--tree-hairline);
  color: var(--tree-ink);
}
:root[data-theme="dark"] .btn-secondary:hover,
:root[data-theme="dark"] .btn-outline-primary:hover,
:root[data-theme="dark"] .btn-secondary:focus-visible,
:root[data-theme="dark"] .btn-outline-primary:focus-visible {
  background: var(--tree-red-dark);
  border-color: color-mix(in srgb, var(--tree-red) 60%, var(--tree-silver));
  color: #ffffff;
}

@media (prefers-reduced-motion: reduce) {
  .btn, button.btn { transition-duration: 1ms; }
  .btn:active, button.btn:active { scale: 1; }
}

/* ---------------------------------------------------------------- surfaces */
/* ---------------------------------------------------------------- card grids
   Bootstrap columns do not stretch their child by default, so cards in a row
   end up ragged. This makes every card fill its column. */
.container-component .row { row-gap: 1.25rem; }
/* only columns that actually hold a card become flex, and as a column, so a
   plain text column keeps normal block flow */
.container-component .row > [class*="col-"]:has(> .card) { display: flex; flex-direction: column; }
.container-component .row > [class*="col-"] > .card { flex: 1 1 auto; }
.card h3 { margin-bottom: .35rem; }
/* specificity has to beat .container-component a:not(.btn), which underlines
   content links */
.container-component .card h3 a { color: var(--tree-ink); text-decoration: none; }
.container-component .card h3 a:hover { color: var(--tree-accent); text-decoration: none; }
.card p { color: var(--tree-ink-2); font-size: var(--fs-small, .9375rem); }

/* ⚠ The hairline is an **inset shadow, not a border**. A real border sits
   outside the padding box, so a photograph inside a card can never reach the
   card's edge and the border draws a visible line around the picture — which
   is exactly what it did once the hovered card turned dark red. An inset ring
   occupies no layout space, looks identical at rest, and simply stops
   existing on hover. */
.card, .sidebar-right .card, .sidebar-left .card {
  background: var(--tree-bg);
  border: 0;
  border-radius: 20px;
  box-shadow: inset 0 0 0 1px var(--tree-hairline);
  overflow: hidden;
}
.card-header, .card-footer { background: transparent; border-color: var(--tree-hairline); }
.card-body { padding: 1.75rem; }
.well, .blog-featured > .card { background: var(--tree-bg-alt); }

/* article and blog listings */
.page-header { border: 0; margin-bottom: 1.5rem; padding: 0; }
.blog-featured .item, .blog .item, .com-content-article {
  border-bottom: 1px solid var(--tree-hairline);
  padding-block: 2.5rem;
}
.blog-featured .item:last-child, .blog .item:last-child { border-bottom: 0; }
.com-content-article__body > * + * { margin-top: 1.25rem; }
.article-info { font-size: .875rem; color: var(--tree-ink-2); }
.article-info dd { margin: 0; }
figure img, .com-content-article img { border-radius: 20px; height: auto; }

/* breadcrumbs, quiet */
.mod-breadcrumbs__wrapper { padding-block: 1.25rem 0; }
.mod-breadcrumbs {
  background: transparent;
  padding: 0;
  font-size: .875rem;
  color: var(--tree-ink-2);
}
.mod-breadcrumbs__item + .mod-breadcrumbs__item::before { color: var(--tree-hairline); }

/* forms */
.form-control, .inputbox, input[type="text"], input[type="email"], input[type="password"], textarea, select {
  border-radius: 12px;
  border: 1px solid var(--tree-hairline);
  background: var(--tree-bg);
  color: var(--tree-ink);
  min-height: 44px;
  font-family: var(--tree-font);
}
.form-control:focus { border-color: var(--tree-accent); box-shadow: none; }
/* input groups: the appended button must follow the theme too, or it stays a
   white patch on a dark ground */
.input-group-text,
.input-group .btn,
.input-group button {
  background: var(--tree-bg);
  border: 1px solid var(--tree-hairline);
  color: var(--tree-ink-2);
  border-radius: 12px;
  min-height: 44px;
}
.input-group .btn:hover { background: var(--tree-bg-alt); color: var(--tree-ink); }

/* system messages */
/* ⚠ Core's alert ships its own white background and a pale text colour inside
   a shadow DOM-ish wrapper; on the dark theme that produced a white slab with
   grey-on-white text — reported as "the contact form error is not readable".
   The colour is forced on every element inside the alert, and the danger
   variant gets the company red as a left marker rather than as a fill. */
joomla-alert, .alert {
  display: block;
  /* ⚠ It sits directly above the page's first section and was touching the
     contact card. Core's own alert stylesheet loads *after* this one and sets
     `margin-bottom: 1rem` at the same specificity, so the value has to be
     forced — measured 16 px before, the clamp after. */
  margin-block: 1rem clamp(1.5rem, 3vw, 2.25rem) !important;
  border-radius: 12px;
  border: 1px solid var(--tree-hairline);
  background: var(--tree-bg-alt) !important;
  color: var(--tree-ink) !important;
  padding: 1rem 1.25rem;
  font-size: var(--fs-small, .9375rem);
}
joomla-alert *, .alert * { color: inherit; }
joomla-alert a, .alert a { color: var(--tree-link); }
/* ⚠ core sets `border-color` per variant with its own custom properties, which
   land later in the cascade than a bare element selector — measured
   `rgb(244,207,206)`, a pale pink, where the company red was intended. */
joomla-alert[type="danger"], joomla-alert.joomla-alert--danger, .alert-danger {
  border-color: var(--tree-hairline) !important;
  border-left: 4px solid var(--tree-fill) !important;
}
joomla-alert[type="success"], .alert-success {
  border-color: var(--tree-hairline) !important;
  border-left: 4px solid var(--tree-graphite) !important;
}
/* the close button is a button, not a red cross on a white field */
joomla-alert button.joomla-alert--close,
joomla-alert .joomla-alert-button,
.alert .btn-close {
  color: var(--tree-ink-2) !important;
  background: none;
  opacity: 1;
}

/* ---------------------------------------------------------------- footer
   Cassiopeia renders the `footer` position with `style="none"`: no chrome, no
   module titles, just each module's own output, stacked. So the columns are
   the modules themselves — this grid turns them into columns, and a module
   with the class `tree-footer-legal` becomes the full-width bar underneath.
   Column headings live inside the modules (see the markup in
   `claude/website-copy-en.md`), because `style="none"` prints no title. */
.container-footer {
  background: var(--tree-bg-alt);
  border-top: 1px solid var(--tree-hairline);
  color: var(--tree-ink-2);
  margin-top: 0;
}
.container-footer > .grid-child {
  display: grid;
  --tree-footer-gap: clamp(1.75rem, 4vw, 3rem);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--tree-footer-gap);
  align-items: start;
  padding-block: clamp(2.5rem, 5vw, 4rem) 1.5rem;
}
.container-footer > .grid-child > * { min-width: 0; }

/* column headings: quiet, uppercase, small — they label, they do not shout */
.container-footer h2,
.container-footer h3,
.container-footer .tree-footer-head {
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--tree-ink-2);
  margin: 0 0 .85rem;
}
.container-footer ul,
.container-footer .mod-menu {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: .55rem;
}
.container-footer li { margin: 0; }
.container-footer a { color: var(--tree-ink); opacity: .8; }
.container-footer a:hover,
.container-footer a:focus-visible { opacity: 1; color: var(--tree-accent); }
.container-footer p {
  font-size: .9375rem;
  line-height: 1.6;
  color: var(--tree-ink-2);
  max-width: 34ch;
  margin: 0 0 .5rem;
}
.container-footer img.tree-footer-logo {
  height: 30px;
  width: auto;
  margin-bottom: 1rem;
}
/* the address block: same data as the Impressum, one line per field */
.container-footer address {
  font-style: normal;
  font-size: .9375rem;
  line-height: 1.7;
  color: var(--tree-ink-2);
}

/* The bottom bar. Keyed off BOTH the module class and "last module in the
   position", so a forgotten Module Class no longer squeezes the copyright
   into a narrow fifth column — which is what happened on staging. */
.container-footer > .grid-child > :nth-last-child(1 of :not(.mod-languages)),
.container-footer .tree-footer-legal {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem 1.5rem;
  margin-top: .5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--tree-hairline);
  font-size: .875rem;
}
.container-footer .tree-footer-legal ul {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
}
.container-footer > .grid-child > :nth-last-child(1 of :not(.mod-languages)) p,
.container-footer .tree-footer-legal p { max-width: none; margin: 0; }

/* If the switcher is published in `footer` instead of `tree-lang` — i.e. the
   {loadposition} route was not taken — it becomes its own grid item. Then it
   is right-aligned directly under the copyright, without a second border. */
.container-footer > .grid-child > .mod-languages {
  grid-column: 1 / -1;
  justify-self: end;
  border-top: 0;
  padding-top: 0;
  /* cancel the grid gap so the pills sit on the copyright's line, right */
  margin-top: calc(-1 * var(--tree-footer-gap) + .25rem);
}

/* The language switcher and the colour-scheme control are **one component in
   two flavours**, so they are written as one rule set with one set of numbers.
   They were close but not equal — different shell fill, different segment
   height, a circular active chip on one and a pill on the other — and that
   reads as sloppy rather than as a pair. Reported twice; this version stops
   styling them separately. */
.container-footer .mod-languages ul,
.container-footer .tree-theme {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--tree-hairline);
  border-radius: 999px;
  background: transparent;
  margin: 0;
  list-style: none;
}
.container-footer .mod-languages a,
.container-footer .tree-theme button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 30px;
  min-width: 3.25rem;
  padding: 0 .85rem;
  border: 0;
  border-radius: 999px;
  background: none;
  font: inherit;
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .04em;
  line-height: 1;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--tree-ink-2);
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease;
}
.container-footer .mod-languages .lang-active > a,
.container-footer .mod-languages a[aria-current="true"],
.container-footer .tree-theme button[aria-pressed="true"] {
  background: var(--tree-ink);
  color: var(--tree-bg);
}
.container-footer .mod-languages a:hover,
.container-footer .tree-theme button:hover[aria-pressed="false"] { color: var(--tree-ink); }
/* ⚠ mod_languages wraps its items in more than the list: a heading, a hidden
   "active language" badge, and in some configurations a `<span>` around the
   label. Any of those can carry a ground of its own and show up as a grey
   patch inside the pill — reported 2026-09-13. Everything inside the shell is
   therefore reset to no background and no border, and the shell clips whatever
   is left to its own rounded shape. */
.container-footer .mod-languages ul { overflow: hidden; }
.container-footer .mod-languages :is(li, span, div, p) {
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
}
.container-footer .mod-languages a { background: none; }
.container-footer .mod-languages .lang-active::after,
.container-footer .mod-languages h3,
.container-footer .mod-languages .visually-hidden,
.container-footer .mod-languages .sr-only { display: none; }
.tree-footer-legal .mod-languages,
.tree-footer-legal .tree-theme { align-self: center; }
.tree-footer-legal .mod-languages { margin-left: auto; }
.tree-footer-legal .mod-languages + .tree-theme { margin-left: .75rem; }

/* the language switcher (mod_languages), wherever it is published */
.mod-languages ul {
  display: flex;
  gap: .35rem;
  align-items: center;
  list-style: none;
  padding: 0;
  margin: 0;
}
.mod-languages li { margin: 0; }
.mod-languages a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 .6rem;
  border-radius: 999px;
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.mod-languages .lang-active > a,
.mod-languages a[aria-current="true"] {
  background: var(--tree-graphite);
  color: var(--tree-silver);
  opacity: 1;
}
.mod-languages .badge { display: none; }   /* Joomla's "active" badge duplicates the state */

/* ⚠ Reported 2026-09-12: the switcher pills came out round in light mode and
   square in dark. Our own rule sets the same radius in both themes, so the
   square corners come from something else painting a background on a
   different element — Bootstrap's `.btn`, a dropdown `select`, a flag image,
   or a wrapper Cassiopeia gives a card radius to under `data-bs-theme=dark`.
   Rather than guess which, every shape the switcher can render is pinned to
   the pill radius in both themes. The `!important` is deliberate: this has to
   beat whatever rule is winning, and it applies to nothing outside the
   language switcher. */
.mod-languages a,
.mod-languages .btn,
.mod-languages li,
.mod-languages img,
.mod-languages select,
.mod-languages .dropdown-toggle,
.mod-languages .dropdown-menu { border-radius: 999px !important; }
.mod-languages .dropdown-menu { padding-inline: .35rem; }
.mod-languages img { max-height: 18px; width: auto; }
/* on black, a graphite pill disappears — invert it */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .mod-languages .lang-active > a,
  :root:not([data-theme="light"]) .mod-languages a[aria-current="true"] {
    background: var(--tree-silver);
    color: var(--tree-graphite);
  }
}
:root[data-theme="dark"] .mod-languages .lang-active > a,
:root[data-theme="dark"] .mod-languages a[aria-current="true"] {
  background: var(--tree-silver);
  color: var(--tree-graphite);
}

/* Cassiopeia's own back-to-top link, in the company palette: a red disc with a
   white arrow, dark red when hovered or pressed — the same pair as the primary
   button, so the one piece of floating chrome on the page reads as ours. Red
   as a filled surface is correct in both themes (white on #6f1009 measures
   12:1); on black it gets a faint ring so the disc does not float unbounded. */
.back-to-top-link {
  inset-inline-end: clamp(.75rem, 3vw, 1.5rem);
  bottom: clamp(.75rem, 3vw, 1.5rem);
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background-color: var(--tree-fill);
  border: 1px solid var(--tree-fill);
  color: #ffffff;
  box-shadow: 0 2px 10px rgb(0 0 0 / .18);
  transition: opacity .2s ease-in, background-color .18s ease,
              border-color .18s ease, scale .15s ease;
}
.back-to-top-link .icon-arrow-up { font-size: .95rem; line-height: 1; }
.back-to-top-link:hover,
.back-to-top-link:focus {
  background-color: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
.back-to-top-link:active { scale: .96; }
.back-to-top-link:focus-visible { outline: 3px solid var(--tree-accent); outline-offset: 3px; }
/* core keeps the link click-through at opacity 0; hide it properly until its
   script marks it visible, so there is no invisible target in the corner */
.back-to-top-link:not(.visible) { visibility: hidden; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .back-to-top-link { border-color: rgb(255 255 255 / .28); }
}
:root[data-theme="dark"] .back-to-top-link { border-color: rgb(255 255 255 / .28); }
@media (prefers-reduced-motion: reduce) {
  .back-to-top-link { transition: opacity .2s ease-in; }
  .back-to-top-link:active { scale: 1; }
}

/* ---------------------------------------------------------------- theme control
   Rendered by a Custom HTML module in the header; styled here so the module
   needs no CSS of its own. */
.tree-theme {
  display: flex;
  align-items: center;
  gap: .1rem;
  border: 1px solid var(--tree-hairline);
  border-radius: 999px;
  padding: 2px;
}
.tree-theme button {
  font: inherit;
  font-size: .78rem;
  letter-spacing: .04em;
  background: none;
  border: 0;
  color: var(--tree-ink-2);
  padding: .25rem .6rem;
  border-radius: 999px;
  cursor: pointer;
  min-height: 30px;
}
.tree-theme button[aria-pressed="true"] { background: var(--tree-ink); color: var(--tree-bg); }
.tree-theme button:hover[aria-pressed="false"] { color: var(--tree-ink); }

/* the bar holds: copyright · language · colour scheme. Both controls are
   styled together further down — see "one component in two flavours". */
.container-footer .tree-footer-legal > * { flex: 0 0 auto; }
.container-footer .tree-footer-legal > p { margin-right: auto; }

/* ---------------------------------------------------------------- a11y */
:focus-visible { outline: 3px solid var(--tree-accent); outline-offset: 2px; }
.skip-link:focus, .skip:focus { background: var(--tree-accent); color: var(--tree-on-accent); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: 1ms !important; animation-duration: 1ms !important; }
}

@media print {
  .container-header, .container-footer, .sidebar-right, .sidebar-left { display: none; }
  body { background: #fff; color: #000; }
}

/* ---------------------------------------------------------------- imagery
   Photographs are content, so they come from the article body as
   <picture>/<img> — never as CSS backgrounds. That keeps alt text, lazy
   loading and the srcset in the hands of whoever edits the page, and keeps
   the images in the sitemap. These rules only decide how they sit. */
/* A picture with a caption. Since 3.27.0 it carries the same shell as the
   interactive map — 20px radius, one hairline — because the contact page's map
   became a picture, and a page should not change character just because a
   component stopped being scriptable. `.tree-map__links` is deliberately not
   scoped to `.tree-map`, so the button row under the map works here too. */
.tree-figure { margin-block: clamp(2.5rem, 6vw, 4.5rem); }
.tree-figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 20px;
  border: 1px solid var(--tree-hairline);
}
.tree-figure figcaption {
  margin-top: .75rem;
  font-size: .9375rem;
  color: var(--tree-ink-2);
  /* ⚠ No `max-width` here. A caption that stops at 60ch under a full-width
     picture reads as a stray paragraph rather than as part of the figure —
     reported 2026-09-17, and the same objection as the note under the contact
     form. The measure is already set by the container. */
}
.tree-figure .tree-map__links { margin-top: 1rem; }

/* ------------------------------------------- a picture in two colour schemes
   The contact map exists as a light and a dark rendering. ⚠ `<picture>` with a
   `media` source is the textbook answer and is wrong twice here: the editor on
   b.tree.bz strips `<picture>` and `<source>` (website-copy-en §3a), and a
   media query cannot see the site's own Light/Dark control, which is a data
   attribute on <html> and not a system preference. Two `<img>`s and a display
   rule answer both at once.

   ⚠ Both images must keep `loading="lazy"`. A lazy image inside a
   `display: none` box has no layout box, so it never intersects the viewport
   and is never fetched — which is what stops the page paying for the variant it
   does not show. Drop the attribute and the visitor downloads both. */
.tree-figure--themed .tree-figure__dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-figure--themed .tree-figure__light { display: none; }
  :root:not([data-theme="light"]) .tree-figure--themed .tree-figure__dark { display: block; }
  /* the dark rendering is already dark; §4.9's blanket dimming would flatten it */
  :root:not([data-theme="light"]) .tree-figure--themed .tree-figure__dark { filter: none; }
}
:root[data-theme="dark"] .tree-figure--themed .tree-figure__light { display: none; }
:root[data-theme="dark"] .tree-figure--themed .tree-figure__dark { display: block; filter: none; }
:root[data-theme="light"] .tree-figure--themed .tree-figure__light { display: block; }
:root[data-theme="light"] .tree-figure--themed .tree-figure__dark { display: none; }

/* Two escapes from the 1120 px measure.

   `.tree-wide` widens to the page gutters — up to 1200 px, safe everywhere.

   `.tree-bleed` goes edge to edge. ⚠ `100vw` includes the classic scrollbar,
   so on its own it would add a horizontal scrollbar on every desktop page.
   `overflow-x: clip` on the root absorbs that overshoot; unlike
   `overflow: hidden` it does not create a scroll container, so the sticky
   header keeps working. Browsers without `clip` (Safari < 16) fall back to
   `visible` and would show a small horizontal scroll on bleed pages — the
   reason `.tree-wide` exists as the conservative choice. */
html, body { overflow-x: clip; }

.tree-wide {
  width: calc(100% + 2 * var(--tree-gutter));
  max-width: none;
  margin-inline: calc(-1 * var(--tree-gutter));
}
.tree-bleed {
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
}
.tree-wide img,
.tree-bleed img {
  border-radius: 0;
  aspect-ratio: 2 / 1;
  object-fit: cover;
}
@media (width <= 700px) {
  /* a 2:1 band is a letterbox slit on a phone — crop taller instead */
  .tree-wide img,
  .tree-bleed img { aspect-ratio: 4 / 3; }
}
@media (width >= 1200px) {
  /* on a wide screen a 2:1 band is 700 px tall and swallows the fold */
  .tree-wide img,
  .tree-bleed img { aspect-ratio: 12 / 5; max-height: 68vh; }
}

/* Focal point: a centre crop cuts the subject off when it sits at one edge.
   Add one of these to the <img> in the article. */
.tree-focus-left   { object-position: 15% center; }
.tree-focus-right  { object-position: 80% center; }
/* ⚠ These are the only control over what a `cover` crop keeps, and the band
   they sit in changes height with the viewport, so the values are chosen to
   keep the subject inside the *shortest* window the component can have, not
   the average one. Retuned 2026-09-13 after trees were cut off at the top of
   the IT hero and at the foot of two closing bands. */
.tree-focus-top    { object-position: center 12%; }
.tree-focus-mid    { object-position: center 55%; }
.tree-focus-bottom { object-position: center 85%; }

/* inset band: stays inside the measure, rounded like the cards */
.tree-inset img {
  border-radius: var(--border-radius-xl);
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* photo tiles on cards */
.card { overflow: hidden; }
.card > .card-img-top {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 0;
}
.card:has(> .card-img-top) .card-body { padding-block-start: 1.25rem; }

/* Photographs sit a touch quieter in dark mode, like every other surface. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-figure img,
  :root:not([data-theme="light"]) .card-img-top { filter: brightness(.88) saturate(.92); }
}
:root[data-theme="dark"] .tree-figure img,
:root[data-theme="dark"] .card-img-top { filter: brightness(.88) saturate(.92); }

/* ------------------------------------------------- hero: text over a photo
   The photograph stays an <img> (content, alt text, srcset, sitemap) and is
   positioned behind the text rather than being a CSS background. A scrim
   between the two guarantees contrast, so the type is white in both colour
   schemes — the photograph, not the theme, is the surface here. */
.tree-hero {
  /* full-bleed on its own, so the band rules for `.tree-bleed img`
     (aspect-ratio, max-height) cannot fight the hero's own geometry */
  width: 100vw;
  max-width: 100vw;
  margin-inline: calc(50% - 50vw);
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: clamp(26rem, 62vh, 42rem);
  overflow: hidden;
  isolation: isolate;
  margin-block: 0 clamp(2.5rem, 6vw, 4.5rem);
}
.tree-hero--band {
  min-height: clamp(20rem, 46vh, 30rem);
  margin-block: clamp(2.5rem, 6vw, 4.5rem);
}
/* A service page opens with a photograph too, but shorter than the home page:
   the home hero is the one that gets the full 62 vh, so the subpages read as
   subpages. */
.tree-hero--page { min-height: clamp(21rem, 50vh, 32rem); }
.tree-hero > .tree-hero__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  /* ⚠ width/height attributes give the <img> an intrinsic aspect-ratio, and
     with it Chromium computes the height from the width and overflows the
     hero. The image is sized by the insets and cropped by object-fit. */
  aspect-ratio: auto;
  max-height: none;
  object-fit: cover;
  border-radius: 0;
}
/* The scrim. Measured, not guessed: with white type the darkest pixel behind
   any glyph has to stay at 4.5:1 or better. A single side gradient failed on
   the fog photograph (1.4:1 on a phone, where the text spans the full width),
   so the scrim is two layers — a side gradient for the wide layout and a
   bottom-up gradient that does the work when the text is full width — plus a
   flat veil that guarantees a floor on a bright photograph. */
.tree-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(90deg, rgb(0 0 0 / .52) 0%, rgb(0 0 0 / .38) 55%, rgb(0 0 0 / .22) 100%),
    linear-gradient(0deg, rgb(0 0 0 / .42) 0%, rgb(0 0 0 / .14) 62%, rgb(0 0 0 / 0) 100%),
    rgb(0 0 0 / .10);
}
/* A bright photograph — fog, snow, a pale sky — needs more scrim than a dusk
   shot. Add `tree-hero--bright` to the section rather than darkening every
   hero. Measured on the fog photograph: 4.56:1 without it, 6.0:1 with. */
.tree-hero--bright::after {
  background:
    linear-gradient(90deg, rgb(0 0 0 / .60) 0%, rgb(0 0 0 / .46) 55%, rgb(0 0 0 / .30) 100%),
    linear-gradient(0deg, rgb(0 0 0 / .50) 0%, rgb(0 0 0 / .20) 62%, rgb(0 0 0 / 0) 100%),
    rgb(0 0 0 / .14);
}

/* A page hero is a 2:1 crop, so the sky sits directly behind the headline on a
   wide screen where the standard scrim is tuned for a tall dusk photograph.
   Measured on the four service photographs: the base scrim gave 2.40:1 on the
   hill-and-clouds picture; these values put the worst pixel behind every
   glyph above the floor on all four, in both themes. */
@media (width > 700px) {
  .tree-hero--page::after {
    background:
      linear-gradient(90deg, rgb(0 0 0 / .64) 0%, rgb(0 0 0 / .50) 55%, rgb(0 0 0 / .34) 100%),
      linear-gradient(0deg, rgb(0 0 0 / .58) 0%, rgb(0 0 0 / .24) 66%, rgb(0 0 0 / 0) 100%),
      rgb(0 0 0 / .14);
  }
  .tree-hero--page.tree-hero--bright::after {
    background:
      linear-gradient(90deg, rgb(0 0 0 / .70) 0%, rgb(0 0 0 / .56) 55%, rgb(0 0 0 / .42) 100%),
      linear-gradient(0deg, rgb(0 0 0 / .64) 0%, rgb(0 0 0 / .32) 66%, rgb(0 0 0 / 0) 100%),
      rgb(0 0 0 / .18);
  }
}

@media (width <= 700px) {
  /* the text column is the whole width here, so the vertical layer carries it */
  .tree-hero::after {
    background:
      linear-gradient(0deg, rgb(0 0 0 / .66) 0%, rgb(0 0 0 / .44) 70%, rgb(0 0 0 / .26) 100%),
      rgb(0 0 0 / .08);
  }
  .tree-hero--bright::after {
    background:
      linear-gradient(0deg, rgb(0 0 0 / .74) 0%, rgb(0 0 0 / .54) 70%, rgb(0 0 0 / .36) 100%),
      rgb(0 0 0 / .12);
  }
}
.tree-hero__text {
  position: relative;
  width: 100%;
  max-width: var(--tree-container);
  margin-inline: auto;
  padding: clamp(2rem, 6vw, 4rem) var(--tree-gutter) clamp(2rem, 5vw, 3.5rem);
  color: #fff;
}
.tree-hero__text > * + * { margin-top: 1.25rem; }
.tree-hero h1,
.tree-hero h2,
.tree-hero h3 { color: #fff; }
.tree-hero h1 { font-size: clamp(2.25rem, 5vw, 3.75rem); }
/* The hero text runs the full measure. It was capped at 46 characters, which
   is right for a column of running text and wrong here: the container is
   already 1120 px and the photograph is 1440, so the paragraph stopped at
   roughly a third of the picture and the hero read as a left-hand panel.
   Reported 2026-09-12: "these are too left aligned, use full width."
   ⚠ This moves the end of every line out over the bright part of the
   photograph, so the horizontal scrim below had to be flattened to match —
   the two changes are one change. */
.tree-hero .lead,
.tree-hero p {
  color: rgb(255 255 255 / .88);
  max-width: 68ch;
}
@media (width >= 62em) {
  .tree-hero .lead,
  .tree-hero p { max-width: none; }
}
/* On a photograph the red button disappears into the scrim, so the hero uses
   the second company colour: silver, inverting to dark red on hover — the
   same behaviour as every other light button. */
.tree-hero .btn-primary {
  background: var(--tree-silver);
  border-color: var(--tree-silver);
  color: var(--tree-graphite);
}
.tree-hero .btn-primary:hover,
.tree-hero .btn-primary:focus-visible,
.tree-hero .btn-primary:active {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
/* ⚠ the dark-mode primary-hover rules are 0,4,0 and would otherwise turn this
   button red on hover; these repeat the inversion at 0,5,0 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-hero .btn-primary:hover,
  :root:not([data-theme="light"]) .tree-hero .btn-primary:focus-visible,
  :root:not([data-theme="light"]) .tree-hero .btn-primary:active {
    background: var(--tree-red-dark);
    border-color: var(--tree-red-dark);
    color: #ffffff;
  }
}
:root[data-theme="dark"] .tree-hero .btn-primary:hover,
:root[data-theme="dark"] .tree-hero .btn-primary:focus-visible,
:root[data-theme="dark"] .tree-hero .btn-primary:active {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
.tree-hero :focus-visible { outline-color: #fff; }
/* Photographs are damped in dark mode; a hero already carries a scrim, so it
   is damped once, not twice. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-hero > .tree-hero__img { filter: brightness(.92); }
}
:root[data-theme="dark"] .tree-hero > .tree-hero__img { filter: brightness(.92); }

/* --------------------------------------------- cards as one clickable area
   Bootstrap ships `.stretched-link` (an absolutely positioned ::after on the
   link) and Cassiopeia includes it, so the whole card becomes the hit area
   with one tab stop and a short accessible name — no JavaScript, and no
   nesting of headings inside a giant anchor. The card only has to be a
   containing block.

   Revised 2026-09-12: the old hover — a silver wash — was too quiet to read
   as "this whole box is a link". A hovered card now **fills**: the surface
   takes the company red, everything on it turns white, and the card lifts.
   Nobody can miss it.

   ⚙ One line switches the whole site between the two options that were on the
   table: dark red (`--tree-fill`) or the dark silver (`--tree-graphite`).
   Change `--tree-card-hover` below and every clickable box follows.

   Contrast on the filled card: white on #570f07 is 13.4:1, white on #6f1009
   (dark mode) is 12:1, white on #3f4445 is 9.6:1 — all far above the floor
   for the 15 px body text inside. */
:root { --tree-card-hover: var(--tree-fill); }   /* ← or var(--tree-graphite) */

.card:has(.stretched-link) {
  position: relative;
  transition: background-color .18s ease, border-color .18s ease,
              translate .18s ease, box-shadow .18s ease;
}
.card:has(.stretched-link):hover,
.card:has(.stretched-link:focus-visible) {
  background: var(--tree-card-hover);
  translate: 0 -2px;
  /* no inset ring here: that is what removes the line around the photograph */
  box-shadow: 0 10px 26px rgb(0 0 0 / .22);
}
/* everything on the filled card, including what normally carries a colour */
.card:has(.stretched-link):hover :is(h2, h3, h4, p, li, span, a),
.card:has(.stretched-link:focus-visible) :is(h2, h3, h4, p, li, span, a) {
  color: #ffffff;
}
/* ⚠ The card heading is a link, and `.container-component .card h3 a:hover`
   (0,3,2) outranks the rule above (0,3,1) — which made the heading dark red
   on a dark red card, i.e. invisible. Measured, then fixed with a selector
   that carries the container class too. */
.container-component .card:has(.stretched-link):hover h3,
.container-component .card:has(.stretched-link):hover h3 a,
.container-component .card:has(.stretched-link:focus-visible) h3,
.container-component .card:has(.stretched-link:focus-visible) h3 a,
.container-component .card:has(.stretched-link):hover p,
.container-component .card:has(.stretched-link:focus-visible) p { color: #ffffff; }

/* the icon badge inverts with it: a translucent white pane, white glyph */
.card:has(.stretched-link):hover .tree-ico::before,
.card:has(.stretched-link:focus-visible) .tree-ico::before {
  background: rgb(255 255 255 / .16);
  color: #ffffff;
}
/* and so does a tick marker, if a clickable card ever carries a list */
.card:has(.stretched-link):hover .tree-feats > li::before,
.card:has(.stretched-link:focus-visible) .tree-feats > li::before {
  color: #ffffff;
}
.card:has(.stretched-link):hover :is(.tree-price, h3.tree-tld),
.card:has(.stretched-link:focus-visible) :is(.tree-price, h3.tree-tld) { color: #ffffff; }
.card:has(.stretched-link):hover .tree-chips > li,
.card:has(.stretched-link:focus-visible) .tree-chips > li {
  border-color: rgb(255 255 255 / .35);
  background: transparent;
}
/* The photograph goes soft and steps back, so the words on top of the filled
   card are the thing you read. The scale is 1.08 rather than 1.03 because a
   blur samples past the edges of its own box: without the extra size the
   blurred border of the image would show as a pale fringe inside the card. */
.card:has(.stretched-link) img { transition: scale .45s ease, filter .3s ease; }
.card:has(.stretched-link):hover img,
.card:has(.stretched-link:focus-visible) img {
  scale: 1.08;
  filter: blur(3px) saturate(.9);
}

/* Boxes that are **not** links do not react to the pointer. A hover state on
   something you cannot click is a promise the page does not keep. */

/* the focus ring belongs around the whole card, not around the words */
.card:has(.stretched-link:focus-visible) {
  outline: 3px solid var(--tree-accent);
  outline-offset: 2px;
}
.card .stretched-link:focus-visible { outline: none; }
@media (prefers-reduced-motion: reduce) {
  .card:has(.stretched-link),
  .card:has(.stretched-link) img { transition: none; translate: 0; }
  .card:has(.stretched-link):hover img,
  .card:has(.stretched-link:focus-visible) img { scale: 1.02; }
}

/* ------------------------------------------- a page that opens with a hero
   Measured on staging: 310 px of empty space between the header and the top of
   the hero — 56 px of container padding, 32 px on <main>, 40 px on
   `.item-page`, a 39 px breadcrumb row and a 91 px "Details" block. On a page
   whose first element is a photograph none of that belongs there.

   `:has()` scopes every rule to exactly that case, so ordinary pages keep
   their breadcrumbs, their page heading and their spacing. Nothing is
   hard-coded to a menu item, and switching the article back to a text opening
   restores everything by itself. */
.container-component:has(.tree-hero:first-child) { padding-block-start: 0; }
.container-component:has(.tree-hero:first-child) > main { margin-top: 0; }
.com-content-article:has(> .com-content-article__body > .tree-hero:first-child) { padding-top: 0; }

/* Breadcrumbs and the article's metadata line are chrome for a subpage, not
   for a landing page — and the duplicate page heading is an SEO defect next
   to the hero's own <h1>. All three can also be switched off in Joomla
   (menu item → Page Display, Articles → Options, Breadcrumbs module
   assignment); this only makes the hero layout correct without waiting for
   that. */
.container-component:has(.tree-hero:first-child) .mod-breadcrumbs__wrapper { display: none; }
.container-component:has(.tree-hero:first-child) .page-header,
.com-content-article:has(> .com-content-article__body > .tree-hero:first-child) > .article-info,
.com-content-article:has(> .com-content-article__body > .tree-hero:first-child) > .icons { display: none; }

/* The hero then sits flush under the sticky header; the section after it keeps
   the normal rhythm. */
.container-component:has(.tree-hero:first-child) .tree-hero:first-child { margin-block-start: 0; }

/* ------------------------------------------------------------ legal pages
   Impressum and Datenschutz are dense reference documents, not landing
   pages: the display-sized h2 of the marketing pages shouts, and a 62 ch
   measure makes a 14-section document endless. Add `tree-legal` to a wrapper
   div in the article body to get document typography instead. */
.tree-legal h2 {
  font-size: 1.375rem;
  letter-spacing: -0.015em;
  line-height: 1.3;
  margin-top: clamp(2rem, 3.5vw, 2.75rem);
}
.tree-legal h2 + p,
.tree-legal h2 + ul { margin-top: .75rem; }
.tree-legal p,
.tree-legal li { max-width: 74ch; }
.tree-legal p { margin-bottom: .75rem; }
.tree-legal ul { margin-top: .75rem; padding-left: 1.25rem; }
.tree-legal li { margin-bottom: .35rem; }
.tree-legal code {
  font-size: .9375em;
  padding: .1em .35em;
  border-radius: 6px;
  background: var(--tree-bg-alt);
}
.tree-legal a { color: var(--tree-link); }

/* ------------------------------------------- icons and visualisations
   Joomla ships Font Awesome 6 Free self-hosted in
   `media/vendor/fontawesome-free/webfonts/` and loads
   `media/system/css/joomla-fontawesome.min.css` on every page, so a glyph
   costs no extra request and no third-party connection. The editor on the
   live site strips `<svg>` and `<picture>`, so icons are drawn by CSS from a
   class on an element that already exists — nothing to strip, nothing to
   paste twice.

   Usage in an article body: put `tree-ico tree-i-NAME` on the heading.

       <h3 class="tree-ico tree-i-shield">IT security and GDPR</h3>

   The badge is a rounded square in the second company colour with the glyph
   in the first; on black the ground darkens instead, because silver there
   would be a glaring panel. */
.tree-ico::before {
  content: var(--tree-ico, "\f00c");
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  font-style: normal;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin-bottom: .85rem;
  border-radius: 14px;
  background: var(--tree-ico-bg);
  color: var(--tree-ico-fg);
  font-size: 1.0625rem;
  line-height: 1;
}
/* a section heading carries a slightly larger badge */
h2.tree-ico::before { width: 3rem; height: 3rem; border-radius: 15px; font-size: 1.1875rem; }
/* inside a card the badge lines up with the card's text, not its padding */
.card .tree-ico::before { margin-bottom: .7rem; }

/* the glyph set actually used by the pages — every codepoint below is one
   Joomla's own stylesheet references, so it is guaranteed to be in the font */
.tree-i-support   { --tree-ico: "\f1cd"; }  /* life ring — support        */
.tree-i-desktop   { --tree-ico: "\f390"; }  /* workstation               */
.tree-i-mobile    { --tree-ico: "\f3ce"; }  /* phone, device management  */
.tree-i-wifi      { --tree-ico: "\f1eb"; }  /* network                   */
.tree-i-plug      { --tree-ico: "\f1e6"; }  /* infrastructure            */
.tree-i-cloud     { --tree-ico: "\f0c2"; }  /* cloud services            */
.tree-i-cube      { --tree-ico: "\f1b2"; }
.tree-i-cubes     { --tree-ico: "\f1b3"; }
.tree-i-database  { --tree-ico: "\f1c0"; }
/* A larger badge, for a box whose whole point is the icon — a photograph in
   the same place would make the box look clickable, which these are not.
   Reported 2026-09-12: photo tiles on the Web page "suggest that the items are
   clickable (similar at the start page)". On the home page the tiles *are*
   links; here they are not, so the picture goes and the icon grows. */
.tree-ico--lg::before {
  width: 4rem;
  height: 4rem;
  border-radius: 20px;
  font-size: 1.65rem;
  margin-bottom: 1.1rem;
}

.tree-i-shield    { --tree-ico: "\f3ed"; }  /* security                  */
.tree-i-userlock  { --tree-ico: "\f502"; }  /* privacy of the holder     */
.tree-i-unlock    { --tree-ico: "\f09c"; }  /* leaving is allowed        */
.tree-i-key       { --tree-ico: "\f084"; }  /* certificate, credential   */
.tree-i-check     { --tree-ico: "\f058"; }  /* what you get              */
.tree-i-users     { --tree-ico: "\f0c0"; }  /* who it is for             */
.tree-i-user      { --tree-ico: "\f007"; }  /* a person, not a form      */
.tree-i-handshake { --tree-ico: "\f2b5"; }  /* how it is engaged         */
.tree-i-comments  { --tree-ico: "\f075"; }  /* plain language            */
.tree-i-envelope  { --tree-ico: "\f0e0"; }  /* mail                      */
.tree-i-globe     { --tree-ico: "\f0ac"; }  /* Europe, domains           */
.tree-i-bolt      { --tree-ico: "\f0e7"; }  /* fast                      */
.tree-i-search    { --tree-ico: "\f002"; }  /* findable                  */
.tree-i-eye       { --tree-ico: "\f06e"; }
.tree-i-code      { --tree-ico: "\f121"; }  /* open source               */
.tree-i-cog       { --tree-ico: "\f013"; }  /* the server is our job     */
.tree-i-cogs      { --tree-ico: "\f085"; }
.tree-i-tools     { --tree-ico: "\f0ad"; }
.tree-i-sync      { --tree-ico: "\f021"; }  /* migration, renewal        */
.tree-i-archive   { --tree-ico: "\f187"; }  /* backup                    */
.tree-i-download  { --tree-ico: "\f019"; }  /* transfer in               */
.tree-i-chart     { --tree-ico: "\f1fe"; }  /* traffic                   */
.tree-i-clock     { --tree-ico: "\f017"; }
.tree-i-lightbulb { --tree-ico: "\f0eb"; }  /* concept                   */
.tree-i-palette   { --tree-ico: "\f1fc"; }  /* design                    */
.tree-i-pencil    { --tree-ico: "\f044"; }  /* content                   */
.tree-i-location  { --tree-ico: "\f3c5"; }  /* region                    */
.tree-i-tree      { --tree-ico: "\f126"; }
/* added 2026-09-12 for the references page — one glyph per client's field */
.tree-i-heart     { --tree-ico: "\f4be"; }  /* hand holding a heart      */
.tree-i-basket    { --tree-ico: "\f291"; }  /* food bank                 */
.tree-i-leaf      { --tree-ico: "\f06c"; }  /* naturopathy               */
.tree-i-wine      { --tree-ico: "\f4e3"; }  /* wine seminars             */
.tree-i-home      { --tree-ico: "\f015"; }  /* a farm, an estate         */
.tree-i-stetho    { --tree-ico: "\f0f1"; }  /* a practice                */
.tree-i-bank      { --tree-ico: "\f19c"; }  /* an institution            */
.tree-i-file      { --tree-ico: "\f15c"; }  /* documentation             */
.tree-i-phone     { --tree-ico: "\f098"; }
.tree-i-tag       { --tree-ico: "\f02b"; }  /* price                     */

/* ---------------------------------------------------------- feature lists
   A `<ul>` of package features rendered as ticks instead of bullet points:
   the marker says "included", which a dot does not. Add `tree-feats` to the
   list; no per-item markup. */
.tree-feats {
  list-style: none;
  padding-left: 0;
  margin-top: 1rem;
}
.tree-feats > li {
  position: relative;
  padding-left: 1.85rem;
  margin-bottom: .45rem;
  font-size: var(--fs-small, .9375rem);
  color: var(--tree-ink-2);
}
/* A check mark, drawn at text weight — not a filled disc. The disc version
   (3.5.0) read as a sticker at 20 px and was rejected on sight. The colour
   problem it was solving is solved by `--tree-mark` instead: dark red on
   white, white on black. */
.tree-feats > li::before {
  content: "\f00c";
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  -webkit-font-smoothing: antialiased;
  position: absolute;
  left: 0;
  top: .18em;
  color: var(--tree-mark);
  font-size: .82em;
}
/* the same list, used for what a service does not include */
.tree-feats--no > li::before { content: "\f00d"; color: var(--tree-ink-2); }

/* ---------------------------------------------------------------- steps
   An ordered list drawn as a numbered sequence: the numeral comes from a CSS
   counter in a filled red disc, and a hairline connects one step to the next.
   Add `tree-steps` to the `<ol>`; each `<li>` holds an `<h3>` and a `<p>`.
   Below 62 em the steps stack and the connector runs vertically, so the
   sequence stays readable on a phone. */
.tree-steps {
  list-style: none;
  padding-left: 0;
  margin-top: 2rem;
  counter-reset: tree-step;
  display: grid;
  gap: 1.75rem;
}
.tree-steps > li {
  counter-increment: tree-step;
  position: relative;
  padding-left: 3.75rem;
  min-height: 2.75rem;
}
.tree-steps > li::before {
  content: counter(tree-step);
  position: absolute;
  left: 0;
  top: 0;
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--tree-fill);
  color: #ffffff;
  font-weight: 600;
  font-size: 1.0625rem;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
/* the connector: vertical while stacked */
.tree-steps > li::after {
  content: "";
  position: absolute;
  left: 1.375rem;
  top: 3.1rem;
  bottom: -2.1rem;
  width: 1px;
  background: var(--tree-hairline);
}
.tree-steps > li:last-child::after { display: none; }
.tree-steps > li > h3 { margin-top: .35rem; margin-bottom: .3rem; }
.tree-steps > li > p {
  margin-bottom: 0;
  color: var(--tree-ink-2);
  font-size: var(--fs-small, .9375rem);
}

@media (min-width: 62em) {
  /* one row, whatever the number of steps — three on the home page, four on
     the web page — and the connector turns horizontal */
  .tree-steps {
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: 2rem;
  }
  .tree-steps > li { padding-left: 0; padding-top: 3.75rem; }
  .tree-steps > li::after {
    left: 3.25rem;
    right: -2rem;
    top: 1.375rem;
    bottom: auto;
    width: auto;
    height: 1px;
  }
  .tree-steps > li > h3 { margin-top: 0; }
}

/* ------------------------------------------------------- price and suffix
   Two small display treatments so a number or a domain suffix carries the
   page instead of a bullet list: the price in the brand red at display size,
   the suffix as the card's whole visual. */
/* The price is figures, not a badge: no box, no chip, nothing around it. It
   was briefly given a filled surface to solve the dark-mode colour and the
   review was right that it looked worse — the number should carry the card by
   itself. The colour comes from `--tree-display`: the company dark red on
   white, plain off-white on black. */
.tree-price {
  display: block;
  color: var(--tree-display);
  font-size: clamp(1.75rem, 3.4vw, 2.25rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
  font-variant-numeric: tabular-nums;
  margin-bottom: .1rem;
}
.tree-price-note {
  display: block;
  font-size: .8125rem;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--tree-ink-2);
}
.card h3.tree-tld {
  color: var(--tree-display);
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  margin-bottom: .5rem;
  /* A domain ending is one token: ".cloud" broken across two lines reads as
     two different endings. The card is narrow, so the name never wraps and
     the description below it carries the width instead. */
  white-space: nowrap;
}

/* ------------------------------------------------------ lists in documents
   The legal pages are the one place where a real list is the right element,
   but the browser's dot is not: a short red rule reads as a company mark and
   keeps the paragraph rhythm. Numbered lists there keep their numerals, in
   red, because the numbering is referenced in the text. */
.tree-legal ul { list-style: none; padding-left: 1.25rem; }
.tree-legal ul > li { position: relative; }
.tree-legal ul > li::before {
  content: "";
  position: absolute;
  left: -1.25rem;
  top: .72em;
  width: .6rem;
  height: 2px;
  background: var(--tree-mark);
}
.tree-legal ol > li::marker { color: var(--tree-accent); font-weight: 600; }

/* ------------------------------------------------------------ contact page
   The contact page is `com_contact`, not an article: core renders the address
   block and the e-mail form, and everything below styles that output. No
   layout override — the only markup file in this template stays the menu.

   Page composition (see `claude/website-copy-en.md` §9):
     main-top module   → the photograph with the <h1> over it
     component         → "How to reach us" (the contact record) + the form
     main-bottom module→ what happens after you send

   The component's two blocks become two columns on a wide screen: the form
   where the eye lands, the address card beside it. */
.com-contact {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3rem);
  align-items: start;
}
@media (min-width: 62em) {
  .com-contact {
    grid-template-columns: minmax(0, 1fr) 20rem;
    /* ⚠ Two rows, and the sizing is the whole trick: row 1 is `min-content`
       (the address card) and row 2 is `1fr`, so when the form — which spans
       both rows — is taller than the column beside it, **all** of the extra
       height lands in row 2. The second card is therefore free to sit at the
       top of row 2, i.e. directly under the address card, instead of being
       pushed down with it. Earlier attempts left 353 px and 690 px of hole. */
    grid-template-rows: min-content 1fr;
    column-gap: clamp(2rem, 5vw, 4rem);
    row-gap: 1.5rem;
  }
  /* the form heading and the form itself take the wide column; the record's
     own block becomes the card beside them, spanning both rows */
  /* Three rows, and the two tall blocks overlap in the middle one: the record
     card spans rows 1–2, the form spans rows 2–3. That is what puts the misc
     card (row 3) directly under the record card whatever the form's height —
     placing it in a row of its own below the form instead left it stranded at
     the foot of the page, measured 690 px below the card. */
  /* Core prints an `<h2>Contact Form</h2>` above the form. On a page whose
     hero already says "Talk to a person" it is a redundant label, and keeping
     it would force the form into its own row and undo the arrangement above.
     The form keeps its `<legend>`, so it is still named for screen readers. */
  .com-contact > h2:has(+ .com-contact__form) { display: none; }
  .com-contact > .com-contact__form { grid-column: 1; grid-row: 1 / span 2; margin-top: 0; }
  .com-contact > .com-contact__container { grid-column: 2; grid-row: 1; }
  .com-contact > .page-header,
  .com-contact > .com-contact__tags { grid-column: 1 / -1; }
  /* The record's "Other information" belongs **beside the form**, not under
     it. It cannot sit directly under the address card — the card and this
     block are siblings of a tall form, and a grid cannot stack two siblings in
     one column independently of that neighbour (measured: 353 px and 690 px
     below the card, whichever placement was tried). Bottom-aligning it in the
     same row as the form puts it in the right column, level with the end of
     the form, which is where the column had its empty space. */
  .com-contact > .com-contact__miscinfo {
    grid-column: 2;
    grid-row: 2;
    align-self: start;
    margin-top: 0;
  }
}

/* the record's "Other information": the same card as the record above it */
.com-contact__miscinfo {
  margin-top: .5rem;
  background: var(--tree-bg-alt);
  border: 1px solid var(--tree-hairline);
  border-radius: 20px;
  padding: 1.25rem 1.5rem;
}
/* in a 20 rem column the fact boxes stack; on a phone they spread again */
@media (min-width: 62em) {
  .com-contact__miscinfo .tree-facts { grid-template-columns: 1fr; gap: .5rem; }
  .com-contact__miscinfo .tree-facts > li { background: var(--tree-bg); }
}
.com-contact__miscinfo dl { margin: 0; }
.com-contact__miscinfo dt { display: none; }          /* the "Other information" label */
.com-contact__miscinfo dd { margin: 0; }
.com-contact__miscinfo .tree-facts { margin-top: 0; }
.com-contact__miscinfo p:first-child { margin-top: 0; }
/* core's own heading above it repeats what the content says */
.com-contact > :is(h2, h3):has(+ .com-contact__miscinfo) { display: none; }

/* the vCard link core offers inside the card: a small, quiet line, not a
   sentence with a link in the middle of it */
.com-contact__info {
  display: flex;
  flex-direction: column;
  gap: .9rem;
  /* core prints "Download information as:" as a bare text node before the
     link — it cannot be selected, so it is demoted to a caption instead */
  font-size: .8125rem;
  color: var(--tree-ink-2);
}
.com-contact__info > .contact-address,
.com-contact__info > dl { font-size: var(--fs-small, .9375rem); color: var(--tree-ink); }
.com-contact__info > a {
  align-self: start;
  color: var(--tree-ink);
  text-decoration: none;
  border: 1px solid var(--tree-hairline);
  border-radius: 999px;
  padding: .45rem 1rem;
  min-height: 38px;
  display: inline-flex;
  align-items: center;
}
.com-contact__info > a:hover {
  background: var(--tree-fill);
  border-color: var(--tree-fill);
  color: #ffffff;
}
/* the position line above the address */
.com-contact__position {
  margin: 0 0 1rem;
  font-size: var(--fs-small, .9375rem);
  color: var(--tree-ink-2);
}
.com-contact__position dt { display: none; }
.com-contact__position dd { margin: 0; }

/* --- the record: a quiet card, not a definition list with icons in it --- */
.com-contact__container {
  background: var(--tree-bg-alt);
  border: 1px solid var(--tree-hairline);
  border-radius: 20px;
  padding: 1.75rem;
}
/* the form heading is a section label, not a display headline, and the grid
   gap already provides its space */
.com-contact > h2 {
  font-size: clamp(1.5rem, 2.2vw, 1.875rem);
  letter-spacing: -0.02em;
  margin-top: 0;
}
.com-contact__container > h2,
.com-contact__container > h3 {
  font-size: 1.3125rem;
  letter-spacing: -0.015em;
  margin: 0 0 1rem;
}
/* dt (the icon) in the narrow column, every dd of that group beside it */
.com-contact .contact-address {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  row-gap: .15rem;
  margin: 0;
  font-size: var(--fs-small, .9375rem);
}
/* the glyph sits on the first line of its group, not above it: same line box
   as the value beside it */
.com-contact .contact-address dt {
  grid-column: 1;
  /* reported twice as "the icon colour on the right is still wrong": this was
     the salmon tint on the dark card. It is a marker with nothing behind it,
     so it takes `--tree-mark` — dark red on the silver card, white on the
     dark one. */
  color: var(--tree-mark);
  font-weight: 400;
  font-size: .9375rem;
  line-height: 1.55;
  display: flex;
  align-items: center;
  min-height: 1.55em;
}
.com-contact .contact-address dd {
  grid-column: 2;
  margin: 0;
  color: var(--tree-ink);
  line-height: 1.55;
}
/* a gap between one contact method and the next, not between the lines of
   an address */
.com-contact .contact-address dt:not(:first-of-type) { margin-top: .85rem; }
.com-contact .contact-address dt:not(:first-of-type) + dd { margin-top: .85rem; }
.com-contact .contact-address a { color: var(--tree-ink); text-decoration: none; }
.com-contact .contact-address a:hover { color: var(--tree-accent); text-decoration: underline; }
/* ⚠ no `white-space: pre-line` here. Core already converts the field's line
   breaks with nl2br(), and the layout indents the span across two source
   lines — pre-line would turn that leading newline into a blank first line and
   push the address one line below its icon. */
.com-contact .contact-street { display: inline-block; line-height: 1.55; }

/* --- the form ----------------------------------------------------------
   Core marks the form `.well`, which our surfaces rule paints silver. On this
   page the card beside it is the silver element, so the form sits on the page
   itself: labels above their field, one column, a sane measure. */
.com-contact__form.contact-form,
.com-contact__form .well,
.com-contact__form form.well {
  background: none;
  border: 0;
  padding: 0;
  max-width: 38rem;
}
.com-contact__form fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
/* the legend repeats the heading above it */
.com-contact__form legend {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.com-contact__form .control-group + .control-group { margin-top: 1.15rem; }
.com-contact__form .control-label label {
  display: inline-block;
  margin-bottom: .35rem;
  font-size: .9375rem;
  font-weight: 500;
  color: var(--tree-ink);
}
.com-contact__form .star,
.com-contact__form .red { color: var(--tree-accent); }
/* "* Required field" is a note, not a label */
.com-contact__form .field-spacer label {
  font-size: .8125rem;
  font-weight: 400;
  color: var(--tree-ink-2);
  margin-bottom: 0;
}
.com-contact__form .field-spacer + .control-group { margin-top: .85rem; }
.com-contact__form textarea.form-control { min-height: 11rem; resize: vertical; }
.com-contact__form .form-control { width: 100%; }
/* Joomla's client-side validation marks a field `invalid` */
.com-contact__form .form-control.invalid,
.com-contact__form .invalid .form-control {
  border-color: var(--tree-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--tree-accent) 18%, transparent);
}
.com-contact__form .control-group:has(> .controls > button),
.com-contact__form .control-group:has(> .controls > .btn) { margin-top: 1.75rem; }

/* Any checkbox row — core's "send a copy to yourself" and a consent field
   added as a custom field — is one line: box first, then its label. Core puts
   the label in `.control-label` and the box in `.controls`, so the row is
   reversed rather than re-ordered by hand. */
/* ⚠ `:not(:has(altcha-widget))` matters: the captcha's own markup contains a
   checkbox, and without the exclusion the row-reverse rule below shrink-wraps
   the widget to 180 px. Measured, not guessed. */
.com-contact__form .control-group:has(> .controls input[type="checkbox"]):not(:has(altcha-widget)):not(:has(.checkboxes)):not(.tree-consent) {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  align-items: baseline;
  gap: .6rem;
}
.com-contact__form .control-group:has(> .controls input[type="checkbox"]):not(:has(altcha-widget)):not(:has(.checkboxes)):not(.tree-consent) > .controls { flex: none; }
.com-contact__form .control-group:has(> .controls input[type="checkbox"]):not(:has(altcha-widget)):not(:has(.checkboxes)):not(.tree-consent) .control-label label {
  margin-bottom: 0;
  font-weight: 400;
  color: var(--tree-ink-2);
}
.com-contact__form .form-check-inline { display: block; margin: 0; }

/* A custom field of type "Checkboxes" prints its own text beside the box and
   repeats the field title in the control label above, which is redundant.

   ⚠ It cannot simply be hidden: `validate.js` injects the error message
   (`span.form-control-feedback`) **into that very label**, so `display: none`
   would hide the message with it. The label is therefore emptied — zero font
   size, no margin, so it collapses to nothing — and the injected chip is given
   its size back. `column-reverse` then places the message under the checkbox
   rather than above it. */
.com-contact__form .control-group:has(.checkboxes) {
  display: flex;
  flex-direction: column-reverse;
  gap: .35rem;
}
.com-contact__form .control-group:has(.checkboxes) > .control-label,
.com-contact__form .control-group:has(.checkboxes) > .control-label label {
  font-size: 0;
  margin: 0;
  line-height: 0;
}
.com-contact__form .control-group:has(.checkboxes) > .control-label .form-control-feedback {
  font-size: .78rem;
  line-height: 1.4;
  margin-left: 0;
}
/* the box itself takes the company red while the consent is missing */
.com-contact__form fieldset.checkboxes.invalid .form-check-input {
  outline: 2px solid var(--tree-fill);
  outline-offset: 3px;
}

/* ------------------------------------------------------- the consent tick
   Part of the form's own markup since 3.20.0 (`html/com_contact/contact/
   default_form.php`), not a custom field: a custom field on a contact form
   only works once the Public group holds "Edit Custom Field Value", which is
   a permission this site has no reason to hand out.

   `validate.js` appends its message inside `label[for="tree-consent"]` — the
   sentence itself — so the chip is pushed onto its own line under it rather
   than trailing the last word. */
.com-contact__form .tree-consent { margin-top: 1.25rem; }
.com-contact__form .tree-consent .form-check-label { display: block; }
.com-contact__form .tree-consent .form-control-feedback {
  display: flex;
  width: fit-content;
  margin: .45rem 0 0;
}
.com-contact__form .tree-consent .form-check-input.invalid {
  outline: 2px solid var(--tree-fill);
  outline-offset: 3px;
}

/* a consent checkbox added as a custom field (context: Mail) */
.com-contact__form .form-check,
.com-contact__form .checkboxes .form-check {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  margin-top: .25rem;
}
.com-contact__form input[type="checkbox"] {
  inline-size: 1.15rem;
  block-size: 1.15rem;
  margin-top: .2rem;
  accent-color: var(--tree-fill);
  flex: none;
}
.com-contact__form .form-check-label,
.com-contact__form .checkboxes label {
  font-size: .9375rem;
  font-weight: 400;
  color: var(--tree-ink-2);
  margin: 0;
}
.com-contact__form .form-check-label a { color: var(--tree-link); }

/* the note under the form: what happens to the data */
.tree-formnote {
  margin-top: 1rem;
  font-size: .8125rem;
  line-height: 1.55;
  color: var(--tree-ink-2);
  /* centred on its own measure: as a full-width line at the left edge it read
     as a stray paragraph rather than a note belonging to the form above it */
  max-width: 46rem;
  margin-inline: auto;
  text-align: center;
}

/* ------------------------------------------- a hero delivered by a module
   On the contact page the component is `com_contact`, so the photograph
   cannot live in an article body — it is a Custom HTML module in `main-top`.
   That puts the hero above the component but below the breadcrumb row, so the
   chrome rules of §4.10 need the module case too.

   ⚠ Cassiopeia renders `main-top` with `style="card"`. The module must be set
   to **Module Style = None** (Advanced tab); the second block below repairs
   the layout if it is not, rather than letting a full-bleed band sit inside a
   padded card. */
/* ⚠ Measured, not assumed: a module in `main-top` is wrapped twice, and with
   the position's own chrome three times —
       .container-component > .main-top.card > .card-body > .mod-custom > .tree-hero
   and with Module Style = None still —
       .container-component > .mod-custom > .tree-hero
   so a rule keyed on `> .tree-hero` matches nothing and the container keeps its
   top padding: an empty band above the photograph. These rules therefore look
   for the hero at any depth **outside `<main>`**, which is what "the hero came
   from a module" actually means, and neutralise whatever wrapper it arrived
   in. */
.container-component:has(> :not(main) .tree-hero) { padding-block-start: 0; }
.container-component:has(> :not(main) .tree-hero) .mod-breadcrumbs__wrapper { display: none; }
/* Any module of ours above or below the component is content, not a card:
   the position's chrome is stripped whatever it is, so a forgotten
   "Module Style = None" is a non-event rather than a photograph in a box. */
.container-component > :not(main):has(:is(.tree-hero, .tree-steps, .tree-map, .tree-formnote)) {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.container-component > :not(main):has(:is(.tree-hero, .tree-steps, .tree-map, .tree-formnote)) > .card-body,
.container-component > :not(main):has(:is(.tree-hero, .tree-steps, .tree-map, .tree-formnote)) .mod-custom { padding: 0; margin: 0; }
.container-component > :not(main) .tree-hero:first-child { margin-block-start: 0; }
/* the module that follows the component keeps the page rhythm */
.container-component > :not(main):has(:is(.tree-steps, .tree-formnote)) { margin-block-start: clamp(2rem, 5vw, 3rem); }

/* --------------------------------------------------------------- the map
   A real slippy map — pan, zoom levels, marker — whose tiles come out of a
   folder on this server. Nothing is requested from a tile provider, so the
   page keeps its no-third-party property: no consent gate, and the only
   obligation is the OpenStreetMap credit, which the script paints into the
   corner. See `js/tree-map.js` and `claude/contact-page.md` §2.9.

   Without JavaScript the container holds only its links, which is why the
   address and the route link live outside the map, not inside it.          */
.tree-map { margin-block: clamp(1.5rem, 3vw, 2.25rem); }
.tree-map__view {
  position: relative;
  overflow: hidden;
  border-radius: 20px;
  border: 1px solid var(--tree-hairline);
  aspect-ratio: 16 / 9;
  /* a vertical swipe still scrolls the page — the map only takes the
     horizontal drag. A map that traps the scroll is a trap. */
  touch-action: pan-y;
  cursor: grab;
  background: var(--tree-bg-alt);
  isolation: isolate;
  margin-bottom: 1rem;
}
.tree-map__view.is-grabbing { cursor: grabbing; }
.tree-map__view:focus-visible { outline: 3px solid var(--tree-accent); outline-offset: 2px; }
.tree-map__layer { position: absolute; inset: 0; }
.tree-map__tile {
  position: absolute;
  top: 0;
  left: 0;
  width: 256px;
  height: 256px;
  max-width: none;
  border: 0;
  border-radius: 0;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}
/* the marker: the company red, drawn in CSS, no icon file. The tip of the
   teardrop sits on the coordinate. */
.tree-map__pin {
  position: absolute;
  top: 0;
  left: 0;
  width: 20px;
  height: 20px;
  margin: -20px 0 0 -10px;
  border-radius: 50% 50% 50% 0;
  background: var(--tree-fill);
  box-shadow: 0 0 0 2px rgb(255 255 255 / .92), 0 3px 8px rgb(0 0 0 / .38);
  pointer-events: none;
  z-index: 1;
}
.tree-map__pin::after {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  background: #fff;
  rotate: 45deg;
}
.tree-map__ctrl {
  position: absolute;
  top: .75rem;
  inset-inline-end: .75rem;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  z-index: 2;
}
.tree-map__ctrl button {
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  font: inherit;
  font-size: 1.05rem;
  line-height: 1;
  border-radius: 10px;
  border: 1px solid var(--tree-hairline);
  background: var(--tree-bg);
  color: var(--tree-ink);
  cursor: pointer;
}
.tree-map__ctrl button:hover:not(:disabled) {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #fff;
}
.tree-map__ctrl button:disabled { opacity: .45; cursor: default; }
/* the credit is a licence condition, so it is painted on the map itself */
.tree-map__credit {
  position: absolute;
  inset-inline-end: 0;
  bottom: 0;
  margin: 0;
  padding: .2rem .5rem;
  font-size: .6875rem;
  line-height: 1.4;
  color: var(--tree-ink-2);
  background: rgb(255 255 255 / .82);
  border-start-start-radius: 8px;
  z-index: 2;
}
.tree-map__note {
  margin: 0 0 .6rem;
  font-size: .8125rem;
  color: var(--tree-ink-2);
}
.tree-map__links {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  margin-top: 0;
  max-width: none;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-map__tile { filter: brightness(.88) saturate(.9); }
  :root:not([data-theme="light"]) .tree-map__credit { background: rgb(0 0 0 / .7); }
}
:root[data-theme="dark"] .tree-map__tile { filter: brightness(.88) saturate(.9); }
:root[data-theme="dark"] .tree-map__credit { background: rgb(0 0 0 / .7); }

/* 16:9 is a letterbox on a phone; a map wants height there */
@media (width <= 48em) {
  .tree-map__view { aspect-ratio: 4 / 3; }
}

/* ------------------------------------------------------------ the captcha
   Whichever self-hosted captcha ends up in the form, it arrives as one more
   `.control-group` inside it. This gives it the same rhythm as the fields and
   keeps a widget from spilling out of the column. ALTCHA's documented custom
   properties are set here too; a property a plugin does not use costs
   nothing. */
.com-contact__form .control-group:has(altcha-widget),
.com-contact__form .control-group:has(.altcha),
.com-contact__form .control-group:has(.captcha) { margin-top: 1.5rem; }
/* Joomla 6.1 ships ALTCHA in core as the "Proof of Work Captcha" plugin, with
   the widget self-hosted at media/vendor/altcha/. These are its own documented
   custom properties, read out of that stylesheet, so the widget takes the
   site's palette instead of its grey default. */
.com-contact__form altcha-widget,
.com-contact__form .altcha {
  display: block;
  --altcha-max-width: 38rem;
  --altcha-border-width: 1px;
  --altcha-border-radius: 12px;
  --altcha-color-base: var(--tree-bg);
  --altcha-color-border: var(--tree-hairline);
  --altcha-color-text: var(--tree-ink);
  --altcha-color-border-focus: var(--tree-accent);
  --altcha-color-active: var(--tree-accent);
  --altcha-color-error-text: var(--tree-accent);
  --altcha-color-footer-bg: var(--tree-bg-alt);
  /* ⚠ The tick itself is a plain browser checkbox inside the widget's own
     shadow root — ALTCHA's stylesheet never styles it — so the descendant rule
     below cannot reach it and it stayed the browser's blue. `accent-color` is
     an inherited property, so setting it on the host is what crosses the
     shadow boundary. Reported 2026-09-13: "the checkmark is blue". */
  accent-color: var(--tree-fill);
}
.com-contact__form .altcha { min-height: 52px; }
.com-contact__form .altcha input[type="checkbox"] { accent-color: var(--tree-fill); }
.com-contact__form .control-group:has(altcha-widget) > .control-label,
.com-contact__form .control-group:has(.altcha) > .control-label { display: none; }

/* an in-page jump to the form must clear the sticky header */
#contact-form,
.com-contact__form { scroll-margin-top: 5.5rem; }

/* ------------------------------------------------------------------ chips
   A short list of facts, as pills rather than as ticks. Added 2026-09-12:
   the tick list had spread to five sections on one page, and a marker that
   appears everywhere stops meaning anything. A tick now says exactly one
   thing — "this is in the package" — and everything else that used to be a
   bulleted list is a row of chips.

       <ul class="tree-chips"><li>…</li></ul>                            */
.tree-chips {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem .55rem;
  padding: 0;
  margin: 1.25rem 0 0;
}
.tree-chips > li {
  padding: .45rem .95rem;
  border: 1px solid var(--tree-hairline);
  border-radius: 999px;
  background: var(--tree-bg);
  color: var(--tree-ink-2);
  font-size: .9rem;
  line-height: 1.35;
}
/* the emphatic variant: the company silver, for the chips that carry a claim */
.tree-chips--solid > li {
  background: var(--tree-bg-alt);
  border-color: transparent;
  color: var(--tree-ink);
}

/* --------------------------------------------------------------- timeline
   The company history: a rule with a dot per milestone, the year set beside
   it. `<ol class="tree-timeline">`, one `<li>` per milestone holding a
   `<span class="tree-timeline__year">`, an `<h3>` and a `<p>`.            */
.tree-timeline {
  list-style: none;
  padding: 0;
  margin: 2rem 0 0;
  position: relative;
}
.tree-timeline > li {
  position: relative;
  padding-left: 1.85rem;
  padding-bottom: 2rem;
}
.tree-timeline > li:last-child { padding-bottom: 0; }
/* the rule */
.tree-timeline > li::before {
  content: "";
  position: absolute;
  left: 5px;
  top: .55rem;
  bottom: -.55rem;
  width: 2px;
  background: var(--tree-hairline);
}
.tree-timeline > li:last-child::before { display: none; }
/* the dot */
.tree-timeline > li::after {
  content: "";
  position: absolute;
  left: 0;
  top: .45rem;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--tree-fill);
  box-shadow: 0 0 0 4px var(--tree-bg);
}
.tree-timeline__year {
  display: block;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tree-mark);
  font-variant-numeric: tabular-nums;
  margin-bottom: .15rem;
}
.tree-timeline > li > h3 { margin: 0 0 .3rem; }
.tree-timeline > li > p { margin-bottom: 0; color: var(--tree-ink-2); }

@media (min-width: 48em) {
  /* the year moves into its own column beside the rule */
  .tree-timeline > li { padding-left: 7.5rem; }
  .tree-timeline > li::before { left: 5.75rem; }
  .tree-timeline > li::after { left: 5.25rem; }
  .tree-timeline__year {
    position: absolute;
    left: 0;
    top: .35rem;
    width: 4.5rem;
    text-align: right;
    margin: 0;
    font-size: .9375rem;
    letter-spacing: .02em;
    text-transform: none;
  }
}

/* the consent gate in front of a map whose tiles come from someone else */
.tree-map__gate {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 1rem;
  text-align: center;
  aspect-ratio: 16 / 9;
  padding: clamp(1.25rem, 4vw, 2.5rem);
  border: 1px solid var(--tree-hairline);
  border-radius: 20px;
  background: var(--tree-bg-alt);
  margin-bottom: 1rem;
}
.tree-map__gate p {
  margin: 0;
  max-width: 46ch;
  font-size: .9375rem;
  color: var(--tree-ink-2);
}
@media (width <= 48em) {
  .tree-map__gate { aspect-ratio: 4 / 3; }
}

/* a person's role, under their name on a team card */
.tree-role {
  margin: -.1rem 0 .6rem;
  font-size: .8125rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--tree-mark);
  font-weight: 600;
}
/* a card that holds only a domain suffix needs no body padding of a full card */
.card:has(> .card-body > .tree-tld:only-child) > .card-body { padding: 1.25rem 1.5rem; }
.card > .card-body > .tree-tld:only-child { margin-bottom: 0; }

/* ------------------------------------------------------------- portraits
   A team card's photograph is a face, and the first attempt gave it its own
   geometry: a square, inset from the card edge with its own margin and radius.
   Reported 2026-09-13 — "the tile with myself should be similar to the tile on
   the start page, not cropped within the box" — and he is right: a card has one
   picture treatment, and it is the flush 16:9 tile of `.card-img-top`.

   So the portrait is now simply a card tile. The *photograph* carries the crop
   (a 16:9 rendition made from the original, head and shoulders in frame), and
   the only thing left for CSS is to hold the focal point on the face if the
   tile ever ends up narrower than 16:9.

       <img class="card-img-top tree-portrait" src="…" width="1200" height="675">
*/
.card-img-top.tree-portrait { object-position: center 38%; }

/* ----------------------------------------------------------------- panel
   A section on the alternate surface, full-bleed, with its content on the
   normal measure and left-aligned. It is the quiet way to break a long page
   when there is no photograph spare: same components inside, different
   ground. Added 2026-09-13 for the Microsoft 365 section of the IT page,
   which was the fourth identical-looking block on that page.

       <div class="tree-panel">
         <div class="tree-panel__inner"> <h2>…</h2> <p>…</p> … </div>
       </div>
*/
.tree-panel {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-block: clamp(3rem, 7vw, 5rem);
  padding-block: clamp(2.5rem, 6vw, 4rem);
  background: var(--tree-panel-bg, var(--tree-bg-alt));
}
.tree-panel__inner {
  max-width: var(--tree-container);
  margin-inline: auto;
  padding-inline: var(--tree-gutter);
}
.tree-panel__inner > * + * { margin-top: 1.25rem; }
.tree-panel__inner > :first-child { margin-top: 0; }
.tree-panel + .tree-panel { margin-block-start: 0; }
/* a panel that ends the article should not collide with the closing band */
.tree-panel:has(+ .tree-cta) { margin-block-end: 0; }

/* ⚠ Two full-bleed grounds meeting must **touch**. A panel carries its own
   `margin-block`, a photographic band carries `margin-block` too, and adjacent
   margins collapse to the larger of the two — so a panel followed by a band
   left a 5 rem strip of the page ground between them. On a white page that is
   whitespace and nobody notices; on a black page it is a black bar across the
   width, which is exactly how it was reported on 2026-09-22: *"on the about
   page there is a black bar before the image."*
   Both sides have to be zeroed: collapsing takes the maximum, so zeroing only
   one leaves the other's margin standing. */
.tree-panel:has(+ .tree-hero--band) { margin-block-end: 0; }
.tree-panel + .tree-hero--band     { margin-block-start: 0; }
.tree-hero--band + .tree-panel     { margin-block-start: 0; }
.tree-cta + .tree-panel            { margin-block-start: 0; }
/* Components that use the alternate surface would vanish into the panel, so
   inside one they take the page ground instead — the same inversion the card
   makes on a filled hover. */
.tree-panel .card,
.tree-panel .tree-facts > li { background: var(--tree-bg); }
.tree-panel .card { border-color: var(--tree-hairline); }

/* A panel now carries a whole page section, not just a paragraph block, so it
   has to reproduce that section's vertical rhythm. `* + *` at 1.25rem is right
   between a heading and its paragraph and far too tight around a card row, a
   step list or a second heading. Added 2026-09-22 with the alternating
   sections below. */
.tree-panel__inner > * + .row,
.tree-panel__inner > .row + *,
.tree-panel__inner > * + .tree-steps,
.tree-panel__inner > .tree-steps + *,
.tree-panel__inner > * + .tree-timeline,
.tree-panel__inner > .tree-timeline + * { margin-top: clamp(1.75rem, 3vw, 2.25rem); }
.tree-panel__inner > * + h2 { margin-top: clamp(2.5rem, 5vw, 3.5rem); }

/* ------------------------------------------------- alternating sections
   Requested 2026-09-22: "take a look at the apple page, every block is
   separated. Therefore, if there is on a page section no background but the
   same type as before, change it to the dark grey (in light accordingly)."

   A page is a sequence of sections. Some carry a photograph and separate
   themselves; the rest are heading-plus-content on the page ground, and two of
   those in a row read as one long block. Every second section in such a run is
   therefore wrapped in `.tree-panel`, so the ground alternates and no two
   neighbouring blocks look alike. The wrapping is done in the article, because
   the article is a flat list of siblings with nothing for CSS to paint behind.

   The ground is `--tree-bg-alt` in both themes: silver #eceff3 in light, #131517
   in dark. That is **the footer's colour**, which is the whole argument for it —
   a band and the footer are the same idea, a second surface on the page, and
   having two different second surfaces on one page is one too many.

   ⚠ 3.34.0 shipped the company graphite #3f4445 as the dark-mode ground instead,
   on the reasoning that #131517 against #000000 is barely a separation. Rejected
   on sight the same day: *"please use the grey from the footer for the page
   difference."* The graphite band read as a light slab dropped into a black page.
   The subtler ground is the decision — do not re-raise it.

   Because the ground is back to a surface the dark palette was already picked
   against, the panel needs no token overrides: `--tree-ink-2` on #131517 is
   6.8:1 and the accent tint 6.0:1, both measured. `--tree-panel-bg` remains as a
   hook for a future one-off panel and is unset. */

/* --------------------------------------------------- boxes on a shared grid
   Cards in one row are the same height already (the column is the flex
   parent, §4.5), but their *contents* were not aligned: a two-line heading
   pushed that card's role label and paragraph a line lower than its
   neighbours', so three boxes side by side read as three different rhythms.
   Reported 2026-09-12: "make sure that the headings and text are aligned for
   all boxes that are on the same line".

   The fix is `subgrid`: the Bootstrap row becomes the grid that owns the row
   tracks — badge, heading, role, text — and every card inherits those tracks
   instead of laying out its own. Each card then starts each part at the same
   line as its neighbours, however many lines the heading takes.

   ⚠ Scope: rows whose cards carry an icon badge **and** whose card bodies hold
   at most three children — the four tracks are badge, heading, role, text. A
   card with two paragraphs would put both in the last track, on top of each
   other; that is exactly what happened on the About page's team cards, and the
   `:not(:has(… :nth-child(4)))` guard is what stops it. Three children is the
   limit, not four: the heading occupies *two* tracks (badge + its own text), so
   n children need n+1 tracks and the grid has four.

   ⚠⚠ The guard must appear on **every** selector of this block, the column
   counts in the media queries included. It adds four class-equivalents of
   specificity; a guarded `grid-template-columns: 1fr` would otherwise outrank
   an unguarded `repeat(3, 1fr)` further down and every row would stack in one
   column at every width. That is exactly what 3.18.0 shipped.

   Scope also excludes:
   Photo cards (the home page's service tiles) keep the flex behaviour, because
   their picture is a fixed ratio and their headings are one word.

   `@supports` guards the whole thing: without subgrid (Safari < 16, Chrome
   < 117) the flex layout of §4.5 stays exactly as it was — equal heights,
   contents aligned to the top of each card. */
@supports (grid-template-rows: subgrid) {
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) {
    display: grid;
    grid-template-columns: 1fr;
    align-items: stretch;
    /* the gutters came from the columns' own padding, which this layout
       removes — the grid has to put them back itself */
    gap: 1.25rem 1.5rem;
    margin-inline: 0;
  }
  @media (min-width: 48em) {
    .container-component .row:has(> .col-md-6 > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) { grid-template-columns: repeat(2, 1fr); }
    .container-component .row:has(> .col-md-4 > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) { grid-template-columns: repeat(3, 1fr); }
  }
  @media (min-width: 62em) {
    .container-component .row:has(> [class*="col-lg-4"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) { grid-template-columns: repeat(3, 1fr); }
    .container-component .row:has(> [class*="col-lg-3"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) { grid-template-columns: repeat(4, 1fr); }
  }
  /* four tracks: the badge, the heading text, the role line, the paragraph */
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) > [class*="col-"] {
    display: grid;
    grid-row: span 4;
    grid-template-rows: subgrid;
    width: auto;
    max-width: none;
    flex: none;
    padding: 0;
  }
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) > [class*="col-"] > .card {
    display: grid;
    grid-row: span 4;
    grid-template-rows: subgrid;
    padding: 1.75rem;
  }
  /* the padding moved to the card, so the body can disappear and let its
     children take part in the shared grid */
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) > [class*="col-"] > .card > .card-body {
    display: contents;
  }
  /* the badge is a pseudo-element of the heading, so the heading itself is a
     two-track subgrid: badge, then its own text (an anonymous grid item) */
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) .card .tree-ico {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
    align-content: start;
    gap: 0;
  }
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) .card .tree-ico::before {
    align-self: start;
  }
  /* the paragraph takes the last track and fills it, so a short text does not
     leave a card looking half empty */
  .container-component .row:has(> [class*="col-"] > .card .tree-ico):not(:has(> [class*="col-"] > .card > .card-body > :nth-child(4))) .card .tree-role { align-self: start; }
}

/* -------------------------------------------------- form validation message
   Joomla's validator appends `<span class="form-control-feedback">` **inside
   the field's `<label>`** and adds `.invalid` to the label and the control. Out
   of the box that is a bare line of text in Bootstrap's danger colour; this
   makes it a small red chip with a warning glyph, so it reads like a message
   rather than like part of the label.

   ⚠ The grey bubble that says "Please fill in this field" is the **browser's**
   own required-field UI, not Joomla's and not ours. No site can restyle it;
   only removing `required` (and with it the browser's own check) would stop
   it, which would be a bad trade. */
.form-control-feedback {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  margin-left: .5rem;
  padding: .1rem .6rem .15rem;
  border-radius: 999px;
  background: var(--tree-fill);
  color: #ffffff;
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: .01em;
  vertical-align: middle;
}
.form-control-feedback::before {
  content: "\f06a";                     /* exclamation-circle */
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  -webkit-font-smoothing: antialiased;
  font-size: .8em;
}
label.invalid { color: var(--tree-ink); }
/* the control itself: the company red, and a ring rather than a glow */
.form-control.invalid,
.form-control-danger,
input.invalid,
textarea.invalid,
select.invalid {
  border-color: var(--tree-fill) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--tree-fill) 22%, transparent);
}
.has-danger .form-check-input { accent-color: var(--tree-fill); }

/* ------------------------------------------------------------- closing band
   Every page used to end the same way: a heading, a paragraph, a red button on
   the page ground. Six pages doing that in a row is the definition of
   repetitive, and the foot of a page is where a visitor decides whether to
   write — so the last section is now a photograph with the invitation on it.

       <div class="tree-cta">
         <img class="tree-cta__img" src="…" srcset="…" alt="…">
         <div class="tree-cta__text">
           <h2>…</h2> <p>…</p> <p><a class="btn btn-primary" href="…">…</a></p>
         </div>
       </div>

   It is deliberately **not** the hero: shorter, the text centred rather than
   bottom-left, and it closes a page instead of opening one. Same scrim family,
   measured the same way. Added 2026-09-12. */
.tree-cta {
  position: relative;
  isolation: isolate;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-block: clamp(3rem, 7vw, 5rem) 0;
  min-height: clamp(17rem, 34vh, 24rem);
  display: grid;
  place-items: center;
  overflow: hidden;
}
.tree-cta > .tree-cta__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
  border-radius: 0;
  margin: 0;
}
.tree-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  /* measured, not guessed: at .62/.46/.38 the 17 px line came out at 4.17:1
     over the snow-and-meadow photograph, just under the 4.5 floor. These
     values put every band above it. */
  background:
    linear-gradient(0deg, rgb(0 0 0 / .70) 0%, rgb(0 0 0 / .58) 60%, rgb(0 0 0 / .52) 100%),
    rgb(0 0 0 / .16);
}
/* A band with a blown-out highlight in it — a low sun, snow, a white sky —
   needs more scrim than a dusk photograph, exactly as `.tree-hero--bright`
   does for the heroes. Added 2026-09-17 for the References band: measured
   5.66:1 at 1440 px on the base scrim, which passes, but **1.15:1** at 390 px,
   where `cover` zooms a 2:1 band into a tall box and puts the sun itself
   behind the text. One modifier on the section, not a darker scrim for every
   band. */
.tree-cta--bright::after {
  background:
    linear-gradient(0deg, rgb(0 0 0 / .78) 0%, rgb(0 0 0 / .66) 60%, rgb(0 0 0 / .60) 100%),
    rgb(0 0 0 / .22);
}

/* ---------------------------------------------------- the promise line
   The free first conversation, inside the closing block rather than in a box
   of its own (§4.37 is the record of why a box was the wrong answer). The
   emphasis is typographic only — one step up in size, one step up in weight,
   and space around it — because that is how everything else on this site is
   emphasised: the hero lead, the hosting price, the domain ending. No rule, no
   badge, no panel.

       <p class="tree-cta__free">The first conversation is free…</p>

   The colour is inherited, so the line is white on a photographic band and the
   full ink on a plain one; the second rule is needed only because
   `.tree-cta--plain p` is the more specific selector. */
.tree-cta__free {
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
  font-weight: 600;
  line-height: 1.4;
  color: inherit;
  margin-top: 1.5rem;
}
.tree-cta--plain .tree-cta__free { color: var(--tree-ink); }

.tree-cta__text {
  position: relative;
  width: 100%;
  max-width: 46rem;
  padding: clamp(2.5rem, 6vw, 4rem) var(--tree-gutter);
  text-align: center;
  color: #fff;
}
.tree-cta__text > * + * { margin-top: 1.1rem; }
.tree-cta h2 {
  color: #fff;
  margin-top: 0;
  font-size: clamp(1.7rem, 3.4vw, 2.5rem);
}
.tree-cta p {
  color: rgb(255 255 255 / .9);
  max-width: none;
  margin-inline: auto;
}
/* the same inversion as the hero: red disappears into a scrim, silver does not */
.tree-cta .btn-primary {
  background: var(--tree-silver);
  border-color: var(--tree-silver);
  color: var(--tree-graphite);
}
.tree-cta .btn-primary:hover,
.tree-cta .btn-primary:focus-visible,
.tree-cta .btn-primary:active {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
/* ⚠ the dark-mode primary-hover rules are 0,4,0; repeat the inversion above
   them, exactly as the hero has to (§4.11). */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-cta .btn-primary:hover,
  :root:not([data-theme="light"]) .tree-cta .btn-primary:focus-visible,
  :root:not([data-theme="light"]) .tree-cta .btn-primary:active {
    background: var(--tree-red-dark);
    border-color: var(--tree-red-dark);
    color: #ffffff;
  }
}
:root[data-theme="dark"] .tree-cta .btn-primary:hover,
:root[data-theme="dark"] .tree-cta .btn-primary:focus-visible,
:root[data-theme="dark"] .tree-cta .btn-primary:active {
  background: var(--tree-red-dark);
  border-color: var(--tree-red-dark);
  color: #ffffff;
}
/* Without a photograph: the same band on the alternate surface. Used on the
   pages that have no picture spare, so that no two pages close the same way.
   Swap it for the photographic version by adding an <img> and dropping the
   modifier — nothing else changes. */
/* ⚠ Page ground, not the alternate surface. The plain band is always the last
   thing before the footer, and the footer is on `--tree-bg-alt` too — so the
   two ran together as one block of colour. Reported on the home page,
   2026-09-13: "make the background for the contact tile on the bottom black so
   there is a difference in colour to the footer." A hairline gives it an edge
   against ordinary content above; between two full-bleed bands there is
   already an edge, so it is dropped there. */
.tree-cta--plain {
  background: var(--tree-bg);
  min-height: 0;
  border-block-start: 1px solid var(--tree-hairline);
}
.tree-hero + .tree-cta--plain,
.tree-cta + .tree-cta--plain,
.tree-panel + .tree-cta--plain { border-block-start: 0; }
.tree-cta--plain::after { display: none; }
.tree-cta--plain .tree-cta__text { color: var(--tree-ink); }
.tree-cta--plain h2 { color: var(--tree-ink); }
.tree-cta--plain p { color: var(--tree-ink-2); }
.tree-cta--plain .btn-primary {
  background: var(--tree-fill);
  border-color: var(--tree-fill);
  color: #ffffff;
}
.tree-cta--plain .btn-primary:hover,
.tree-cta--plain .btn-primary:focus-visible,
.tree-cta--plain .btn-primary:active {
  background: var(--tree-graphite);
  border-color: var(--tree-graphite);
  color: #ffffff;
}

/* ⚠ A closing band is full-bleed, so everything around it has to stop drawing
   page chrome, or the band arrives with a black strip above it and a stray
   hairline below it — reported on the home page, 2026-09-13, where the band
   follows the full-bleed region band:
     · 80 px of top margin between two full-bleed elements = a bar of page
       ground the width of the window;
     · `.com-content-article` carries a 1 px bottom border (it is the same rule
       that separates blog items), which lands under the band at container
       width and reads as a second line;
     · the article's own 2.5 rem bottom padding puts that line even further
       from anything it could be separating. */
.tree-hero + .tree-cta,
.tree-cta + .tree-cta,
.tree-hero + .tree-panel,
.tree-panel + .tree-cta,
.tree-cta + .tree-panel { margin-block-start: 0; }
/* …and the element above it has to drop its bottom margin too, or the gap
   simply moves from one side of the boundary to the other (measured: 72 px of
   page ground between the region band and the closing band). */
.tree-hero:has(+ .tree-cta),
.tree-split:has(+ .tree-cta),
.tree-hero:has(+ .tree-panel),
.tree-panel:has(+ .tree-cta),
.tree-cta:has(+ .tree-panel) { margin-block-end: 0; }

.container-component:has(.tree-cta:last-child) { padding-block-end: 0; }
.com-content-article:has(.tree-cta:last-child),
.com-content-article:has(> .com-content-article__body > .tree-cta:last-child) {
  padding-bottom: 0;
  border-bottom: 0;
}
.com-content-article__body:has(> .tree-cta:last-child) > .tree-cta:last-child { margin-bottom: 0; }
/* core's Prev/Next article navigation would sit *below* the band, on the page
   ground, under a heading that has just said "write to us". The proper switch
   is the global article option (Show Navigation = Hide, §12); this makes a
   forgotten setting harmless on the pages that end with a band. */
.com-content-article:has(> .com-content-article__body > .tree-cta:last-child) > .pagenavigation { display: none; }
/* photographs are damped in dark mode everywhere else; the scrim already does
   that job here, and damping twice makes the band muddy */
:root[data-theme="dark"] .tree-cta__img,
:root:not([data-theme="light"]) .tree-cta__img { filter: none; }

/* ------------------------------------------------------------- split block
   Text on one side, a photograph on the other — the pattern Apple uses to
   break a long page without shouting. Added 2026-09-12, replacing the small
   icon badge that sat above a section heading: at 44 px above a 40 px
   headline the badge was neither an illustration nor an ornament.

       <div class="tree-split">
         <div class="tree-split__text"> <h2>…</h2> <p>…</p> </div>
         <img class="tree-split__img" src="…" alt="…">
       </div>

   `tree-split--reverse` puts the picture on the left. Below 62 em the text
   comes first and the picture follows, always. */
.tree-split {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: center;
  margin-block: clamp(2.5rem, 6vw, 4.5rem);
}
@media (min-width: 62em) {
  .tree-split { grid-template-columns: 1fr 1fr; }
  .tree-split--reverse .tree-split__text { order: 2; }
}
.tree-split__text > h2 { margin-top: 0; }
.tree-split__text > * + * { margin-top: 1.25rem; }
.tree-split__img {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 20px;
  display: block;
}
@media (min-width: 62em) {
  .tree-split__img { aspect-ratio: 5 / 4; }
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .tree-split__img { filter: brightness(.88) saturate(.92); }
}
:root[data-theme="dark"] .tree-split__img { filter: brightness(.88) saturate(.92); }

/* ------------------------------------------------------------------ facts
   Small boxes, each an icon and a line of text. Replaces the pill/tag row
   (2026-09-12): pills read as metadata, which is not what these are — they
   are the things you get. Each item may carry its own `tree-i-NAME`; without
   one it takes the tick.

       <ul class="tree-facts">
         <li class="tree-i-shield">Firewalls and secure remote access</li>
       </ul>                                                              */
.tree-facts {
  list-style: none;
  padding: 0;
  margin: 1.5rem 0 0;
  display: grid;
  gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
.tree-facts > li {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .8rem 1rem;
  border-radius: 14px;
  background: var(--tree-bg-alt);
  color: var(--tree-ink);
  font-size: var(--fs-small, .9375rem);
  line-height: 1.4;
}
.tree-facts > li::before {
  content: var(--tree-ico, "\f00c");
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  -webkit-font-smoothing: antialiased;
  display: grid;
  place-items: center;
  flex: none;
  width: 2rem;
  height: 2rem;
  border-radius: 10px;
  background: var(--tree-ico-bg);
  color: var(--tree-ico-fg);
  font-size: .8125rem;
  line-height: 1;
}

/* ------------------------------------------------- the call to action in the nav
   The last item of the main menu becomes a button. In Joomla this needs one
   setting on the menu item — *Link Type → Link CSS Style* = `tree-nav-cta` —
   and nothing else: the item stays an ordinary menu item, so the language
   filter, the associations and the active state all keep working.

   In the mobile panel it becomes a full-width button at the end of the list,
   which is where a thumb expects it. */
.mod-menu a.tree-nav-cta,
.mod-menu .tree-nav-cta > a {
  background: var(--tree-fill);
  color: #ffffff;
  border-radius: 999px;
  padding-inline: 1.1rem;
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  font-weight: 500;
  transition: background-color .18s ease, color .18s ease;
}
.mod-menu a.tree-nav-cta:hover,
.mod-menu a.tree-nav-cta:focus-visible,
.mod-menu .tree-nav-cta > a:hover,
.mod-menu .tree-nav-cta > a:focus-visible {
  background: var(--tree-graphite);
  color: #ffffff;
}
/* ⚠ Cassiopeia underlines header links on hover and on the active item with
   `.container-header .mod-menu.mod-list > li > a:hover` (0,3,2), which outranks
   a rule written as `.mod-menu a.tree-nav-cta:hover` (0,3,1). On a filled pill
   that underline reads as a rendering fault, so it is switched off at a weight
   that actually wins. Reported: "with hover, there should not be a line below." */
.container-header .mod-menu > li > a.tree-nav-cta,
.container-header .mod-menu > li > a.tree-nav-cta:hover,
.container-header .mod-menu > li > a.tree-nav-cta:focus,
.container-header .mod-menu > li.active > a.tree-nav-cta,
.container-header .mod-menu.mod-list > li > a.tree-nav-cta:hover,
.tree-navpanel .mod-menu a.tree-nav-cta:hover,
.mod-menu a.tree-nav-cta:hover { text-decoration: none; }
/* ⚠ The bar under the item is **not** a text-decoration and not on the link:
   Cassiopeia draws it as a pseudo-element on the *list item* —
   `.container-header .mod-menu > li.active:after, … > li:hover:after` — which
   is why suppressing `a::after` changed nothing. Reported twice; measured in
   the rendered page the second time. */
/* On the page it points at, the button inverts to the second company colour:
   silver with graphite text. It says "you are here" without needing the bar
   that was just removed. Requested 2026-09-13.
   ⚠ The colour has to be re-stated here because `.container-header .mod-menu >
   li > a` (0,3,2) paints every header link with `--tree-ink` and would
   otherwise put near-black text on the red pill — which is exactly what the
   current item looked like before this rule existed. */
.container-header .mod-menu > li:is(.current, .active) > a.tree-nav-cta,
.tree-navpanel .mod-menu > li:is(.current, .active) > a.tree-nav-cta {
  background: var(--tree-silver);
  color: var(--tree-graphite);
}
.container-header .mod-menu > li:is(.current, .active) > a.tree-nav-cta:hover,
.container-header .mod-menu > li:is(.current, .active) > a.tree-nav-cta:focus-visible {
  background: var(--tree-graphite);
  color: #ffffff;
}
/* and the non-current button keeps white text against the same header rule */
.container-header .mod-menu > li > a.tree-nav-cta { color: #ffffff; }
.container-header .mod-menu > li > a.tree-nav-cta:hover,
.container-header .mod-menu > li > a.tree-nav-cta:focus-visible { color: #ffffff; }

.container-header .mod-menu > li:has(> a.tree-nav-cta)::after,
.container-header .mod-menu > li:has(> a.tree-nav-cta):hover::after,
.container-header .mod-menu > li.active:has(> a.tree-nav-cta)::after,
.mod-menu .current > a.tree-nav-cta::after,
.mod-menu .current.tree-nav-cta > a::after { display: none; content: none; }
@media (width < 62em) {
  .tree-navpanel .mod-menu a.tree-nav-cta,
  .tree-navpanel .mod-menu .tree-nav-cta > a,
  .tree-navpanel a.tree-nav-cta {
    justify-content: center;
    width: 100%;
    min-height: 48px;
    margin-top: .5rem;
  }
}

/* ---------------------------------------------------------------------------
   The error page (error.php)

   A 404 is a dead end, so the page is built to be short and to offer a way on,
   not to fill the screen. No photograph: every picture on this site is used
   exactly once and earns its place, and a landscape band behind an apology
   reads as decoration. What carries it is the type and the space around it.

   The status code sits above the headline as a small caption rather than as a
   giant number — the visitor does not care what 404 means, and the people who
   do care want it findable, not decorative.
   --------------------------------------------------------------------------- */
.tree-error {
  max-width: 34rem;
  margin-block: clamp(3rem, 9vw, 7rem) clamp(3.5rem, 10vw, 8rem);
}

.tree-error__code {
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--tree-ink-2);
  margin: 0 0 1rem;
}

.tree-error h1 { margin: 0; }

.tree-error .lead { margin-top: 1.25rem; }

.tree-error__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  margin-top: 2.25rem;
}

/* Only ever rendered with Site Debug on, so it is styled to look like what it
   is — a developer's note that happens to be on a public-facing page. */
.tree-error__detail {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--tree-hairline);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .875rem;
  color: var(--tree-ink-2);
}

/* ------------------------------------------------------------------ prose
   `.tree-prose` marks a section that is mostly running text — the About page.
   It is a **hook with no measure on it**, and that is deliberate.

   3.33.0 gave it `max-width: 68ch`, on the typographic argument that the
   1120 px container runs to about 110 characters a line where comfortable
   reading is 60 to 80. Reverted in 3.35.0, reported from the built page:
   *"on the about us page, text is not using full width."* That is the same
   judgement already recorded on 2026-09-12, when the 62-character cap was
   removed from every paragraph on the site — a capped column inside a
   full-width section leaves a third of the section empty, and that reads as
   a mistake rather than as a measure.

   ⚠ Do not re-introduce a measure here. The place a measure is wanted is
   `.tree-legal`, which is a document rather than a page and keeps its 74 ch.

   The class stays because the section rhythm uses it (a `.tree-prose` block
   is one of the sections the alternating grounds count), and because it is
   where a future prose-only rule would go.
   --------------------------------------------------------------------- */
