/* =========================================================================
   Mindful Bookkeeping, LLC
   One stylesheet. Still water: warm off-white ground, deep teal ink, the
   mustard reserved for a single device, the highlighter swipe.

   Order of the file:
     1. Tokens
     2. Reset
     3. Type
     4. Chrome: skip link, header, nav, footer
     5. Layout: container, sections, hero
     6. Components
     7. Forms
     8. Dialog
     9. Motion
    10. Utilities

   Contrast, computed against WCAG 2.1 relative luminance on 2026-09-16.
   The math was checked against the two known pairs first: black on white
   returns 21.00:1, and the mid grey that WCAG documents as the 4.5:1
   boundary on white returns 4.54:1.

     ink on mist       13.81:1   expected above 13      pass
     lagoon on mist     5.15:1   expected 4.5 or above  pass
     ochre on mist      5.56:1   expected above 6       under the expectation
     paper on teal      7.07:1   expected above 7       pass
     ink on gold        7.92:1   expected above 8       under the expectation
     pool on teal       3.25:1   decoration only, no requirement

   Both shortfalls clear WCAG AA for body text at 4.5:1 and AAA is not
   claimed anywhere, so the palette is left exactly as the spec sets it.
   They are recorded here so nobody re-derives them later and thinks the
   tokens drifted. Further pairs, for anyone placing text:

     ink on water      12.91:1
     ink on paper      14.81:1
     lagoon on water    4.81:1
     paper on lagoon    5.52:1
     gold on teal       3.78:1   decoration only, never text
   ========================================================================= */

/* =========================================================================
   1. Tokens
   The one exception to the rule below is the dialog backdrop scrim, which
   the plan fixes as a literal.
   ========================================================================= */
/* Mindful Bookkeeping, LLC. Tokens first; no raw color anywhere else. */
:root {
  --mist: #F6F4EF;
  --paper: #FDFCFA;
  --water: #E7EEEE;
  --ink: #14292C;
  --teal: #1F5F68;
  --lagoon: #2E7079;
  --pool: #7FB7BC;
  --gold: #E4B531;
  --ochre: #7A5E12;
  --line: rgba(20, 41, 44, 0.14);
  --line-on-teal: rgba(127, 183, 188, 0.35);
  --shadow: 0 6px 18px rgba(20, 41, 44, 0.12);
  --radius: 6px;
  --font-display: "Seraphina", "Iowan Old Style", Georgia, serif;
  --font-body: "Proda Sans", -apple-system, "Segoe UI", sans-serif;
  --font-hand: "Monthly Stories", "Segoe Print", cursive;
  --max: 1120px;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --section-y: clamp(4rem, 9vw, 7.5rem);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

/* =========================================================================
   2. Reset
   ========================================================================= */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body {
  margin: 0;
  background: var(--mist);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1.1875rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
img, svg, video { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }

/* Several components below are grids and flex rows, so the attribute has
   to win over their display value. Used by the nav panel, the empty error
   line and every [data-success] note before its form is answered. */
[hidden] { display: none !important; }

/* =========================================================================
   3. Type
   Seraphina has no optical size axis, so there is no font-variation-settings
   anywhere in this file. Weight carries the distinction the axis used to:
   500 for h1, h2 and h3, 400 for figures and running heads.

   The sizes here are tuned to the licensed faces, not to the Google ones
   they replaced. Seraphina sets on a taller x-height than Newsreader did,
   so display came down; Proda Sans and Monthly Stories sit lower than
   Albert Sans and Kalam, so body and the hand went up. docs/FONTS.md
   carries the measurements and the arithmetic.
   ========================================================================= */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  margin: 0 0 0.55em;
  color: var(--ink);
}
h1 {
  font-size: clamp(2.4rem, 5.6vw, 4rem);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.01em;
}
h2 {
  font-size: clamp(1.75rem, 3.3vw, 2.6rem);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.01em;
}
h3 {
  font-size: 1.4rem;
  font-weight: 500;
  line-height: 1.3;
}
h4 {
  font-size: 1.1875rem;
  font-weight: 500;
  line-height: 1.35;
}

p, ul, ol { margin: 0 0 1.1em; }
p:last-child, ul:last-child, ol:last-child { margin-bottom: 0; }
strong, b { font-weight: 600; }
em, i { font-style: italic; }
small, .small { font-size: 1.03125rem; }

a, .link {
  color: var(--teal);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}
a:hover, .link:hover {
  color: var(--lagoon);
  text-decoration-thickness: 2px;
}

/* A link that has to be read across the room: the phone number and the
   email address under a page heading. Display face, so it sits with the
   headings rather than with the body copy. */
.link--large {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(1.4rem, 2.6vw, 1.9rem);
  color: var(--teal);
  text-decoration-thickness: 1px;
}

/* The numbered legal pages. Their headings are section labels rather than
   page headings, so they sit well under the h2 scale, and the running text
   keeps the same measure the compliance line uses. */
.legal h2 {
  font-size: 1.35rem;
  font-weight: 500;
  margin-top: 2.4rem;
}
.legal p, .legal li { max-width: 68ch; }

:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 3px;
}

