@charset "UTF-8";
/* ==========================================================================
   Safe to Plant

   Design direction
   ----------------
   The subject's own artifact is the seed-packet sowing chart: a horizontal
   twelve-month strip with coloured bars for sow, plant and harvest. That chart
   is the signature element here - the Growing Year band - and it is drawn from
   real computed data rather than decoration. Everything else on the page is
   kept quiet so the band carries the personality.

   Type inverts the usual editorial pairing: a tall grotesque for display and a
   serif for reading, which is how agricultural bulletins and extension
   leaflets actually set their pages. Dates and temperatures are set in mono,
   because on this site a date is a measurement, not a word.

   Colour runs on one axis - cold to growing. Frost blue at one end, leaf green
   at the other, with a single hot marigold reserved for "this window is
   closing". Nothing else is allowed to be warm.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Colour ---------------------------------------------------------------- */
  --ink:          #14202a;
  --ink-2:        #4a5a64;
  --ink-3:        #74838b;
  --paper:        #edf0ee;
  --paper-2:      #e3e8e5;
  --card:         #ffffff;
  --rule:         #d3dcd9;
  --rule-soft:    #e4eae7;

  --frost:        #7fa9c4;
  --frost-deep:   #2e5a78;
  --frost-pale:   #dfeaf1;
  --leaf:         #4c7a46;
  --leaf-pale:    #e2ede0;
  --marigold:     #e39a12;
  --marigold-pale:#fbeed2;
  --rust:         #b4552f;
  --rust-pale:    #f6e2d9;

  /* Window colours, one per calendar action ------------------------------- */
  --w-indoor:     #8e6fb0;
  --w-transplant: #2e5a78;
  --w-direct:     #4c7a46;
  --w-autumn:     #7a8a3c;
  --w-plant:      #4c7a46;
  --w-harvest:    #e39a12;

  /* Type ------------------------------------------------------------------ */
  --display: "Darker Grotesque", "Haettenschweiler", "Arial Narrow", sans-serif;
  --body:    "Newsreader", "Iowan Old Style", Georgia, "Times New Roman", serif;
  --mono:    "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  /* A fifth-based scale, tightened at the top so display type can be huge
     without the intermediate steps drifting apart.

     The reading sizes are fixed. Body copy that shrinks on a phone is body copy
     nobody reads, and --t-base must stay at or above 16px or iOS Safari zooms
     the page every time the search field is focused. Only the display steps are
     fluid, and each interpolates between a 320px floor and a 1440px ceiling:
     for v0 -> v1 in rem, the vw coefficient is (v1 - v0) / 0.7 and the constant
     is v0 - 0.2 * that. */
  --t-xs:   0.75rem;
  --t-sm:   0.875rem;
  --t-base: 1.0625rem;
  --t-md:   1.1875rem;
  --t-lg:   clamp(1.2rem, 1.14rem + 0.29vw, 1.4rem);
  --t-xl:   clamp(1.5rem, 1.39rem + 0.57vw, 1.9rem);
  --t-2xl:  clamp(1.85rem, 1.41rem + 2.21vw, 3.4rem);
  --t-3xl:  clamp(2.4rem, 1.49rem + 4.57vw, 5.6rem);

  /* Space ----------------------------------------------------------------- */
  /* The small steps are fixed - they are gaps inside components, and a 4px gap
     is 4px at any width. The large steps are the vertical rhythm between
     sections, and 72px of air above and below every section is most of a phone
     screen, so those interpolate on the same 320 -> 1440 basis as the type. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: clamp(1.5rem, 1.36rem + 0.71vw, 2rem);
  --s-7: clamp(2rem, 1.71rem + 1.43vw, 3rem);
  --s-8: clamp(2.5rem, 1.93rem + 2.86vw, 4.5rem);
  --s-9: clamp(3.5rem, 2.5rem + 5vw, 7rem);

  /* The page gutter. Narrow phones cannot spare 40px of margin, wide screens
     look mean without it. */
  --gutter: clamp(1rem, 0.57rem + 2.14vw, 2.5rem);

  --wrap:  74rem;
  --prose: 38rem;

  /* Height of the sticky header, so anchor targets can be scrolled clear of it
     rather than landing underneath. Two rows on a phone, one from 48rem up;
     the value is overridden alongside the layout change. */
  --masthead-h: 5.75rem;

  /* Minimum comfortable target for a finger. Applied under pointer: coarse
     only - a mouse does not need 44px of button to hit a 20px link. */
  --touch: 2.75rem;

  --r-sm: 3px;
  --r-md: 6px;
  --r-lg: 12px;

  --shadow-1: 0 1px 2px rgb(20 32 42 / 0.05), 0 1px 1px rgb(20 32 42 / 0.04);
  --shadow-2: 0 2px 4px rgb(20 32 42 / 0.05), 0 8px 24px -12px rgb(20 32 42 / 0.18);

  /* Motion ---------------------------------------------------------------- */
  --ease:      cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);
  --dur-1: 140ms;
  --dur-2: 320ms;
  --dur-3: 620ms;
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The header is sticky, so an in-page anchor would otherwise land its target
     underneath it. Every jump stops clear of the bar instead. */
  scroll-padding-top: calc(var(--masthead-h) + var(--s-4));
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: var(--t-base);
  line-height: 1.65;
  font-optical-sizing: auto;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  /* Nothing on this site is wide enough to need a sideways scrollbar. If a
     component ever overflows, clip it here rather than letting the whole
     document rubber-band left and right on a phone. */
  overflow-x: clip;
}

img, svg { max-width: 100%; display: block; }
button, input, select { font: inherit; color: inherit; }
table { border-collapse: collapse; width: 100%; }

:where(a) {
  color: var(--frost-deep);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.16em;
  text-decoration-color: color-mix(in srgb, var(--frost-deep) 35%, transparent);
  transition: text-decoration-color var(--dur-1) var(--ease),
              color var(--dur-1) var(--ease);
}
:where(a):hover {
  color: var(--ink);
  text-decoration-color: currentColor;
}

:focus-visible {
  outline: 2px solid var(--frost-deep);
  outline-offset: 3px;
  border-radius: 2px;
}

::selection { background: var(--frost-pale); color: var(--ink); }

.skip {
  position: absolute;
  left: var(--s-4);
  top: -4rem;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  transition: top var(--dur-2) var(--ease);
}
.skip:focus { top: var(--s-4); }

/* --------------------------------------------------------------------------
   3. Typography
   -------------------------------------------------------------------------- */

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.005em;
  margin: 0 0 var(--s-4);
  text-wrap: balance;
  /* Place names are user data and some of them are very long. At 320px a
     display face set at 38px will happily run past the viewport, so allow a
     break inside a word as a last resort rather than overflowing. */
  overflow-wrap: break-word;
}

h1 { font-size: var(--t-3xl); letter-spacing: -0.015em; }
h2 { font-size: var(--t-2xl); }
h3 { font-size: var(--t-xl); line-height: 1.02; }
h4 { font-size: var(--t-lg); line-height: 1.1; }

p { margin: 0 0 var(--s-4); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

.lede {
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--ink-2);
  max-width: 44rem;
}

/* The eyebrow says what kind of thing this section is. It is a label, so it
   is set in the utility face, not the display face. */
.eyebrow {
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 var(--s-3);
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}
.eyebrow--plain::after { display: none; }

.u-mono   { font-family: var(--mono); font-size: 0.92em; font-variant-numeric: tabular-nums; }
.u-alt    { color: var(--ink-3); font-size: 0.86em; }
.u-quiet  { color: var(--ink-2); }
.u-small  { font-size: var(--t-sm); }
.u-nowrap { white-space: nowrap; }
.u-center { text-align: center; }

/* An unmarked list. Used in asides and notes where the bullet would be noise -
   the items are already a set, and the indent is what the eye reads. The second
   selector is not redundant: `.prose ul` outranks a lone class, so without it
   the utility silently loses its own reset inside prose. */
.u-list,
.prose ul.u-list,
.prose ol.u-list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-2); }

/* Available to screen readers, invisible to everyone else. */
.u-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* A date is a measurement on this site, so it is always set as one. */
.date {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  letter-spacing: -0.02em;
}
.date--lg { font-size: var(--t-xl); letter-spacing: -0.04em; }

