/*
 * local: who we work with (paper field). The ledger.
 * Phones: heading, lede, cities, then the rows full width, number and sector on one
 * line with the client's site beside them.
 * Desktop: the heading block is CSS sticky in the left column (cols 1 to 5) while the
 * ledger (cols 7 to 12) runs past it.
 *
 * Volume Knob lives here, in its triggered form: the row crossing the reading line
 * carries .is-active and opens from 'wdth' 78 to 'wdth' 112 on a 0.5s transition (the
 * rules below have always shipped 112; docs/design-spec.md section 7 says 118).
 * The width axis is the page's one sanctioned exception to transform-and-opacity-only,
 * and it is carried by one element at a time, never eight. Everything else that moves
 * in this section is a transform or an opacity: the 2px ink rule scales in, the ink
 * number fades up over the soft one, the thumbnail lifts.
 * A row cannot change height when it opens, because the sector that carries the row's
 * height is an invisible copy of itself already set at the open width, and the copy
 * you read is laid over it and out of flow (see .local-row__name). The sector type is
 * also capped against the ledger's own width, so the widest word in the list always
 * fits its column when its row is open.
 *
 * No hidden states: with JS off every row renders at its resting width, and hover and
 * focus still open a row because neither needs a scroll position.
 */

.local {
  position: relative;
  padding-block: var(--section-y);
}

/* ---------- heading block ---------- */

.local__eyebrow { margin-bottom: 16px; }
.local__title { max-width: 12ch; }
.local__line { margin-top: 18px; max-width: 34ch; }
.local__cities {
  margin-top: clamp(22px, 3.4vw, 34px);
  color: var(--ink-soft);
  line-height: 1.5;
}

/* ---------- the ledger ---------- */

.local__ledger {
  margin: clamp(40px, 7vw, 64px) 0 0;
  padding: 0;
  list-style: none;
  /* A size container, so the sector type can be capped against the ledger's own width
     (see .local-row__name). The container is the list and not the row's text column on
     purpose: containment takes away an element's baseline, and .local-row__in lines the
     row number up on the sector's first baseline. */
  container-type: inline-size;
  /* What a row spends before the sector's column starts: the number column, the two
     gaps and the capture. These mirror grid-template-columns on .local-row__in and the
     width of .local-thumb, and are restated beside each of them further down. A row
     with no capture keeps its two gaps and loses only the capture. */
  --local-fixed: calc(2.2rem + 24px + 80px);
  --local-fixed-bare: calc(2.2rem + 24px);
}
.local-row { position: relative; }
.local-row__in {
  position: relative;
  display: grid;
  grid-template-columns: 2.2rem 1fr auto;
  column-gap: 12px;
  align-items: baseline;
  padding: clamp(16px, 2.2vw, 30px) 0;
  border-top: 1px solid var(--rule);
  color: inherit;
  text-decoration: none;
}
/* The active rule: a 2px ink line drawn over the hairline, so nothing reflows. */
.local-row__in::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -1px;
  height: 2px;
  background: var(--ink);
  opacity: 0;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: opacity .5s var(--ease-soft), transform .5s var(--ease-soft);
}

.local-row__num {
  position: relative;
  align-self: baseline;
  color: var(--ink-soft);
}
.local-row__num::after {
  content: attr(data-n);
  position: absolute;
  left: 0;
  top: 0;
  color: var(--ink);
  opacity: 0;
  transition: opacity .5s var(--ease-soft);
}

/* The sector is the row's only real content, so it wraps rather than being cut. The
   column is 141px beside the number and the capture on a 320px phone and 253px in the
   desktop ledger at 1024px, so a long sector cannot be promised a single line at
   either end. break-word sits on the body, not on the name, so it covers the
   'Client, Location' line under it as well: the Portfolio allows 40 characters of each,
   and an unbreakable run that long crosses the capture and widens the page without it
   (measured at a 320px viewport: take the break-word away and the document is 535px). */