/* =========================================================================
   4. Chrome
   ========================================================================= */

/* 4a. Skip link. Parked off the top of the page until it takes focus. The
   park is in rem because the link's own height is in rem: a fixed -60px
   leaves most of the link showing over the logo once a visitor sets text to
   200 per cent. */
.skip-link {
  position: absolute;
  top: -6rem;
  left: var(--gutter);
  z-index: 200;
  padding: 0.65rem 1.1rem;
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--paper);
  font-weight: 500;
  text-decoration: none;
  transition: top 180ms var(--ease);
}
.skip-link:focus { top: 12px; color: var(--paper); }

/* 4b. Header. Transparent over the mist ground at rest, 88px tall. Once
   main.js sets .is-condensed at 80px of scroll it drops to 64px, takes the
   ground and gains a hairline. The hysteresis lives in the script. Both
   .header__inner and a plain .container are accepted as the row so the
   shell can use either. */
.header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color 240ms var(--ease), border-color 240ms var(--ease);
}
.header__inner,
.header > .container {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2rem);
  height: 88px;
  transition: height 240ms var(--ease);
}
.header.is-condensed {
  background: var(--mist);
  border-bottom-color: var(--line);
}
.header.is-condensed .header__inner,
.header.is-condensed > .container { height: 64px; }

.header__mark {
  display: inline-flex;
  align-items: center;
  margin-right: auto;
  text-decoration: none;
}
.header__mark img,
.header__mark svg {
  height: 52px;
  width: auto;
  transition: height 240ms var(--ease);
}
.header.is-condensed .header__mark img,
.header.is-condensed .header__mark svg { height: 36px; }

.header__cta { flex: none; }

/* 4c. Nav. Proda Sans 500, a hairline that arrives on hover, and the
   current page underlined in the mustard. */
.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.9rem, 2vw, 1.75rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav a {
  display: inline-block;
  padding: 0.35rem 0;
  border-bottom: 2px solid transparent;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 1.03125rem;
  text-decoration: none;
  transition: color 160ms var(--ease), border-color 160ms var(--ease);
}
.nav a:hover {
  color: var(--lagoon);
  border-bottom-color: var(--lagoon);
}
.nav a[aria-current="page"] { border-bottom-color: var(--gold); }
.nav a[aria-current="page"]:hover { border-bottom-color: var(--lagoon); }

.nav__toggle {
  display: none;
  align-items: center;
  gap: 0.55rem;
  padding: 0.55rem 0.85rem;
  min-height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  font-weight: 500;
  font-size: 1.03125rem;
  cursor: pointer;
}
.nav__toggle i {
  display: block;
  width: 18px;
  height: 1.5px;
  background: currentColor;
}
.nav__toggle span:not(.visually-hidden) {
  display: inline-flex;
  flex-direction: column;
  gap: 4px;
}

/* Under 860px the nav becomes a full height paper panel. The toggle sits
   above it so the same button closes it. main.js owns .is-open and
   aria-expanded; every destination in here is also in the footer, so the
   site is still navigable if the script never runs. */
@media (max-width: 859px) {
  .nav__toggle {
    display: inline-flex;
    position: relative;
    z-index: 80;
    margin-left: auto;
  }
  /* The .btn rule sets display at the same specificity later in the file,
     so the header CTA has to be hidden by a longer selector or it stays in
     the bar and crowds the toggle. */
  .header .header__cta { display: none; }
  .nav {
    position: fixed;
    inset: 0;
    z-index: 70;
    padding: 6.5rem var(--gutter) var(--gutter);
    background: var(--paper);
    overflow-y: auto;
    visibility: hidden;
    opacity: 0;
    transform: translateY(-1rem);
    transition: opacity 220ms var(--ease), transform 240ms var(--ease), visibility 0s linear 240ms;
  }
  .nav.is-open {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition: opacity 220ms var(--ease), transform 240ms var(--ease), visibility 0s;
  }
  .nav__list {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25rem;
  }
  .nav a {
    font-family: var(--font-display);
    font-size: 1.5rem;
    font-weight: 500;
    padding: 0.6rem 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .header, .header__inner, .header > .container,
  .header__mark img, .header__mark svg, .nav { transition: none; }
}

/* 4d. Footer. Carries the teal ground and every teal recolor on its own;
   the markup does not need .section--teal for it to read correctly. */
.footer {
  padding-block: clamp(3rem, 6vw, 4.5rem);
}
.footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
}
@media (max-width: 720px) {
  .footer__grid { grid-template-columns: 1fr; }
}
.footer__mark img,
.footer__mark svg {
  height: 46px;
  width: auto;
  margin-bottom: 1rem;
}
.footer__tagline {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.25rem;
}
.footer__social {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  margin-top: 1.25rem;
}
.footer__social a {
  display: inline-flex;
  color: var(--paper);
  text-decoration: none;
}
.footer__social a:hover { color: var(--pool); }
.footer__social svg {
  width: 22px;
  height: 22px;
  fill: currentColor;
}
/* The same two glyphs away from the teal foot, on a light ground: teal at
   rest, lagoon on hover, which is the language every other link uses. The
   fill and the missing underline are not decoration, they are what makes
   the color rule visible: the marks are filled paths inside a link, so
   without currentColor they paint black and the color above does nothing,
   and without the reset an underline runs beneath each glyph. */