.prose { max-width: var(--prose); }
.prose h2 { margin-top: var(--s-7); }
.prose h3 { margin-top: var(--s-6); }
.prose ul, .prose ol { padding-left: 1.25em; margin: 0 0 var(--s-4); }
.prose li { margin-bottom: var(--s-2); }

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */

/* The page gutter is padding rather than a width subtraction, so that a
   notched phone in landscape can widen it to clear the cutout without every
   caller having to know about it. max-width carries the gutters on top of the
   74rem measure, which keeps the content column exactly as wide as before. */
.wrap {
  width: 100%;
  max-width: calc(var(--wrap) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline:
    max(var(--gutter), env(safe-area-inset-left, 0px))
    max(var(--gutter), env(safe-area-inset-right, 0px));
}
.wrap--narrow { max-width: calc(52rem + var(--gutter) * 2); }

.section { padding-block: var(--s-8); }
.section--tight { padding-block: var(--s-6); }
.section + .section { padding-top: 0; }

.rule { border: 0; border-top: 1px solid var(--rule); margin: var(--s-7) 0; }

/* Breadcrumbs. Small, quiet, and never the first thing you read - but they are
   how a visitor who landed on a deep page from search discovers the rest. */
.crumbs { padding-top: var(--s-4); }
.crumbs ol {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.55rem;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  color: var(--ink-3);
}
.crumbs li { display: flex; gap: 0.55rem; }
.crumbs li + li::before { content: "/"; color: var(--rule); }
.crumbs a { color: var(--ink-3); text-decoration: none; }
.crumbs a:hover { color: var(--ink); text-decoration: underline; }
/* A breadcrumb is 12px of text. On a touch screen it needs a bigger target
   than that without becoming visually bigger, so the padding is negative
   margin's twin: it grows the hit area and then takes the space back. */
@media (pointer: coarse) {
  .crumbs ol { gap: 0 0.55rem; }
  .crumbs a { padding-block: 0.55rem; margin-block: -0.55rem; }
}

/* On-page contents. Same voice as the breadcrumbs - mono, small, quiet - and
   for the same reason: it is wayfinding, not content, and the visitor should be
   able to skip it without effort. A single line under the hero rather than a
   sidebar, because the city page's own sidebar is already spoken for and a
   second rail on a 19rem column would collapse to nothing on a phone anyway.
   Scrolls sideways instead of wrapping to three ragged lines when the list is
   long. The jumps work off scroll-padding-top, declared on <html>, so nothing
   lands under the sticky masthead. */
.onpage {
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}
.onpage__inner {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding-block: var(--s-3);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
}
.onpage__label {
  flex: none;
  color: var(--ink-3);
  text-transform: uppercase;
}
.onpage__list {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  align-items: baseline;
  gap: 0 var(--s-4);
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.onpage__list::-webkit-scrollbar { display: none; }
.onpage__list li { flex: none; }
.onpage a { color: var(--ink-2); text-decoration: none; white-space: nowrap; }
.onpage a:hover { color: var(--ink); text-decoration: underline; }
/* Same trick as .crumbs: grow the touch target, then take the space back so
   the line does not get taller. */
@media (pointer: coarse) {
  .onpage a { padding-block: 0.55rem; margin-block: -0.55rem; }
}
/* The label is a nicety, and on a narrow screen the room it takes is better
   spent on the links themselves. */
@media (max-width: 30rem) {
  .onpage__label { display: none; }
}

.stack > * + * { margin-top: var(--s-4); }
.stack--lg > * + * { margin-top: var(--s-6); }

/* One column is the default and the fallback. The aside only becomes a real
   column when there is room for a 19rem rail beside a readable measure. */
.cols {
  display: grid;
  gap: var(--s-7);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 62rem) {
  .cols--main-aside { grid-template-columns: minmax(0, 1fr) 19rem; }
}

/* auto-fill with a floor that fits inside a 320px viewport: 288px of content
   minus nothing, so 15rem tracks would overflow by a hair and force a
   horizontal scrollbar. minmax(0, ...) on the floor via min() keeps a single
   column honest at every width. */
.grid {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
}
.grid--tight { gap: var(--s-3); grid-template-columns: repeat(auto-fill, minmax(min(11rem, 100%), 1fr)); }
.grid--wide  { grid-template-columns: repeat(auto-fill, minmax(min(20rem, 100%), 1fr)); }

/* --------------------------------------------------------------------------
   5. Header
   --------------------------------------------------------------------------
   Search is the site's real navigation. Every page is about somewhere, and
   until the visitor has said where, nothing else is much use - so the field
   has to stay both reachable and wide enough to read a result in.

   Mobile therefore gets one 56px row - brand, search, menu button - and the
   four section links move into a panel behind the button along with the unit
   toggle. The links are worth one tap a session; the field is worth the width.
   The old layout gave the field about 170px and spent a second permanently
   sticky row on links, which is the wrong way round.

   Desktop is untouched by any of this. At 48rem .menu goes back to
   display:contents, which dissolves the wrapper so .nav and .units are direct
   flex children of .masthead__inner again, in their original order, laid out
   by the same rules as before. Nothing about the desktop header is expressed
   in terms of the mobile panel.

   Without JS the wrapper stays display:contents at every width, so the header
   falls back to the two rows it has always been and the button never appears.
   That is why the mobile block below is scoped to .js: the collapsed layout is
   the enhancement, not the baseline. --masthead-h follows the same split, so
   in-page anchors land correctly in all three states. */

.masthead {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--rule);
}

.masthead__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: var(--s-4);
  row-gap: 0;
  min-height: 3rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.3rem;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
  flex: none;
  /* Row one is short; the link should fill it so the whole strip is tappable. */
  padding-block: var(--s-3);
}
.brand:hover { color: var(--frost-deep); }

/* The mark is a thermometer crossing zero - the one idea the site is about. */
.brand__mark {
  width: 0.95rem;
  height: 1.35rem;
  flex: none;
  border-radius: 999px;
  background: linear-gradient(to top, var(--leaf) 0%, var(--leaf) 34%,
              var(--frost) 34%, var(--frost-deep) 100%);
  position: relative;
}
.brand__mark::after {
  content: "";
  position: absolute;
  left: -3px; right: -3px;
  top: 66%;
  height: 1px;
  background: var(--ink);
}

/* Row two. width:100% is what breaks it onto its own line; the negative inline
   margin lets the rule above it and the links inside it run the full width of
   the gutter, so it reads as a band rather than a stray row of text. */
.nav {
  width: 100%;
  order: 3;
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: var(--s-2);
  font-family: var(--mono);
  font-size: var(--t-sm);
  letter-spacing: 0.01em;
  border-top: 1px solid var(--rule-soft);
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}
.nav a {
  color: var(--ink-2);
  text-decoration: none;
  display: flex;
  align-items: center;
  padding: 0.6rem 0.2rem;
  min-height: var(--touch);
}
.nav a:hover, .nav a[aria-current="page"] { color: var(--ink); }
.nav a[aria-current="page"] {
  /* The underline is drawn on the box rather than the text so that it lines up
     with the row's edge instead of floating below a 14px word. */
  box-shadow: inset 0 -2px 0 var(--frost-deep);
}

@media (min-width: 48rem) {
  :root { --masthead-h: 4rem; }

  .masthead__inner { flex-wrap: nowrap; column-gap: var(--s-5); min-height: 4rem; }
  .brand { font-size: 1.65rem; padding-block: 0; }
  .brand__mark { width: 1.1rem; height: 1.55rem; }

  .nav {
    width: auto;
    order: 0;
    margin-left: auto;
    margin-inline-end: 0;
    align-items: center;
    gap: var(--s-5);
    border-top: 0;
    padding-inline: 0;
  }
  .nav a { padding: 0; min-height: 0; }
  .nav a[aria-current="page"] {
    box-shadow: none;
    text-decoration: underline;
    text-underline-offset: 0.35em;
  }
}

/* The menu wrapper and the units wrapper are layout-transparent by default.
   display:contents means the boxes generate no box of their own, so .nav and
   .units are laid out exactly as if the wrappers in the markup were not there.
   This is the whole reason desktop needs no new rules: the default state is
   "wrapper does not exist", and only the mobile block below makes it real. */
.menu,
.menu__units { display: contents; }

/* Both only mean anything inside the panel, so both are off until it exists. */
.menu__label { display: none; }
.menu__btn { display: none; }