.local-row__body { min-width: 0; overflow-wrap: break-word; }
/*
 * The sector is written twice (see includes/sections/local.php) and this is why.
 * Opening a row widens the sector by about half again: measured at 390px, 'Short stay
 * rentals' sets 168.3px at rest and 253.4px open in a 210.8px column. So a line that
 * holds at rest need not hold open. Clipping the overflow cut the sector mid-word on
 * phones; letting it wrap instead made the row's height depend on its state, and every
 * row under the open one moved for the length of the transition.
 * Neither is necessary. The sizer copy is the only one in flow and sits permanently at
 * the open axis, so the box is already exactly as tall as THIS sector needs when open;
 * the copy you read is laid over it, out of flow, so nothing it does can change a
 * height. Same mechanic as .flip__sizer in base.css, which HOS.fx.digitFlip uses to
 * stop a changing digit resizing its own box.
 * Every row pays for its own open state and no more: a sector that still holds one line
 * when it is open costs one line, and a longer one costs the lines it needs, on the row
 * it was typed on. There is no reserve to size, so nothing here has to be kept in step
 * with includes/schema/local.php's character limit, and nothing is spent on the seven
 * rows to protect the eighth.
 *
 * Wrapping only helps between words. One word wider than its column has nowhere to
 * break, and it was common: open, 'Conveyancing' is 8em wide, which is 205px at the
 * phone size and 295px at 1024, in columns of 181px (a 360px phone), 196px (375px) and
 * 253px (1024). The break-word above then split it as 'Conveya / ncing', on the most
 * common Android width and on every small laptop. So the type is capped: the second
 * font-size below is the column's width divided by the widest word in the ledger, both
 * in the same units, which is the largest size at which that word still fits when its
 * row is open. --local-word and --local-word-bare are that word's width in em, for rows
 * with a capture and rows without, written on the list by includes/sections/local.php
 * from the font's own measured advances. The cap is one size for the whole ledger, not
 * one per row: a ledger whose rows are set at different sizes stops reading as a list.
 * Measured with the shipped sectors: 17.2px at 320, 22.1px at 360, 23.9px at 375,
 * untouched from 390 up to the desktop breakpoint, then 30.8px at 1024 and 35.5px at
 * 1100, where the two-column layout halves the ledger, and untouched again from about
 * 1170. No word breaks mid-word at any of seventeen widths from 320 to 1920.
 * The 1rem floor is for a word no size can save (28 unbroken characters, which the
 * admin will accept): past it the break-word takes over, as it always did, and
 * hyphens: auto asks for a hyphen there. That is a hope, not a guarantee: the headless
 * Chrome these numbers were measured in hyphenates nothing, under any English tag.
 * An engine without container units ignores the second declaration and keeps the first.
 */
.local-row__name {
  /* One source for the open width: the sizer and the rules that open a row both read
     it, so the measured copy and the read copy cannot drift apart. The advances in
     includes/sections/local.php were measured at this value. */
  --local-wdth-open: 112;
  position: relative;
  display: block;
  font-family: var(--f-display);
  font-size: clamp(1.6rem, 1.05rem + 1.95vw, 2.75rem);
  font-size: min(
    clamp(1.6rem, 1.05rem + 1.95vw, 2.75rem),
    max(1rem, calc((100cqi - var(--local-fixed)) / var(--local-word, 1))),
    max(1rem, calc((100cqi - var(--local-fixed-bare)) / var(--local-word-bare, 1)))
  );
  line-height: 1;
  letter-spacing: -.03em;
  hyphens: auto;
  font-variation-settings: 'wdth' 78, 'wght' 800;
  transition: font-variation-settings .5s var(--ease-soft);
}
/* The sizer states its axis itself rather than inheriting it, so the rules that open a
   row cannot reach it and it never transitions. visibility: hidden is what takes it out
   of the accessibility tree and out of a selection while it still takes up its space.
   By default it sits at the RESTING axis, which reserves nothing: a row that can never
   open should not carry a spare line for a state it will never be in. That is a phone
   with JavaScript off or with reduced motion on, where there is no scroll trigger, no
   hover and nothing in the row to focus. */
.local-row__name-sizer {
  display: block;
  visibility: hidden;
  font-variation-settings: 'wdth' 78, 'wght' 800;
}
/* A row can open three ways, and each has a signal that is already true at first
   paint, so the choice of axis never changes after the page has drawn:
   by scroll, when local.js will run (index.php sets motion-ready in the head, and
   never under reduced motion); by keyboard, when the row is a link and can take
   focus; and by hover, on a real pointer. In any of them the sizer holds the open
   axis, and the row is as tall as its own open state needs. */