.social { display: flex; gap: 1rem; }
.social a {
  display: inline-flex;
  color: var(--teal);
  text-decoration: none;
}
.social a:hover { color: var(--lagoon); }
.social svg {
  width: 22px;
  height: 22px;
  fill: currentColor;
}

.footer__legal {
  margin-top: clamp(2rem, 5vw, 3.25rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--line-on-teal);
  font-size: 1.03125rem;
}
.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  margin: 0 0 1rem;
  padding: 0;
  list-style: none;
}
.footer__compliance { max-width: 68ch; }

/* =========================================================================
   5. Layout
   ========================================================================= */
.container {
  width: min(var(--max), calc(100% - 2 * var(--gutter)));
  margin-inline: auto;
}

/* Sections carry the grounds. Mist is the body, so a plain .section sets no
   background of its own and the breathing ground shows through it.
   isolation makes each section a stacking context, which is what lets the
   offset rule behind a .frame paint above a tinted ground instead of
   sliding under it. */
.section {
  position: relative;
  isolation: isolate;
  padding-block: var(--section-y);
}
.section--water { background: var(--water); }
.section--teal, .footer { background: var(--teal); color: var(--paper); }

.section--teal h1,
.section--teal h2,
.section--teal h3,
.section--teal h4,
.footer h1,
.footer h2,
.footer h3,
.footer h4 { color: var(--paper); }
.section--teal a:not(.btn),
.section--teal .link,
.footer a:not(.btn),
.footer .link { color: var(--paper); }
.section--teal a:not(.btn):hover,
.section--teal .link:hover,
.footer a:not(.btn):hover,
.footer .link:hover { color: var(--pool); }
.section--teal :focus-visible,
.footer :focus-visible { outline-color: var(--gold); }
.section--teal .ruled,
.section--teal .ruled__row,
.section--teal .picture,
.section--teal .picture__row { border-color: var(--line-on-teal); }
.section--teal .picture__row:last-of-type { border-bottom-color: var(--gold); }
/* The hand is lagoon on light grounds, which disappears on teal. Paper at
   7.07:1 keeps the note readable and keeps pool off text, as the spec has
   it. */
.section--teal .hand, .footer .hand { color: var(--paper); }
.section--teal .btn:not(.btn--gold) {
  border-color: var(--paper);
  color: var(--paper);
  background: transparent;
}
.section--teal .btn:not(.btn--gold):hover {
  background: var(--paper);
  color: var(--teal);
}

.hero {
  position: relative;
  isolation: isolate;
  padding-block: clamp(3rem, 7vw, 5.5rem) var(--section-y);
}
.hero__lede {
  max-width: 46ch;
  margin-top: 1.5rem;
  color: var(--lagoon);
  /* The floor was 1.125rem against a 17px body. With body at 19px the lede
     rendered smaller than the copy beneath it at 390. The floor goes to
     1.25rem so the lede is never under the body size. */
  font-size: clamp(1.25rem, 1.6vw, 1.375rem);
  line-height: 1.55;
}
.section--teal .hero__lede { color: var(--paper); }

/* =========================================================================
   6. Components
   ========================================================================= */
.btn { display: inline-block; padding: 0.85rem 1.4rem; border-radius: var(--radius); border: 1px solid var(--ink); background: transparent; color: var(--ink); font-weight: 500; text-decoration: none; transition: background 160ms, color 160ms, transform 160ms; }
.btn:hover { background: var(--ink); color: var(--paper); }
.btn--solid { background: var(--ink); color: var(--paper); }
.btn--solid:hover { background: var(--teal); border-color: var(--teal); }
.btn--gold { background: var(--gold); color: var(--ink); border-color: var(--gold); }
.btn--gold:hover { background: var(--paper); border-color: var(--paper); }

/* The rule above sets the look. These are the bits a button needs to feel
   like one: a pointer, a centred label, and the press the transition above
   already promises. */