/* --------------------------------------------------------------------------
   5b. Masthead - phone layout
   --------------------------------------------------------------------------
   Everything here is inside max-width:47.999rem AND scoped to .js. The first
   keeps desktop out; the second keeps the no-JS fallback out, because a panel
   that only a button can open is worse than no panel at all. */
@media (max-width: 47.999rem) {

  /* One row instead of two. Kept in sync with min-height below - this value is
     what scroll-padding-top uses to clear the sticky header for #anchors. */
  html.js { --masthead-h: 3.5rem; }

  .js .masthead__inner {
    /* nowrap is what stops .nav wrapping to row two; it is safe now because
       .nav is no longer a flex child here - the panel is out of flow. */
    flex-wrap: nowrap;
    column-gap: var(--s-3);
    min-height: 3.5rem;
  }
  /* The row is 56px and the brand no longer needs to pad it out to a tap
     target, because the whole row is not the brand's any more. */
  .js .brand { padding-block: 0; }

  .js .menu__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Last in the row. .units is order 2 but lives in the panel now, so this
       only has to beat the brand and the field, both order 0. */
    order: 4;
    flex: none;
    /* On the home page there is no search field in the header, so nothing else
       absorbs the free space and the button would sit against the brand. With
       a field present the auto margin resolves to zero and changes nothing. */
    margin-inline-start: auto;
    width: var(--touch);
    height: var(--touch);
    padding: 0;
    /* Pull the button's own padding back into the gutter so the glyph sits
       optically on the margin, the way the brand mark does on the left. */
    margin-inline-end: -0.35rem;
    border: 1px solid var(--rule);
    border-radius: var(--r-md);
    background: var(--card);
    color: var(--ink-2);
    cursor: pointer;
    transition: color var(--dur-1) var(--ease),
                border-color var(--dur-1) var(--ease);
  }

  /* Three bars from one element: the span is the middle, its pseudo-elements
     are the outer two. On open they collapse onto the middle and cross, and
     the middle goes transparent rather than disappearing so the two halves
     keep something to rotate around. */
  .js .menu__bars {
    position: relative;
    display: block;
    width: 1.05rem;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
  }
  .js .menu__bars::before,
  .js .menu__bars::after {
    content: "";
    position: absolute;
    left: 0;
    width: 100%;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
    transition: transform var(--dur-1) var(--ease), top var(--dur-1) var(--ease);
  }
  .js .menu__bars::before { top: -6px; }
  .js .menu__bars::after  { top: 6px; }

  .js .menu__btn[aria-expanded="true"] {
    color: var(--ink);
    border-color: var(--ink-3);
  }
  .js .menu__btn[aria-expanded="true"] .menu__bars { background: transparent; }
  .js .menu__btn[aria-expanded="true"] .menu__bars::before {
    top: 0; transform: rotate(45deg);
  }
  .js .menu__btn[aria-expanded="true"] .menu__bars::after {
    top: 0; transform: rotate(-45deg);
  }

  /* The panel hangs off .masthead, not off .masthead__inner: .masthead is
     position:sticky and therefore a containing block, while .masthead__inner
     is .wrap and carries the gutters. Anchoring to the outer element is what
     lets the panel run edge to edge under a header whose content does not. */
  .js .menu {
    display: block;
    position: absolute;
    top: 100%;
    inset-inline: 0;
    background: var(--card);
    border-bottom: 1px solid var(--rule);
    box-shadow: var(--shadow-2);
    /* Under .search__results (z-index 50) on purpose: if both are somehow up,
       the one the visitor is typing into should be the one on top. */
    z-index: 45;
  }
  /* Closed is the server-rendered state, so the panel is never briefly visible
     on load the way a JS-applied hide would be. */
  .js .menu[data-open="false"] { display: none; }

  .js .menu .nav {
    width: auto;
    order: 0;
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    border-top: 0;
    margin-inline: 0;
    padding-inline: 0;
    font-size: var(--t-sm);
  }
  .js .menu .nav a {
    padding: 0.8rem var(--gutter);
    min-height: 3rem;
    border-bottom: 1px solid var(--rule-soft);
  }
  /* In a vertical list an underline under the row reads as a divider, not as
     "you are here". A bar down the leading edge cannot be confused with one. */
  .js .menu .nav a[aria-current="page"] {
    box-shadow: inset 3px 0 0 var(--frost-deep);
    background: var(--frost-pale);
  }

  .js .menu__units {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-4);
    padding: 0.7rem var(--gutter) 0.85rem;
  }
  .js .menu__units .menu__label {
    display: block;
    font-family: var(--mono);
    font-size: var(--t-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-3);
  }
  /* The toggle drops to a bare C/F under 30rem because the header row has no
     spare width. In the panel it has a whole row, so the long form comes back
     and the visitor sees what the letters actually switch. */
  .js .menu__units .units__long { display: inline; }

  /* Auto-hide. The header gives back its 56px on the way down a long calendar
     and returns on the first upward flick, so the search field is one gesture
     away anywhere on the page rather than permanently costing a row of it.
     Scoped to .js and to phones; the desktop header never moves. */
  .js .masthead {
    transition: transform var(--dur-2) var(--ease);
    will-change: transform;
  }
  .js .masthead[data-hidden="true"] { transform: translateY(-100%); }
}

/* Below 380px the wordmark is about 105px that the search field wants more.
   Clipped rather than display:none - the mark is aria-hidden, so removing the
   text would leave the home link with no accessible name at all. Only under
   .js, because the no-JS header still has a whole row for the brand. */
@media (max-width: 23.75rem) {
  .js .brand__name {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* Unit toggle. It sits in the masthead because the choice applies to every
   number on every page, not to one table - but it is the least important
   control up there, so it is drawn as a small segmented pair rather than
   anything that competes with the search field.

   Pure script, like .locate: without JS the metric half of each pair is
   already the visible one and there is nothing for the button to do. */
.units {
  display: flex;
  flex: none;
  order: 2;
  align-items: stretch;
  border: 1px solid var(--rule);
  border-radius: 999px;
  overflow: hidden;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.02em;
  background: var(--card);
}
.no-js .units { display: none; }

.units__opt {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--ink-3);
  padding: 0.3rem 0.6rem;
  font: inherit;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease);
}
.units__opt + .units__opt { border-left: 1px solid var(--rule); }
.units__opt:hover { color: var(--ink); }
.units__opt[aria-pressed="true"] {
  background: var(--frost-pale);
  color: var(--frost-deep);
  font-weight: 600;
}
/* The abbr carries the long form of each unit for screen readers and hover.
   Its default dotted underline would fight the button shape, so it is off. */
.units__opt abbr { text-decoration: none; border: 0; cursor: inherit; }

/* Row one at 320px is brand plus search field with nothing to spare, so the
   toggle shrinks to a bare C/F there. The abbr title still says which units
   those letters mean, and the full form returns as soon as there is room. */
.units__long { display: none; }
@media (min-width: 30rem) {
  .units__long { display: inline; }
}

@media (pointer: coarse) {
  .units__opt { min-height: var(--touch); padding-inline: 0.7rem; }
}

/* --------------------------------------------------------------------------
   6. Search
   --------------------------------------------------------------------------
   The flex-basis is the load-bearing number here. At 320px the brand takes
   about 100px of a 288px row, so a basis of 8rem lets the field share that row
   instead of wrapping onto a third one and making the sticky header a third of
   the screen. */

.search { position: relative; flex: 1 1 8rem; min-width: 0; }
.search--hero { max-width: 32rem; flex: none; width: 100%; }