html.motion-ready .local-row__name-sizer,
a.local-row__in .local-row__name-sizer {
  font-variation-settings: 'wdth' var(--local-wdth-open), 'wght' 900;
}
@media (hover: hover) and (pointer: fine) {
  .local-row__name-sizer { font-variation-settings: 'wdth' var(--local-wdth-open), 'wght' 900; }
}
/* Out of flow, over the sizer, and the same width, so it breaks in the same places. */
.local-row__name-text {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
.local-row__meta {
  display: block;
  margin-top: 10px;
  color: var(--ink-soft);
  line-height: 1.4;
  text-transform: none;
  letter-spacing: .02em;
  font-stretch: 88%;
}

.local-row__thumb {
  align-self: center;
  transition: transform .5s var(--ease-soft);
}
/*
 * The capture at thumbnail size. Only the chrome is scaled down to suit 80 to 116px:
 * the address pill cannot be read at this size, so it steps aside and the window keeps
 * the three dots that say "this is a website".
 */
.local-thumb {
  width: 80px;
  border-radius: 5px;
}
.local-thumb .frame-browser__bar { height: 13px; padding: 0 5px; gap: 5px; }
.local-thumb .frame-browser__dots { gap: 3px; }
.local-thumb .frame-browser__dots i { width: 4px; height: 4px; }
.local-thumb .frame-browser__url,
.local-thumb .frame-browser__spacer { display: none; }

/* ---------- active row (Volume Knob) ---------- */

.local-row.is-active .local-row__in::before,
.local-row:focus-within .local-row__in::before {
  opacity: 1;
  transform: scaleX(1);
}
.local-row.is-active .local-row__num::after,
.local-row:focus-within .local-row__num::after { opacity: 1; }
.local-row.is-active .local-row__name,
.local-row:focus-within .local-row__name { font-variation-settings: 'wdth' var(--local-wdth-open), 'wght' 900; }
.local-row.is-active .local-row__thumb,
.local-row:focus-within .local-row__thumb { transform: translateY(-6px); }

/* Focus answers straight away; the scrolled state takes its time. */
.local-row:focus-within .local-row__in::before,
.local-row:focus-within .local-row__num::after,
.local-row:focus-within .local-row__name,
.local-row:focus-within .local-row__thumb { transition-duration: .28s; }

/*
 * Hover, on a real pointer only, so a tap on a phone never leaves a row stuck open.
 * While the cursor is anywhere on the ledger the cursor is the reading line, so the
 * scrolled row stands down: exactly one sector is open at a time, which is the whole
 * point of keeping this to one element.
 */
@media (hover: hover) and (pointer: fine) {
  .local-row:hover .local-row__in::before { opacity: 1; transform: scaleX(1); }
  .local-row:hover .local-row__num::after { opacity: 1; }
  .local-row:hover .local-row__name { font-variation-settings: 'wdth' var(--local-wdth-open), 'wght' 900; }
  .local-row:hover .local-row__thumb { transform: translateY(-6px); }
  .local-row:hover .local-row__in::before,
  .local-row:hover .local-row__num::after,
  .local-row:hover .local-row__name,
  .local-row:hover .local-row__thumb { transition-duration: .28s; }

  .local__ledger:hover .local-row.is-active:not(:hover) .local-row__in::before { opacity: 0; transform: scaleX(0); }
  .local__ledger:hover .local-row.is-active:not(:hover) .local-row__num::after { opacity: 0; }
  .local__ledger:hover .local-row.is-active:not(:hover) .local-row__name { font-variation-settings: 'wdth' 78, 'wght' 800; }
  .local__ledger:hover .local-row.is-active:not(:hover) .local-row__thumb { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .local-row__in::before,
  .local-row__num::after,
  .local-row__name,
  .local-row__thumb { transition-duration: .15s; }
  .local-row.is-active .local-row__thumb,
  .local-row:hover .local-row__thumb,
  .local-row:focus-within .local-row__thumb { transform: none; }
}

/* ---------- tablet ---------- */

@media (min-width: 768px) {
  .local__ledger { --local-fixed: calc(3rem + 40px + 96px); --local-fixed-bare: calc(3rem + 40px); }
  .local-row__in { grid-template-columns: 3rem 1fr auto; column-gap: 20px; }
  .local-thumb { width: 96px; }
}

/* ---------- desktop ---------- */

@media (min-width: 1024px) {
  .local__grid {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--gutter);
    align-items: start;
  }
  .local__block {
    grid-column: 1 / span 5;
    position: sticky;
    top: 30svh;
  }
  /* The heading sets to the sticky column's width. It is capped against the column,
     not just the viewport: the longest word has to fit on one line, because Ink Set
     splits the heading into characters and a word that does not fit would break. */
  .local__title { max-width: none; font-size: min(var(--fs-h2), 6vw, 5.75rem); }
  .local__line { max-width: 30ch; }

  .local__ledger {
    grid-column: 7 / span 6;
    margin-top: 0;
    --local-fixed: calc(3.2rem + 40px + 116px);
    --local-fixed-bare: calc(3.2rem + 40px);
  }
  .local-row__in { grid-template-columns: 3.2rem 1fr auto; column-gap: 20px; padding-block: clamp(22px, 2.6vw, 36px); }
  .local-row__meta { margin-top: 12px; }
  .local-thumb { width: 116px; }
}