.btn {
  cursor: pointer;
  text-align: center;
  line-height: 1.25;
}
.btn:active { transform: translateY(1px); }
.btn[disabled] { opacity: 0.55; cursor: default; }
.btn[disabled]:active { transform: none; }

.sticker { display: inline-block; padding: 0.4rem 0.9rem; border-radius: 999px; background: var(--gold); color: var(--ink); font-family: var(--font-hand); font-size: 1.9rem; line-height: 1.2; transform: rotate(-3deg); box-shadow: var(--shadow); }
.ruled { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.ruled__row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 1rem 2rem; padding: 1.6rem 0; border-bottom: 1px solid var(--line); }
@media (max-width: 720px) { .ruled__row { grid-template-columns: 1fr; } }
.frame { margin: 0; position: relative; padding: 14px; background: var(--paper); box-shadow: var(--shadow); width: fit-content; }
.frame::after { content: ""; position: absolute; inset: 0; transform: translate(12px, 12px); border: 1px solid var(--pool); z-index: -1; }
.frame__img { display: block; }

/* The photograph drifts and scales inside the mat under has-fx, so it has
   to be clipped by something that is not .frame itself: clipping .frame
   would take the offset rule with it. A <picture> wrapper does the job and
   the image slots ship WebP with a JPEG fallback anyway. */
.frame > picture,
.frame__media {
  display: block;
  overflow: hidden;
}

/* =========================================================================
   7. Forms
   ========================================================================= */
.field { display: grid; gap: 0.35rem; margin-bottom: 1.25rem; }
.field__input { padding: 0.8rem 0.9rem; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); }
.field__input:focus { border-color: var(--teal); }
.field__error { color: var(--ochre); font-size: 1.03125rem; }
.field.is-invalid .field__input { border-color: var(--ochre); }

.field__label {
  font-size: 1.03125rem;
  font-weight: 500;
}
.field__hint {
  font-size: 1.03125rem;
  color: var(--lagoon);
}
.field__error:empty { display: none; }
textarea.field__input {
  min-height: 8rem;
  resize: vertical;
}
/* The honeypot the contact endpoint reads. Off screen, never focusable. */
.field--trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.section--teal .field__input,
.footer .field__input {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}
.section--teal .field__input:focus,
.footer .field__input:focus { border-color: var(--gold); }
.section--teal .field__hint,
.footer .field__hint { color: var(--paper); }
/* Gold on teal is 3.78:1 and is never used as text, so the error line takes
   paper for the word itself and keeps the mustard as a decorative marker
   instead. */
.section--teal .field__error,
.footer .field__error {
  color: var(--paper);
  border-left: 2px solid var(--gold);
  padding-left: 0.6rem;
}
.section--teal .field.is-invalid .field__input,
.footer .field.is-invalid .field__input { border-color: var(--gold); }

/* =========================================================================
   8. Dialog
   ========================================================================= */
.dialog { border: 0; border-radius: var(--radius); background: var(--paper); box-shadow: var(--shadow); padding: 2rem; max-width: 34rem; width: calc(100% - 2rem); }
.dialog::backdrop { background: rgba(20, 41, 44, 0.45); }

/* The dialog stands over a page that already has an h1 and h2s of its own,
   so its heading takes the panel scale rather than the page h2 scale. The
   chat panel title is the same size for the same reason. */
.dialog__title {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.25rem;
  margin: 0 0 0.5rem;
}

/* The z-index is load-bearing for the wide variant. What the lift brings in
   is a page's own markup, and every .section in it is positioned, so at the
   same stacking level the later element wins and the close action ends up
   underneath the first section. One step up puts it back on top. */
.dialog__close {
  position: absolute;
  z-index: 1;
  top: 0.75rem;
  right: 0.75rem;
  padding: 0.35rem 0.6rem;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--lagoon);
  cursor: pointer;
}
.dialog__close:hover { color: var(--ink); }

/* The wide variant: the lift.

   main.js builds one dialog#lift at boot and fetches /book or /contact into
   it, so what is inside is a page's own markup and it arrives with the
   page's own sections and grounds. The panel is paper, one surface, so the
   water band comes off inside it and the section rhythm is cut right down:
   a page's vertical air is there to be scrolled past, and a panel is read
   in one go. The scroll lives on .dialog__body rather than on the dialog,
   which keeps the close action parked where it was put. */
.dialog--wide { max-width: 56rem; width: calc(100% - 2rem); padding: 0; }
.dialog__body {
  padding: 1.5rem clamp(1rem, 4vw, 2.5rem) 2rem;
  max-height: calc(100vh - 4rem);
  overflow: auto;
}
.dialog--wide .section,
.dialog--wide .hero { padding-block: 1.25rem; min-height: 0; }
/* The panel scrolls under a close action that does not move with it, so
   that action needs the ground under it rather than a line of somebody
   else's copy. */