.search__field {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.35rem 0.4rem 0.35rem 0.8rem;
  transition: border-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.search--hero .search__field { padding: 0.55rem 0.55rem 0.55rem 1.1rem; }
.search__field:focus-within {
  border-color: var(--frost-deep);
  box-shadow: 0 0 0 4px var(--frost-pale);
}

.search__input {
  flex: 1;
  border: 0;
  background: none;
  outline: none;
  /* Must not drop below 16px: iOS Safari zooms the viewport on focus if it
     does, and the visitor is then stranded at 1.3x with no way back. */
  font-size: var(--t-base);
  min-width: 0;
}
.search--hero .search__input { font-size: var(--t-md); }
.search__input::placeholder { color: var(--ink-3); }
/* The clear button Safari and Chrome add to type=search is drawn at the far
   right, inside our own padding, where it collides with Go. */
.search__input::-webkit-search-decoration,
.search__input::-webkit-search-cancel-button { -webkit-appearance: none; }

.search__go {
  flex: none;
  border: 0;
  border-radius: 999px;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.5rem 0.7rem;
  cursor: pointer;
  transition: background var(--dur-1) var(--ease);
}
.search__go:hover { background: var(--frost-deep); }

@media (min-width: 48rem) {
  .search { flex: 1 1 16rem; max-width: 26rem; }
  .search__field { gap: var(--s-3); padding: 0.45rem 0.5rem 0.45rem 1rem; }
  .search--hero .search__field { padding: 0.7rem 0.7rem 0.7rem 1.35rem; }
  .search__go { padding: 0.5rem 0.95rem; }
}

@media (pointer: coarse) {
  .search__field { min-height: var(--touch); }
  .search__go { min-height: 2.25rem; }
}

.search__results {
  position: absolute;
  inset-inline: 0;
  top: calc(100% + 0.4rem);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Ten hits at 44px would run off the bottom of a phone. Cap the list at what
     fits below the header and let it scroll inside itself. */
  max-height: min(60vh, 26rem);
  z-index: 50;
  display: none;
}
.search__results[data-open="true"] { display: block; }

/* In the header the field is only about 170px wide on a phone, which is not
   enough to read "Melbourne - Victoria, Australia" in. The panel breaks out of
   it and is anchored to the right, where the field's own right edge is already
   the edge of the content column, so it cannot leave the viewport. */
@media (max-width: 47.999rem) {
  .masthead .search__results {
    inset-inline: auto 0;
    width: max(100%, min(17rem, calc(100vw - var(--gutter) * 2)));
  }
}

.search__hit {
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
  padding: 0.6rem 1rem;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid var(--rule-soft);
}
.search__hit:last-child { border-bottom: 0; }
.search__hit:hover, .search__hit[data-active="true"] {
  background: var(--frost-pale);
  color: var(--ink);
}
.search__hit strong { font-weight: 600; }
.search__hit span { font-size: var(--t-sm); color: var(--ink-3); margin-left: auto; }
@media (pointer: coarse) {
  /* A row of results is a list of things to hit with a thumb. */
  .search__hit { padding-block: 0.7rem; min-height: var(--touch); align-items: center; }
}

/* "Did you mean X?", sitting above the hits it explains. It has to read as a
   remark about the list rather than the first row of it, or a reader will try
   to click it: hence the tinted strip, the smaller type, and no hover state.
   The keyboard handler only walks .search__hit[href], so this is skipped when
   arrowing down without needing to be told to be. */
.search__note {
  padding: 0.5rem 1rem;
  font-size: var(--t-sm);
  color: var(--ink-2);
  background: var(--frost-pale);
  border-bottom: 1px solid var(--rule);
}
.search__note strong { color: var(--ink); font-weight: 600; }

/* "Use my location". Deliberately quiet - a text button rather than a second
   filled one, because the field beside it is the primary action and two equal
   buttons would make the visitor choose before they had read either.

   Hidden until app.js drops .no-js, since there is no server-side way to do
   this and a button that cannot work is worse than no button. */
.locate {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-2) var(--s-3);
  margin-top: var(--s-3);
}
.no-js .locate { display: none; }

.locate__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  border: 0;
  background: none;
  padding: 0.3rem 0;
  cursor: pointer;
  font-family: var(--mono);
  font-size: var(--t-sm);
  color: var(--frost-deep);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--frost-deep) 35%, transparent);
  text-underline-offset: 0.2em;
  transition: color var(--dur-1) var(--ease),
              text-decoration-color var(--dur-1) var(--ease);
}
.locate__btn:hover { color: var(--ink); text-decoration-color: currentColor; }
.locate__btn[disabled] { color: var(--ink-3); cursor: progress; text-decoration: none; }
@media (pointer: coarse) {
  .locate__btn { min-height: var(--touch); }
}

.locate__icon { width: 0.95rem; height: 0.95rem; flex: none; }
/* Spun while the browser is asking the device where it is, which on a cold GPS
   is long enough that a static label reads as a dead button. */
.locate__btn[disabled] .locate__icon { animation: spin 1.1s linear infinite; }
@media (prefers-reduced-motion: reduce) {
  .locate__btn[disabled] .locate__icon { animation: none; }
}
@keyframes spin { to { transform: rotate(360deg); } }

.locate__msg { font-size: var(--t-sm); color: var(--ink-2); }
.locate__msg[data-tone="warn"] { color: #8a5c04; }

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-block: var(--s-8) var(--s-7);
  position: relative;
  overflow: hidden;
}

/* Ambient frost: a very faint radial wash at the cold end of the page. It is
   the only purely decorative element on the site, and it is nearly invisible
   on purpose. */
.hero::before {
  content: "";
  position: absolute;
  inset: -30% -10% auto -10%;
  height: 60vh;
  background: radial-gradient(60% 60% at 22% 0%,
              color-mix(in srgb, var(--frost) 22%, transparent) 0%, transparent 70%);
  pointer-events: none;
}
.hero > * { position: relative; }

.hero__title { margin-bottom: var(--s-4); }
.hero__title em {
  font-style: normal;
  color: var(--frost-deep);
}

.hero--city {
  padding-bottom: var(--s-6);
  border-bottom: 1px solid var(--rule);
}

.place {
  font-family: var(--mono);
  font-size: var(--t-sm);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Two big numbers, side by side: the answer people came for. On a phone they
   stack, which is the right reading order anyway - last frost, then first. */
.answer {
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  margin-block: var(--s-6);
  padding: var(--s-5) 0;
  border-block: 1px solid var(--rule);
}
.answer__cell { min-width: 0; }
.answer__label {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--s-2);
}
.answer__value {
  font-family: var(--mono);
  font-size: clamp(1.9rem, 1.2rem + 2.4vw, 2.9rem);
  font-weight: 600;
  letter-spacing: -0.045em;
  line-height: 1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.answer__value--frost { color: var(--frost-deep); }
.answer__value--leaf  { color: var(--leaf); }
.answer__note { font-size: var(--t-sm); color: var(--ink-2); margin-top: var(--s-2); }

/* Numbers change when the risk dial moves. A brief tint makes it legible
   rather than magical. */
.answer__value.is-changing { animation: valueSwap var(--dur-2) var(--ease); }
@keyframes valueSwap {
  0%   { opacity: 0.25; transform: translateY(0.22em); }
  100% { opacity: 1;    transform: none; }
}

/* --------------------------------------------------------------------------
   8. Risk dial
   -------------------------------------------------------------------------- */

.dial {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-block: var(--s-5);
}

/* On a phone the track is a full-width three-up grid. Laid out as inline-flex
   it came to about 275px of a 288px row, which is close enough to the edge
   that a fallback mono face - Menlo is wider than Plex - would push it over
   and start a horizontal scroll on the whole page. Equal fractions cannot
   overflow whatever the label metrics turn out to be. */
.dial__track {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  width: 100%;
  position: relative;
  background: var(--paper-2);
  border-radius: 999px;
  padding: 3px;
  isolation: isolate;
}

.dial__thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow-1);
  transition: transform var(--dur-2) var(--ease-out), width var(--dur-2) var(--ease-out);
  z-index: -1;
}

.dial__opt {
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: 999px;
  padding: 0.55rem 0.4rem;
  font-family: var(--mono);
  font-size: var(--t-sm);
  letter-spacing: 0.02em;
  color: var(--ink-3);
  white-space: nowrap;
  transition: color var(--dur-2) var(--ease);
}
.dial__opt[aria-pressed="true"] { color: var(--ink); }
.dial__opt:hover { color: var(--ink-2); }

@media (min-width: 34rem) {
  .dial__track { display: inline-flex; width: auto; }
  .dial__opt { padding: 0.42rem 1.05rem; }
}
@media (pointer: coarse) {
  .dial__opt { min-height: var(--touch); }
}

.dial__blurb {
  font-size: var(--t-sm);
  color: var(--ink-2);
  max-width: 26rem;
  min-height: 1.6em;
}

/* No-JS fallback. The links only exist inside <noscript>, so they need no
   display toggle of their own; the .no-js class on <html> (removed by app.js
   on boot) is what hides the button track that would otherwise do nothing. */