.dialog--wide .dialog__close { background: var(--paper); padding: 0.5rem 0.75rem; }
.dialog--wide .section--water { background: transparent; }
/* The container has already had its gutter from .dialog__body. */
.dialog--wide .container { width: 100%; max-width: none; }
/* A panel sits under the page's headings rather than competing with them,
   so the lifted h1 takes the page h2 size, the same step .dialog__title
   makes for the narrow panel.

   The close action needs the top right corner to itself, so the first line
   in the panel keeps clear of it. */
.dialog--wide h1,
.dialog--wide .dialog__title { font-size: clamp(1.75rem, 3.3vw, 2.6rem); line-height: 1.15; padding-right: 4.5rem; }
/* The hero lede inside the panel is body copy, not a headline. */
.dialog--wide .hero__lede { font-size: 1.1875rem; margin-top: 1rem; }
/* The panel is narrower than the page it came from, and the email address
   is the longest unbreakable string on this site. It steps down with
   everything else, and is told to break rather than run off the edge if a
   narrower panel ever appears. */
.dialog--wide .link--large { font-size: clamp(1.1875rem, 3.6vw, 1.9rem); overflow-wrap: anywhere; }
/* The offer under a confirmed booking is a heading inside the panel, not
   the panel's own, so it keeps the 1.25rem step and the clear corner does
   not apply to it. */
.dialog--wide .measure .dialog__title { font-size: 1.25rem; padding-right: 0; }

/* =========================================================================
   9. Motion
   Every rule here sits under html.has-fx, which main.js sets before first
   paint only when prefers-reduced-motion is off. Without the class the
   finished state is what the browser renders, so nothing depends on
   JavaScript to become readable.
   ========================================================================= */

/* Hand notes write themselves */
.hand { font-family: var(--font-hand); color: var(--lagoon); font-size: clamp(2.2rem, 3.4vw, 2.6rem); line-height: 1.2; }
html.has-fx .hand { clip-path: inset(0 100% 0 0); transition: clip-path 900ms var(--ease); }
html.has-fx .hand.is-written { clip-path: inset(0 0 0 0); }
/* A word-by-word note reveals its words, so it must not also wipe */
html.has-fx .hand:has(.hw-word) { clip-path: none; transition: none; }
html.has-fx .hw-word { opacity: 0; transition: opacity 340ms; }
html.has-fx .hw-word.is-written { opacity: 1; }

/* The highlighter */
.hl { background-image: linear-gradient(transparent 55%, var(--gold) 55%, var(--gold) 92%, transparent 92%); background-repeat: no-repeat; background-size: 100% 100%; padding: 0 0.12em; margin: 0 -0.12em; }
html.has-fx .hl { background-size: 0% 100%; transition: background-size 420ms var(--ease); }
html.has-fx .hl.is-lit { background-size: 104% 100%; }

/* On the teal ground a swipe under the words leaves most of each letter on
   teal, and ink on teal is 1.8:1. So the mustard fills the whole phrase
   there instead, which reads as a marker block and puts the ink on gold at
   7.9:1. The color travels with the swipe or the phrase is unreadable while
   it draws. */
.section--teal .hl {
  background-image: linear-gradient(var(--gold), var(--gold));
  color: var(--ink);
  padding: 0.08em 0.24em;
  margin: 0 -0.24em;
}
html.has-fx .section--teal .hl {
  color: var(--paper);
  transition: background-size 420ms var(--ease), color 220ms var(--ease) 180ms;
}
html.has-fx .section--teal .hl.is-lit { color: var(--ink); }

/* Into focus */
html.has-fx .focus-in { filter: blur(6px); opacity: 0.6; transition: filter 900ms var(--ease), opacity 900ms var(--ease); }
html.has-fx .focus-in.is-focused { filter: blur(0); opacity: 1; }

/* Scroll reveal, parent stagger */
html.has-fx .reveal { opacity: 0; transform: translateY(14px); transition: opacity 640ms var(--ease), transform 640ms var(--ease); }
html.has-fx .reveal.is-in { opacity: 1; transform: none; }
html.has-fx [data-stagger] > .reveal:nth-child(2) { transition-delay: 90ms; }
html.has-fx [data-stagger] > .reveal:nth-child(3) { transition-delay: 180ms; }
html.has-fx [data-stagger] > .reveal:nth-child(4) { transition-delay: 270ms; }

/* Matted photographs drift and develop */
html.has-fx .frame__img { transform: scale(1.02); filter: blur(4px); transition: filter 1200ms var(--ease); }
html.has-fx .frame.is-developed .frame__img { filter: blur(0); animation: drift 26s ease-in-out infinite alternate; }
@keyframes drift { from { transform: scale(1.02) translate(0, 0); } to { transform: scale(1.06) translate(-1.5%, -1%); } }

/* The breathing ground */
.breath { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; }
.breath span { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.55; }
.breath span:nth-child(1) { width: 46vw; height: 46vw; left: -12vw; top: -8vw; background: var(--water); }
.breath span:nth-child(2) { width: 38vw; height: 38vw; right: -10vw; top: 30vh; background: var(--pool); opacity: 0.12; }
.breath span:nth-child(3) { width: 42vw; height: 42vw; left: 30vw; bottom: -14vw; background: var(--water); }
html.has-fx .breath span { animation: breathe 8s ease-in-out infinite; }
html.has-fx .breath span:nth-child(2) { animation-delay: -2.7s; }
html.has-fx .breath span:nth-child(3) { animation-delay: -5.3s; }
@keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.09); } }

/* The monthly picture */
.picture { max-width: 30rem; border-top: 1px solid var(--line); }
.picture__row { display: flex; justify-content: space-between; align-items: baseline; padding: 0.9rem 0; border-bottom: 1px solid var(--line); }
.picture__row:last-of-type { border-bottom-width: 2px; border-bottom-color: var(--teal); }
.picture__figure { font-family: var(--font-display); font-weight: 400; font-size: 1.5rem; font-variant-numeric: tabular-nums; }

/* The rotating word. The reserved width stops the headline reflowing on
   every swap; main.js drives the fade and owns the word list. */
[data-rotate] {
  display: inline-block;
}
html.has-fx [data-rotate] { transition: opacity 220ms var(--ease); }
html.has-fx [data-rotate].is-fading { opacity: 0; }

/* Counted figures hold their width while they run. */
[data-count] { font-variant-numeric: tabular-nums; }

/* The lift arrives. One short rise, under has-fx like everything else, so
   with reduced motion on the panel is simply there. */
html.has-fx .dialog--wide[open] { animation: lift-in 220ms var(--ease) both; }
@keyframes lift-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* =========================================================================
   10. Utilities
   ========================================================================= */
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

.measure { max-width: 62ch; }
.stack > * + * { margin-top: 1.1rem; }
.center { text-align: center; }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }

/* Email previews and code: scroll sideways instead of pushing the page */
pre { overflow-x: auto; font-size: 0.875rem; background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }

/* =========================================================
   Page additions: ink ticks (services) and the FAQ disclosure
   ========================================================= */
/* A ticked list: each item carries an inline SVG check that draws itself
   when its parent reveal lands. Markup: <li class="reveal"><svg class="tick" viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7.5l3.2 3.2L12 3.5"/></svg>text</li> */
.ticked { list-style: none; margin: 0; padding: 0; }
.ticked li { position: relative; padding-left: 1.6rem; margin-bottom: 0.45rem; }
.tick { position: absolute; left: 0; top: 0.42em; width: 14px; height: 14px; }
.tick path { fill: none; stroke: var(--teal); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
html.has-fx .tick path { stroke-dasharray: 24; stroke-dashoffset: 24; transition: stroke-dashoffset 520ms var(--ease) 180ms; }
html.has-fx .is-in .tick path, html.has-fx .is-in.ticked .tick path { stroke-dashoffset: 0; }

/* FAQ: native disclosure, ruled like the lists, no box */
.faq details { border-bottom: 1px solid var(--line); padding: 0.9rem 0; }
.faq details:first-of-type { border-top: 1px solid var(--line); }
.faq summary { cursor: pointer; list-style: none; font-family: var(--font-display); font-weight: 500; font-size: 1.25rem; color: var(--ink); padding-right: 2rem; position: relative; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: ""; position: absolute; right: 0.4rem; top: 0.55em; width: 9px; height: 9px; border-right: 1.5px solid var(--lagoon); border-bottom: 1.5px solid var(--lagoon); transform: rotate(45deg); transition: transform 200ms var(--ease); }
.faq details[open] summary::after { transform: rotate(225deg); top: 0.8em; }
.faq summary:hover { color: var(--teal); }
.faq details > *:not(summary) { margin-top: 0.6rem; }

/* The hero actions sit on one line and wrap on a phone */
.hero__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 1.5rem; margin-top: 1.75rem; }

/* A lone child of a ruled row spans the row */
.ruled__row > :only-child { grid-column: 1 / -1; }

/* =========================================================================
   The pictures

   Three things: the frame that holds a moving picture, the photographic
   ground under a closing band, and the paper grain over the whole page.
   Everything else about a photograph on this site is already the .frame and
   .ruled__row vocabulary above, which is the point: the pictures arrived
   without a new layout idea.
   ========================================================================= */