.dial__links { display: inline-flex; gap: var(--s-3); align-items: baseline; }
.no-js .dial__track { display: none; }

/* --------------------------------------------------------------------------
   9. THE GROWING YEAR BAND - signature element
   -------------------------------------------------------------------------- */

.band {
  --band-h: 2.05rem;
  --band-gap: 0.3rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s-4) var(--s-3) var(--s-3);
  box-shadow: var(--shadow-1);
  position: relative;
  overflow: hidden;
}

.band__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.band__title {
  font-family: var(--display);
  font-weight: 800;
  font-size: var(--t-lg);
  line-height: 1;
  margin: 0;
}
/* On a phone the head wraps, and a subtitle pushed to the right edge of its own
   line looks like a mistake. It only earns margin-left: auto once it is sharing
   the line with the title. */
.band__sub { font-size: var(--t-sm); color: var(--ink-3); }
@media (min-width: 48rem) {
  .band__sub { margin-left: auto; }
}

/* The plot area. Everything inside is positioned in percentage across the
   calendar year, which is exactly what Frost::yearFraction returns. */
.band__plot {
  position: relative;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--paper);
}

/* Frost risk wash. The gradient stops are written inline by PHP from the
   real weekly probability curve, so the blue you see is the actual chance of
   frost that week - the decoration is the data. */
.band__risk {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.band__months {
  display: grid;
  position: relative;
  z-index: 2;
  border-bottom: 1px solid var(--rule);
}
.band__month {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0.35rem 0 0.3rem 0.4rem;
}

/* Month gridlines are drawn once, full height, using the real length of each
   month rather than twelve equal columns - otherwise a bar sitting on 1 March
   lands a few pixels inside February. */
.band__gridlines {
  position: absolute;
  inset: 0;
  display: grid;
  z-index: 1;
  pointer-events: none;
}
.band__gridlines > span { border-left: 1px solid var(--rule-soft); }
.band__gridlines > span:first-child { border-left: 0; }

.band__rows {
  position: relative;
  z-index: 2;
  padding: var(--s-3) 0;
  display: flex;
  flex-direction: column;
  gap: var(--band-gap);
}

.band__row {
  position: relative;
  height: var(--band-h);
  display: flex;
  align-items: center;
}
.band__row:nth-child(even)::before {
  content: "";
  position: absolute;
  inset: calc(var(--band-gap) / -2) 0;
  background: color-mix(in srgb, var(--ink) 2.5%, transparent);
}

.band__rowlabel {
  position: absolute;
  left: 0.5rem;
  z-index: 3;
  font-size: var(--t-sm);
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  max-width: 9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 0 4px var(--card), 0 0 8px var(--card);
}
.band__rowlabel:hover { color: var(--frost-deep); }

/* A window bar. left/width are set inline as percentages. Sowing and planting
   bars ride the top of the row; harvest rides the bottom, so a crop's whole
   life cycle is one row without the bars colliding. */
.band__bar {
  position: absolute;
  top: 0.15rem;
  height: 1.15rem;
  border-radius: 3px;
  z-index: 2;
  background: var(--bar, var(--frost-deep));
  transition: left var(--dur-3) var(--ease-out), width var(--dur-3) var(--ease-out);
  transform-origin: left center;
  cursor: default;
}
.band__bar[data-type="indoor"]     { --bar: var(--w-indoor); }
.band__bar[data-type="transplant"] { --bar: var(--w-transplant); }
.band__bar[data-type="direct"]     { --bar: var(--w-direct); }
.band__bar[data-type="autumn"]     { --bar: var(--w-autumn); }
.band__bar[data-type="plant"]      { --bar: var(--w-plant); }
.band__bar[data-type="harvest"] {
  --bar: repeating-linear-gradient(
    -45deg,
    var(--w-harvest) 0 4px,
    color-mix(in srgb, var(--w-harvest) 55%, var(--card)) 4px 8px
  );
  top: auto;
  bottom: 0.2rem;
  height: 0.55rem;
  border-radius: 999px;
  /* Harvest is a hopeful estimate, not an instruction, so it is drawn as a
     lighter, striped rail rather than a solid block. The stripes live in --bar
     rather than in `background` so that the wrapped tail below picks them up
     without restating them. */
}

/* A window that crosses 31 December - a bare-root tree planted in dormancy,
   or almost any window in the southern hemisphere - is one continuous span of
   time but two separate stretches of a calendar year. The head is the element
   itself, running to the right-hand edge; this draws the remainder back at the
   left-hand edge. Both halves belong to one element so that hovering either
   shows the same title and the risk dial only has one thing to re-time.

   --tail-left and --tail-width are set inline, in percentages of this bar's own
   width, which is the box a pseudo-element resolves against. */
.band__bar.is-wrapped::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--tail-left, 0%);
  width: var(--tail-width, 0%);
  background: var(--bar, var(--frost-deep));
  border-radius: inherit;
}

/* Frost posts: the two dates the whole site is built around. */
.band__post {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 0;
  z-index: 4;
  border-left: 2px dashed var(--frost-deep);
  transition: left var(--dur-3) var(--ease-out);
  pointer-events: none;
}
.band__post::before {
  content: attr(data-label);
  position: absolute;
  top: 0.15rem;
  left: 0.35rem;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--frost-deep);
  background: color-mix(in srgb, var(--card) 88%, transparent);
  padding: 0.05rem 0.3rem;
  border-radius: 2px;
  white-space: nowrap;
}
.band__post--first::before { left: auto; right: 0.35rem; }

/* Today. A thin solid line, deliberately the only red-brown thing here. */
.band__today {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: var(--rust);
  z-index: 5;
  pointer-events: none;
}
.band__today::after {
  content: "Today";
  position: absolute;
  bottom: 0.1rem;
  left: 0.3rem;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--rust);
  background: color-mix(in srgb, var(--card) 85%, transparent);
  padding: 0 0.25rem;
  border-radius: 2px;
}

/* Some crops stop being possible under the cautious posture - the season is
   simply too short. They are dimmed rather than removed, so the reader can see
   that moving the dial is what changed the answer. */
.band__row.is-unavailable { opacity: 0.32; }
.band__row.is-unavailable .band__bar { filter: grayscale(1); }
.is-unavailable .sow__what { text-decoration: line-through; }

/* Touch readout. Anything drawn as a bar carries its detail in title=, which a
   phone never fires - there is no hover to fire it with. Tapping the bar writes
   the same text here instead, so the chart is not a picture of data that only
   desktop visitors can read. One class, used by every chart on the site. */
.band__readout,
.months__readout {
  margin-top: var(--s-3);
  min-height: 1.4em;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--ink-2);
}

.band__key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  margin-top: var(--s-4);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  color: var(--ink-2);
}
.band__keyitem { display: inline-flex; align-items: center; gap: 0.4rem; }
.band__swatch {
  width: 1.1rem; height: 0.5rem; border-radius: 2px;
  background: var(--sw, var(--frost-deep));
  flex: none;
}

/* Load choreography: the frost posts arrive first, then the bars wipe in from
   the left in sequence. One orchestrated moment, not scattered effects. */
@media (prefers-reduced-motion: no-preference) {
  .band[data-animate="true"] .band__post {
    animation: postDrop var(--dur-3) var(--ease-out) backwards;
  }
  .band[data-animate="true"] .band__post--last  { animation-delay: 80ms; }
  .band[data-animate="true"] .band__post--first { animation-delay: 180ms; }

  /* Wrapped bars sit this one out: the wipe scales the element horizontally,
     and the tail's offset is expressed as a multiple of that same width, so it
     would swing across the band for the length of the animation. */
  .band[data-animate="true"] .band__bar:not(.is-wrapped) {
    animation: barWipe 520ms var(--ease-out) backwards;
    animation-delay: calc(260ms + var(--i, 0) * 34ms);
  }
  .band[data-animate="true"] .band__rowlabel {
    animation: labelIn 420ms var(--ease-out) backwards;
    animation-delay: calc(240ms + var(--i, 0) * 34ms);
  }
}

@keyframes postDrop {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to   { opacity: 1; transform: none; }
}
@keyframes barWipe {
  from { opacity: 0; transform: scaleX(0.02); }
  to   { opacity: 1; transform: scaleX(1); }
}
@keyframes labelIn {
  from { opacity: 0; transform: translateX(-0.4rem); }
  to   { opacity: 1; transform: none; }
}