/* A frame holding a video rather than a photograph. The mat, the shadow and
   the offset rule are the .frame's; only two things change. The frame takes
   the full measure instead of shrinking to its content, because a moving
   picture at thumbnail size is a distraction rather than a picture. And the
   file is 1280 by 720, so the element is left at its own ratio and height
   follows width: nothing here crops. */
.frame--video {
  width: 100%;
  margin-block: clamp(2.5rem, 5vw, 3.5rem) 0.9rem;
}
.frame__video {
  display: block;
  width: 100%;
  height: auto;
}

/* It develops in like a photograph, because it is one until it plays. It
   does not also drift: the picture is already moving, and a second motion
   on top of the first is motion without a reason. */
html.has-fx .frame--video .frame__video {
  filter: blur(4px);
  transition: filter 1200ms var(--ease);
}
html.has-fx .frame--video.is-developed .frame__video { filter: blur(0); }

/* A photograph standing on its own under a column of prose, rather than in
   a grid cell that spaces it for free, needs that breath by hand. The one
   place this happens is the flower shop at the foot of the home page. */
.measure + .frame { margin-top: clamp(2rem, 4vw, 3rem); }

/* The closing band on a photographic ground.

   Two layers behind the content and neither of them is a picture a visitor
   is asked to look at. The still water sits at the back, the teal covers it
   at 86 per cent, and what is left is a ground that moves a little where a
   flat fill would not. Both layers take a negative z-index, so they paint
   above the band's own teal background and below everything in it.

   The tint is not decoration, it is the contrast. Measured against the
   shipped water.jpg, paper text clears 5.71:1 over the brightest pixel in
   the picture and 6.90:1 across the middle of it, and the mustard button
   clears 3.06:1 against the same worst case. Solid teal gives 7.07:1 and
   3.78:1. Lower the opacity and the text goes with it.

   image-set puts the WebP first and leaves the JPEG in the shorthand above
   as the fallback, which a browser without image-set simply keeps. */
.band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.band--water::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("/assets/img/water.jpg") center / cover no-repeat;
  background-image: image-set(
    url("/assets/img/water.webp") type("image/webp"),
    url("/assets/img/water.jpg") type("image/jpeg")
  );
  z-index: -2;
}
.band--water::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--teal);
  opacity: 0.86;
  z-index: -1;
}

/* The grain.

   One flattened paper tile over the whole viewport, multiplied in at four
   and a half per cent. The tile ships with a luminance range of under seven
   per cent, so on the mist the darkest and lightest pixels of the ground
   land about three quarters of a level apart out of 255: a pixel diff, not
   a texture. If it ever reads as texture, halve the opacity rather than
   changing the tile.

   It sits at z-index 0, which puts it above the breathing ground at -1 and
   below the page. So it tints the ground the page stands on, and never the
   type, the photographs or the tinted and teal bands, which have their own
   grounds and do not want a second one. main and the footer take a z-index
   of their own to guarantee that: every .section is already positioned and
   would paint above it anyway, but the footer is not, and a page whose main
   holds a plain block would put that block underneath.

   It is static, so reduced motion has nothing to say about it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: url("/assets/img/grain.webp") repeat;
  opacity: 0.045;
  mix-blend-mode: multiply;
}
main, .footer {
  position: relative;
  z-index: 1;
}

/* =========================================================================
   Chat panel

   The assistant. Every page carries the launcher and an empty panel
   container, both hidden, and chat.js unhides them only when
   /api/chat/config says a provider is configured. In phase one none of this
   is ever seen, so none of it is allowed to cost the page anything.

   Nothing here invents a color or a surface. The panel is the dialog's own
   frame: paper, one hairline, the 6px corner and the single --shadow token,
   which is also the heaviest shadow the site owns. Links are teal, text is
   ink, the field is .field__input and the send action is .btn .btn--solid,
   so the panel is made of parts the rest of the site already uses.

   The launcher is the one other place a 999px radius is acceptable. The
   rule against pills is a rule about type sitting in a lozenge on the page;
   this is a control floating clear of the page in the lower right, and a
   rounded control reads as a control rather than as a stray button. The
   sticker in the hero is the only other 999px on the site.
   ========================================================================= */
.chat__launch {
  position: fixed;
  right: clamp(1rem, 3vw, 1.75rem);
  bottom: clamp(1rem, 3vw, 1.75rem);
  z-index: 90;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.7rem 1.3rem;
  border: 1px solid var(--ink);
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 1.03125rem;
  line-height: 1.25;
  text-align: center;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: background 160ms var(--ease), border-color 160ms var(--ease), transform 160ms var(--ease);
}
.chat__launch:hover { background: var(--teal); border-color: var(--teal); }
.chat__launch:active { transform: translateY(1px); }

/* The launcher steps aside for as long as the panel is open: on a phone it
   lands squarely on the send row. chat.js takes the class off the document
   element before it hands focus back, so the launcher is always there to
   receive it. */
html.chat-open .chat__launch { display: none; }

.chat__panel {
  position: fixed;
  right: clamp(1rem, 3vw, 1.75rem);
  bottom: clamp(1rem, 3vw, 1.75rem);
  z-index: 91;
  display: flex;
  flex-direction: column;
  width: min(24rem, calc(100vw - 2rem));
  max-height: min(34rem, calc(100vh - 3rem));
  padding: 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow);
  font-size: 1.03125rem;
  line-height: 1.55;
}

.chat__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
}
.chat__title {
  margin: 0;
  font-size: 1.25rem;
}
.chat__close {
  flex: none;
  padding: 0.25rem 0.4rem;
  border: 0;
  border-radius: var(--radius);
  background: transparent;
  color: var(--lagoon);
  font-size: 1.03125rem;
  cursor: pointer;
}
.chat__close:hover { color: var(--ink); }

.chat__status {
  margin: 0.4rem 0 0.9rem;
  padding-bottom: 0.8rem;
  border-bottom: 1px solid var(--line);
  color: var(--lagoon);
  font-size: 0.9375rem;
  line-height: 1.45;
}

.chat__thread {
  flex: 1 1 auto;
  overflow-y: auto;
  padding-right: 0.25rem;
}

/* No bubble, no fill, no box. White space separates the turns and one
   hairline opens each new exchange, the way the ruled lists work. */
.chat__msg { margin: 0 0 1.1rem; }
.chat__msg p { margin: 0 0 0.5rem; }
.chat__msg p:last-child { margin-bottom: 0; }
.chat__msg--you {
  margin-top: 1.1rem;
  padding-top: 1.05rem;
  border-top: 1px solid var(--line);
  text-align: right;
}
.chat__msg--you:first-child { margin-top: 0; padding-top: 0; border-top: 0; }

.chat__note { margin: 0 0 1rem; color: var(--lagoon); font-size: 0.9375rem; }

.chat__typing { display: flex; align-items: center; gap: 0.3rem; min-height: 1.4rem; }
.chat__dot { width: 0.3rem; height: 0.3rem; border-radius: 50%; background: var(--pool); }
html.has-fx .chat__dot { animation: chat-pulse 1.1s ease-in-out infinite; }
html.has-fx .chat__dot:nth-of-type(2) { animation-delay: 0.15s; }
html.has-fx .chat__dot:nth-of-type(3) { animation-delay: 0.3s; }
@keyframes chat-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 0.85; } }

/* The suggested questions are text actions, not buttons: underline and
   hover do the work, as they do everywhere else. */
.chat__suggestions { display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem; margin: 0 0 0.9rem; }
.chat__suggestion {
  min-height: 44px;
  padding: 0.35rem 0;
  border: 0;
  background: transparent;
  color: var(--teal);
  font-size: 0.9375rem;
  text-align: left;
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  cursor: pointer;
}
.chat__suggestion:hover { color: var(--lagoon); text-decoration-thickness: 2px; }

.chat__row { display: flex; align-items: flex-end; gap: 0.6rem; }
/* The element is in the selector on purpose: the page's own
   textarea.field__input stands eight rows tall, and a chat asks for one row
   that grows. Only the height is taken back; the rule, the corner and the
   focus border stay the site's. */
textarea.chat__input {
  flex: 1 1 auto;
  min-height: 44px;
  max-height: 8.75rem;
  padding: 0.6rem 0.75rem;
  font-size: 1.03125rem;
  line-height: 1.45;
  resize: none;
}
.chat__send { flex: none; min-height: 44px; padding: 0.6rem 1.1rem; font-size: 1.03125rem; }
.chat__send[aria-disabled="true"] { background: var(--lagoon); border-color: var(--lagoon); cursor: default; }
.chat__hint { margin: 0.5rem 0 0; color: var(--lagoon); font-size: 0.9375rem; line-height: 1.3; }

.chat__offline { padding-top: 0.9rem; border-top: 1px solid var(--line); }
.chat__offline p { margin: 0 0 0.5rem; font-size: 0.9375rem; }
.chat__offline-links { color: var(--lagoon); }

/* Phones: the panel takes the foot of the screen rather than floating in a
   corner that is most of the screen. */
@media (max-width: 34rem) {
  .chat__panel {
    right: 0;
    left: 0;
    bottom: 0;
    width: auto;
    max-height: 85vh;
    border-left: 0;
    border-right: 0;
    border-bottom: 0;
    border-radius: var(--radius) var(--radius) 0 0;
  }
}

html.has-fx .chat__panel:not([hidden]) { animation: chat-rise 240ms var(--ease) both; }
@keyframes chat-rise { from { opacity: 0; transform: translateY(0.75rem); } to { opacity: 1; transform: none; } }