/* Twelve months compressed into 288px is not a chart, it is a smear. So the
   band has a floor of 44rem and scrolls sideways below that, which also means
   the plot is the same 704px wide on a phone as it is on a 44rem desktop -
   every measurement inside it, including the row labels, holds at both.

   On wide screens the floor is below the available width and none of this does
   anything, which is why there is no breakpoint here. */
.band__scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  /* Without this a sideways swipe that runs out of band keeps going and
     triggers the browser's back gesture, losing the page. */
  overscroll-behavior-x: contain;
  /* The scroller is focusable so a keyboard can reach the year. .band clips its
     overflow, so the ring is drawn inside the box or not at all. */
  outline-offset: -2px;
}
.band__plot { min-width: 44rem; }

/* app.js measures the scroller and sets .is-scrollable when there is genuinely
   more year off-screen, plus .is-atstart / .is-atend for where in it we are.
   Doing it in JS rather than at a breakpoint means the hint is never a lie -
   and without JS the band still scrolls, it just does not advertise it.

   The fade is a mask on the scroller rather than a gradient over it, so it sits
   at the edge while the content moves underneath, and it only appears on the
   side that has something hidden behind it. Fading December when December is
   already fully on screen would be worse than not fading at all. */
.band__scroll.is-scrollable,
.table__scroll.is-scrollable {
  --fade-l: 0px;
  --fade-r: 2.5rem;
  /* Prefixed first for Safari before 15.4, which is still a real share of the
     phones this site is read on. Where neither is understood the fade simply
     does not happen and nothing else changes. */
  -webkit-mask-image: linear-gradient(to right,
                      transparent 0, #000 var(--fade-l),
                      #000 calc(100% - var(--fade-r)), transparent 100%);
  mask-image: linear-gradient(to right,
              transparent 0, #000 var(--fade-l),
              #000 calc(100% - var(--fade-r)), transparent 100%);
}
.is-scrollable:not(.is-atstart) { --fade-l: 2.5rem; }
.is-scrollable.is-atend        { --fade-r: 0px; }

.band__hint {
  display: none;
  margin-top: var(--s-2);
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  color: var(--ink-3);
}
/* Shown only while there is still year to the right. Once the reader has got
   there the hint has done its job and taking it away is quieter than leaving a
   stale instruction under the chart. */
.band:has(.band__scroll.is-scrollable:not(.is-atend)) .band__hint { display: block; }

@media (min-width: 48rem) {
  .band { padding: var(--s-5) var(--s-5) var(--s-4); }
}

/* --------------------------------------------------------------------------
   10. Cards and panels
   -------------------------------------------------------------------------- */

.card {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s-5);
  box-shadow: var(--shadow-1);
}
.card--flush { padding: 0; overflow: hidden; }
.card__head {
  padding: var(--s-4) var(--s-5);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: baseline;
  gap: var(--s-3);
}
.card__body { padding: var(--s-5); }

/* A link card: used for towns and crops. The whole tile is the target. */
.tile {
  display: block;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: var(--s-4);
  text-decoration: none;
  color: var(--ink);
  transition: border-color var(--dur-1) var(--ease),
              transform var(--dur-1) var(--ease),
              box-shadow var(--dur-1) var(--ease);
}
.tile:hover {
  border-color: var(--frost);
  transform: translateY(-1px);
  box-shadow: var(--shadow-2);
  color: var(--ink);
}
.tile__name { font-weight: 600; font-size: var(--t-md); line-height: 1.2; }
.tile__meta {
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--ink-3);
  margin-top: var(--s-1);
  display: flex;
  gap: var(--s-3);
  flex-wrap: wrap;
}
.tile__dates {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--rule-soft);
  font-family: var(--mono);
  font-size: var(--t-sm);
  color: var(--ink-2);
  display: flex;
  justify-content: space-between;
  gap: var(--s-2);
}

/* Statement panel for the one number a page is about. */
.figure {
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  display: grid;
  gap: var(--s-2);
}
.figure__value {
  font-family: var(--mono);
  font-size: clamp(3rem, 2rem + 5vw, 5rem);
  font-weight: 600;
  line-height: 0.9;
  letter-spacing: -0.05em;
}
.figure__label {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper);
  color: color-mix(in srgb, var(--paper) 62%, transparent);
}
.figure__note { color: var(--paper); color: color-mix(in srgb, var(--paper) 78%, transparent); font-size: var(--t-sm); }

/* --------------------------------------------------------------------------
   11. Tables
   -------------------------------------------------------------------------- */

.table { font-size: var(--t-sm); }
.table th, .table td {
  text-align: left;
  padding: 0.62rem 0.5rem;
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: baseline;
}
@media (min-width: 48rem) {
  .table th, .table td { padding-inline: 0.75rem; }
}
.table th {
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  border-bottom-color: var(--rule);
  white-space: nowrap;
}
.table tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background var(--dur-1) var(--ease); }
.table tbody tr:hover { background: var(--frost-pale); }
.table td.num, .table th.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}
.table caption {
  caption-side: bottom;
  text-align: left;
  font-size: var(--t-xs);
  color: var(--ink-3);
  padding-top: var(--s-3);
}

/* A wide table on a phone scrolls rather than wrapping every cell to three
   lines. Same reasoning, and the same overscroll guard, as the growing-year
   band: a swipe that runs past the end must not navigate away. */
.table__scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
}
/* The edge fade itself is defined alongside the band's, in section 9, so both
   scrollers share one mask and one set of edge classes. A table's fade is
   narrower than the band's - 2rem rather than 2.5rem - because a fade wider
   than the last column reads as a rendering fault. Both overrides carry the
   same :not() guards as the originals: this block sits later in the file, so
   without them it would win over the at-start / at-end rules and reinstate a
   fade over an edge that has nothing behind it. */
.table__scroll.is-scrollable:not(.is-atend)   { --fade-r: 2rem; }
.table__scroll.is-scrollable:not(.is-atstart) { --fade-l: 2rem; }
@media (pointer: coarse) {
  /* A table row is often a link to a town. Give it a thumb-sized target. */
  .table td { padding-block: 0.8rem; }
}

/* --------------------------------------------------------------------------
   12. Badges and notes
   -------------------------------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.05em;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: var(--paper-2);
  color: var(--ink-2);
  white-space: nowrap;
}
.badge--frost   { background: var(--frost-pale);    color: var(--frost-deep); }
.badge--leaf    { background: var(--leaf-pale);     color: var(--leaf); }
.badge--warn    { background: var(--marigold-pale); color: #8a5c04; }
.badge--danger  { background: var(--rust-pale);     color: var(--rust); }

.note {
  border-left: 3px solid var(--frost);
  background: var(--card);
  padding: var(--s-4) var(--s-5);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  font-size: var(--t-sm);
  color: var(--ink-2);
}
.note--warn   { border-left-color: var(--marigold); }
.note--danger { border-left-color: var(--rust); }
.note strong { color: var(--ink); }

/* Sample-data banner. Deliberately loud: pages built from generated numbers
   must never be mistaken for pages built from observations. */
.banner {
  background: var(--rust);
  color: #fff;
  font-size: var(--t-sm);
  padding: var(--s-3) 0;
}
.banner a { color: #fff; text-decoration-color: rgb(255 255 255 / 0.55); }
.banner__inner { display: flex; gap: var(--s-3); align-items: baseline; flex-wrap: wrap; }
.banner strong { font-family: var(--mono); letter-spacing: 0.06em; text-transform: uppercase; font-size: var(--t-xs); }

/* --------------------------------------------------------------------------
   13. Sow-now list
   -------------------------------------------------------------------------- */

.sow { display: grid; gap: var(--s-2); }

.sow__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--s-3);
  padding: 0.6rem 0.75rem;
  border-radius: var(--r-md);
  background: var(--card);
  border: 1px solid var(--rule-soft);
  text-decoration: none;
  color: var(--ink);
  transition: border-color var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.sow__item:hover { border-color: var(--frost); background: var(--frost-pale); color: var(--ink); }
.sow__what { font-weight: 600; }
.sow__how { font-size: var(--t-sm); color: var(--ink-2); display: block; font-weight: 400; }
.sow__when { font-family: var(--mono); font-size: var(--t-xs); color: var(--ink-3); text-align: right; }
.sow__item[data-urgency="closing"] { border-color: var(--marigold); }
.sow__item[data-urgency="closing"] .sow__when { color: #8a5c04; font-weight: 500; }
/* Below about 380px the crop name and the deadline fight for the same row and
   both lose. Stacking puts the name on its own line and drops the date under
   it, left-aligned, where it reads as a caption rather than a column. */
@media (max-width: 23.75rem) {
  .sow__item { grid-template-columns: minmax(0, 1fr); }
  .sow__when { text-align: left; }
}

/* --------------------------------------------------------------------------
   14. Frost risk curve (frost-dates page)
   -------------------------------------------------------------------------- */

.curve { width: 100%; height: 9rem; display: block; overflow: visible; }
.curve__area { fill: var(--frost); opacity: 0.22; }
.curve__line { fill: none; stroke: var(--frost-deep); stroke-width: 1.75; stroke-linejoin: round; }
.curve__axis { stroke: var(--rule); stroke-width: 1; }
.curve__tick {
  font-family: var(--mono);
  font-size: 9px;
  fill: var(--ink-3);
  letter-spacing: 0.06em;
}

@media (prefers-reduced-motion: no-preference) {
  .curve__line {
    stroke-dasharray: var(--len, 2000);
    stroke-dashoffset: var(--len, 2000);
    animation: draw 1.5s var(--ease-out) 180ms forwards;
  }
  .curve__area { animation: fade 900ms var(--ease) 700ms backwards; }
}
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes fade { from { opacity: 0; } }

/* Monthly climate strip -----------------------------------------------------*/
.months {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 2px;
  align-items: end;
  height: 7rem;
}
.months__bar {
  position: relative;
  /* Flat fallback first. A gradient built from color-mix() is a single
     declaration, so a browser that cannot parse color-mix() throws the whole
     background away and the bar renders empty - not a degraded chart, an
     invisible one. This line is what it falls back to. */
  background: var(--leaf);
  background: linear-gradient(to top,
    color-mix(in srgb, var(--frost) 55%, transparent),
    color-mix(in srgb, var(--leaf) 45%, transparent));
  border-radius: 2px 2px 0 0;
  min-height: 2px;
}
.months__label {
  font-family: var(--mono);
  font-size: 0.6rem;
  text-align: center;
  color: var(--ink-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding-top: 0.25rem;
}
.months__frost { background: var(--frost-deep); }

/* --------------------------------------------------------------------------
   15. Crop page pieces
   -------------------------------------------------------------------------- */

.steps { display: grid; gap: var(--s-3); counter-reset: step; }

.step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--s-4);
  align-items: start;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  padding: var(--s-4) var(--s-4);
}
@media (min-width: 34rem) {
  .step { padding-inline: var(--s-5); }
}
/* The marker is the window's colour, not a number: the order of steps is
   already given by the dates, so numbering them would repeat information. */
.step__dot {
  width: 0.7rem; height: 0.7rem;
  border-radius: 2px;
  margin-top: 0.55rem;
  background: var(--bar, var(--frost-deep));
  flex: none;
}
.step[data-type="indoor"]     { --bar: var(--w-indoor); }
.step[data-type="transplant"] { --bar: var(--w-transplant); }
.step[data-type="direct"]     { --bar: var(--w-direct); }
.step[data-type="autumn"]     { --bar: var(--w-autumn); }
.step[data-type="plant"]      { --bar: var(--w-plant); }
.step[data-type="harvest"]    { --bar: var(--w-harvest); }
.step__label {
  font-family: var(--display);
  font-weight: 800;
  font-size: var(--t-lg);
  line-height: 1;
}
.step__date {
  font-family: var(--mono);
  font-size: var(--t-md);
  color: var(--frost-deep);
  font-variant-numeric: tabular-nums;
  margin-top: var(--s-1);
}
.step__note { font-size: var(--t-sm); color: var(--ink-2); margin-top: var(--s-2); }

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(9rem, 100%), 1fr));
  gap: var(--s-4);
  border-top: 1px solid var(--rule);
  padding-top: var(--s-4);
}
.facts__k {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.facts__v { font-size: var(--t-md); font-weight: 500; margin-top: var(--s-1); }

/* --------------------------------------------------------------------------
   16. FAQ
   -------------------------------------------------------------------------- */

.faq { border-top: 1px solid var(--rule); }
.faq__item { border-bottom: 1px solid var(--rule); }
.faq__q {
  font-family: var(--display);
  font-weight: 800;
  font-size: var(--t-lg);
  line-height: 1.1;
  padding: var(--s-4) 2rem var(--s-4) 0;
  cursor: pointer;
  list-style: none;
  position: relative;
}
.faq__q::-webkit-details-marker { display: none; }
.faq__q::after {
  content: "";
  position: absolute;
  right: 0.4rem;
  top: 50%;
  width: 0.55rem; height: 0.55rem;
  border-right: 2px solid var(--ink-3);
  border-bottom: 2px solid var(--ink-3);
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--dur-2) var(--ease);
}
.faq__item[open] .faq__q::after { transform: translateY(-30%) rotate(-135deg); }
.faq__a { padding-bottom: var(--s-5); color: var(--ink-2); max-width: 46rem; }
@media (prefers-reduced-motion: no-preference) {
  .faq__item[open] .faq__a { animation: reveal var(--dur-2) var(--ease-out); }
}
@keyframes reveal {
  from { opacity: 0; transform: translateY(-0.3rem); }
}

/* --------------------------------------------------------------------------
   17. Footer
   -------------------------------------------------------------------------- */

.foot {
  margin-top: var(--s-9);
  background: var(--ink);
  /* Fallback before the color-mix(): the background above is a plain colour and
     always lands, so if the colour is dropped the footer is dark text on dark. */
  color: var(--paper);
  color: color-mix(in srgb, var(--paper) 76%, transparent);
  padding-top: var(--s-7);
  /* The last thing on the page has to clear the home-indicator bar on a phone,
     or the copyright line sits under it. */
  padding-bottom: calc(var(--s-6) + env(safe-area-inset-bottom, 0px));
  font-size: var(--t-sm);
}
.foot a { color: var(--paper); text-decoration-color: rgb(255 255 255 / 0.3); }
.foot a:hover { color: #fff; }
.foot__grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  margin-bottom: var(--s-6);
}
.foot__title {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper);
  color: color-mix(in srgb, var(--paper) 55%, transparent);
  margin-bottom: var(--s-3);
}
.foot__list { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-2); }
@media (pointer: coarse) {
  /* Fifteen links stacked 8px apart is a minefield for a thumb. */
  .foot__list { gap: var(--s-1); }
  .foot__list a { display: inline-block; padding-block: 0.4rem; }
}
.foot__legal {
  border-top: 1px solid rgb(255 255 255 / 0.12);
  padding-top: var(--s-4);
  font-size: var(--t-xs);
  line-height: 1.7;
  color: var(--paper);
  color: color-mix(in srgb, var(--paper) 60%, transparent);
}

/* --------------------------------------------------------------------------
   18. Ads
   --------------------------------------------------------------------------
   There are no hand-placed ad units on this site. AdSense Auto ads decide
   where to inject, so all that is needed here is to keep whatever it injects
   from breaking the page: give it room, never let it overlap the sticky
   masthead, and keep it out of the print sheet. */

ins.adsbygoogle,
.google-auto-placed {
  margin-block: var(--s-5);
  max-width: 100%;
  /* Auto ads sometimes measure a container mid-layout and overflow it. */
  overflow: hidden;
}

/* Anchor and vignette formats are fixed to the viewport; keep them below the
   masthead's stacking context so they cannot cover the search field. */
ins.adsbygoogle[data-anchor-status],
ins.adsbygoogle[data-vignette-loaded] { z-index: 40; }

/* --------------------------------------------------------------------------
   19. Reveal on scroll
   -------------------------------------------------------------------------- */

/* Scoped to .js deliberately. Hiding content at opacity:0 and waiting for
   JavaScript to reveal it makes JS load-bearing for reading the page - if
   app.js 403s, throws, or is blocked, the element is simply gone with no clue
   why. app.js sets .js on <html> as its first act, so without it these rules
   never apply and the content is visible as authored. Never write a bare
   [data-reveal] { opacity: 0 } here. */
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(0.9rem);
    transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
  }
  .js [data-reveal].is-visible { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------------
   20. Printable sheet
   --------------------------------------------------------------------------
   The wall chart at /planting-calendar/<town>/print. On screen it is a preview
   of a piece of paper, so it is drawn as a sheet rather than as page furniture:
   a white rectangle with a border, holding a grid that survives a monochrome
   printer and a photocopy. The colour is a convenience on screen, never the
   thing carrying the meaning - that is the letter in the cell. */

.printbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.printbar__acts { display: flex; flex-wrap: wrap; gap: var(--s-2); }
/* Both buttons are pure script: one calls window.print, the other the share or
   clipboard API. Without JS they are furniture that does nothing when pressed,
   so they are not shown. The link beside them still works, and the browser's
   own print command still prints the sheet - the CSS does that part. */
.no-js [data-print-now], .no-js [data-share] { display: none; }

/* The first button-shaped control on the site. Everything else that acts is
   either a link or the risk dial, so this is deliberately quiet: a bordered
   pill that reads as an action without competing with the sheet beside it. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--paper);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  min-height: var(--touch);
  font-family: var(--mono);
  font-size: var(--t-sm);
  letter-spacing: 0.02em;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease),
              border-color var(--dur-1) var(--ease);
}
.btn:hover { background: var(--frost-deep); border-color: var(--frost-deep); color: #fff; }
.btn--quiet {
  background: none;
  color: var(--ink-2);
  border-color: var(--rule);
}
.btn--quiet:hover { background: var(--frost-pale); border-color: var(--frost); color: var(--ink); }

.sheet {
  background: #fff;
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
  padding: var(--s-5);
}
@media (min-width: 48rem) {
  .sheet { padding: var(--s-6); }
}

.sheet__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3) var(--s-5);
  border-bottom: 2px solid var(--ink);
  padding-bottom: var(--s-3);
  margin-bottom: var(--s-4);
}
.sheet__eyebrow {
  font-family: var(--mono);
  font-size: var(--t-xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0;
}
.sheet__title {
  font-family: var(--display);
  font-weight: 800;
  font-size: var(--t-xl);
  line-height: 1.05;
  margin: 0.15rem 0 0;
}
.sheet__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-4);
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.sheet__k {
  display: block;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* The grid. Month columns are equal and narrow; the crop column takes what is
   left. text-align on the cells is centre because a single letter left-aligned
   in a wide column does not read as belonging to that column. */
.sheet__grid { width: 100%; border-collapse: collapse; }
.sheet__grid th, .sheet__grid td { padding: 0.3rem 0.35rem; }
.sheet__mon {
  text-align: center;
  width: 5.5%;
  font-size: var(--t-xs);
}
.sheet__crop {
  font-weight: 500;
  font-size: var(--t-sm);
  text-align: left;
  white-space: nowrap;
  text-transform: none;
  letter-spacing: 0;
  font-family: inherit;
  color: var(--ink);
}
.sheet__cell {
  text-align: center;
  white-space: nowrap;
  border-left: 1px solid var(--rule-soft);
}
.sheet__grid tbody tr:nth-child(even) { background: color-mix(in srgb, var(--ink) 3%, transparent); }
.sheet__grid tbody tr:hover { background: var(--frost-pale); }

/* One letter per window type, in the same colours the growing-year band uses,
   so a reader who has seen the band recognises them. The letter is what
   survives a black-and-white print; the colour is a bonus. */
.sheet__mark {
  display: inline-block;
  min-width: 1.15rem;
  padding: 0.05rem 0.1rem;
  border-radius: 2px;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-weight: 600;
  line-height: 1.35;
  color: #fff;
  background: var(--bar, var(--frost-deep));
}
.sheet__mark[data-type="indoor"]     { --bar: var(--w-indoor); }
.sheet__mark[data-type="transplant"] { --bar: var(--w-transplant); }
.sheet__mark[data-type="direct"]     { --bar: var(--w-direct); }
.sheet__mark[data-type="autumn"]     { --bar: var(--w-autumn); }
.sheet__mark[data-type="harvest"] {
  --bar: none;
  color: var(--ink-2);
  border: 1px solid var(--rule);
}

.sheet__foot {
  margin-top: var(--s-5);
  border-top: 1px solid var(--rule);
  padding-top: var(--s-3);
  font-size: var(--t-xs);
  line-height: 1.6;
  color: var(--ink-3);
}
.sheet__foot p { margin: 0 0 0.35rem; }
.sheet__foot strong { color: var(--ink-2); }

/* --------------------------------------------------------------------------
   21. Reduced motion
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  [data-reveal] { opacity: 1; transform: none; }

  /* Killing the transition above does not stop the header hiding - it makes it
     hide instantly, which is the version a motion-sensitive visitor least
     wants. The header stays put instead. app.js checks the same query and
     never sets the attribute, so this is the backstop for a preference that
     changes after load. Selector matches the mobile rule exactly so file
     order decides it. */
  .js .masthead[data-hidden="true"] { transform: none; }
}

/* --------------------------------------------------------------------------
   22. Print
   --------------------------------------------------------------------------
   People print planting calendars and pin them to the shed wall. That is a
   real use of this site, so the print sheet is a designed artifact rather
   than an afterthought. */

@media print {
  :root { --paper: #fff; --card: #fff; }

  body { background: #fff; font-size: 10.5pt; line-height: 1.4; }

  .masthead, .foot, .search, .dial, .nav, .onpage,
  ins.adsbygoogle, .google-auto-placed,
  .band__today, [data-print="hide"] { display: none !important; }

  .wrap { width: 100%; max-width: none; padding-inline: 0; }
  .section { padding-block: 0.6cm; }

  a { color: inherit; text-decoration: none; }
  /* Print the destination of content links, since a printed link is useless
     otherwise - but not for navigation, which is hidden anyway. */
  .prose a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-family: var(--mono);
    font-size: 8pt;
    color: #555;
  }

  .band, .card, .tile, .step {
    border: 1px solid #999;
    box-shadow: none;
    break-inside: avoid;
  }
  .band__plot { min-width: 0; }
  .band__scroll { overflow: visible; }
  .table__scroll { overflow: visible; }
  /* All three of these exist to signal that there is more to the right. On
     paper the whole year is already there. The selector has to match the two
     classes the fade rule uses, or it loses on specificity and prints a
     gradient down the right-hand edge of the sheet. */
  .band__scroll.is-scrollable,
  .table__scroll.is-scrollable { -webkit-mask-image: none; mask-image: none; }
  .band__hint { display: none !important; }
  .band__bar { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .band__risk { display: none; }

  .faq__item { break-inside: avoid; }
  .faq__a { display: block !important; }

  h1 { font-size: 20pt; }
  h2 { font-size: 15pt; break-after: avoid; }

  /* The wall chart. On paper the sheet is the page, so it loses the border,
     the shadow and the padding that made it look like a sheet on screen. */
  .sheet {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
  }
  .sheet__title { font-size: 16pt; }
  .sheet__head { border-bottom-width: 1.5pt; }

  /* Twelve month columns plus crop names read best on the long edge. @page is
     document-scoped, so this cannot be a bare `size: landscape` - that would
     turn every printed page on the site sideways, including the city page.
     A named page applied to .sheet alone is the scoped version. Where it is
     not supported the sheet simply prints portrait, which it still fits: at
     1cm margins twelve 1.1cm columns and a 5cm crop column come to about 18cm
     against A4's 19cm of printable width. */
  .sheet { page: chart; }
  @page chart { size: landscape; margin: 1cm; }

  .sheet__grid { font-size: 7.5pt; }
  .sheet__grid th, .sheet__grid td { padding: 0.12rem 0.2rem; }
  .sheet__crop { font-size: 7.5pt; }
  /* Zebra striping and the coloured marks have to be asked for explicitly or
     most browsers drop them, and without the stripe a 40-row grid is very hard
     to read across. */
  .sheet__grid tbody tr:nth-child(even),
  .sheet__mark { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  .sheet__grid tbody tr:hover { background: none; }
  .sheet__mark { min-width: 0.9rem; font-size: 7pt; }
  /* Repeat the column heads on every sheet: a second page of unlabelled
     letters is unreadable. */
  .sheet__grid thead { display: table-header-group; }
  .sheet__grid tr { break-inside: avoid; }
  .sheet__foot { break-inside: avoid; }

  @page { margin: 1.4cm; }
}