/*
  Palette is deliberately warm: every token pulls the blue channel down
  and the red up relative to a neutral grey, so the whole page sits in
  warm brown rather than taupe. Keep that bias when adding colours --
  a neutral grey dropped in here will read cold and stand out.
*/
:root {
  --dark: #322B22;        /* header, footer, closing band */
  --dark-soft: #3E3529;
  --beige: #EDE3D2;       /* practice areas, page ground */
  --sand: #F4EDDF;        /* cards, about band */
  --cream: #FAF4E7;       /* light text on dark */
  --ink: #332B20;         /* headings */
  --body: #5C5241;        /* body copy */
  --line: #CEC2A9;
  --rule: #91846B;
  --wrap: 1180px;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--beige);
  color: var(--body);
  font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: 17px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 700;
  color: var(--ink);
  line-height: 1.2;
  margin: 0;
}

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

a { color: inherit; }

img { max-width: 100%; height: auto; }

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 2rem;
}

.skip-link {
  position: absolute;
  left: -9999px;
  background: var(--dark);
  color: var(--cream);
  padding: 0.75rem 1.25rem;
  z-index: 200;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* Visually hidden but present for screen readers -- used where a heading
   or label is needed for document structure but not for sighted layout. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- Header ---------- */

.site-header {
  background: var(--dark);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-top: 0.7rem;
  padding-bottom: 0.7rem;
  flex-wrap: wrap;
}

.brand { text-decoration: none; }

.brand-name {
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 600;
  font-size: 1.32rem;
  line-height: 1.12;
  color: var(--cream);
  display: block;
}

/*
  The wordmark is deliberately still on this page -- the hero sequence
  carries the motion, and two entrances competing at the top of the page
  read as busy. The keyframes are kept for reuse on other pages; apply
  with:  animation: brand-line-in 0.62s cubic-bezier(0.22,0.7,0.3,1) both;
  and a 0.11s delay on the second line.
*/
@keyframes brand-line-in {
  from { opacity: 0; transform: translateY(-7px); }
  to   { opacity: 1; transform: translateY(0); }
}

.brand-line { display: block; }

.nav {
  display: flex;
  align-items: center;
  gap: 2.4rem;
  font-size: 0.94rem;
  flex-wrap: wrap;
}

.nav a {
  color: var(--cream);
  text-decoration: none;
  padding-bottom: 2px;
}
.nav a:hover, .nav a:focus { text-decoration: underline; }

.nav .is-current {
  color: #E7C98F;
  border-bottom: 1px solid #E7C98F;
}

/*
  Same boxed CTA as the contact page header, so the two pages match.
  The extra "a" in the selector is doing real work: .nav a is class+type
  and beats a lone class, so its 2px padding-bottom was winning over the
  shorthand here and the text rode low in the box. Found that one the
  hard way. :]
*/
.nav a.nav-call {
  margin-left: 1.5rem;
  border: 1px solid #E7C98F; color: #E7C98F;
  padding: 0.6rem 1.2rem; letter-spacing: 0.04em;
  transition: background 0.22s cubic-bezier(0.22, 0.7, 0.3, 1), color 0.22s cubic-bezier(0.22, 0.7, 0.3, 1);
}
.nav a.nav-call:hover, .nav a.nav-call:focus {
  background: #E7C98F; color: var(--dark); text-decoration: none;
}

/*
  Keyboard focus ring, copied over from the contact page so tabbing
  looks the same everywhere: deep gold on the light sections, bright
  gold where the ground is dark.
*/
:where(a, button):focus-visible { outline: 2px solid #775E31; outline-offset: 3px; }
.site-header :where(a):focus-visible,
.hero :where(a):focus-visible,
.cred-bar :where(a):focus-visible,
.closing :where(a):focus-visible,
.site-footer :where(a):focus-visible { outline-color: #E7C98F; }

/* ---------- Hero ---------- */

/*
  min-height is a floor, not the real height -- the copy is always
  taller than this, so the hero is sized by its content and the two
  margins below do all the spacing. That's on purpose: it used to be
  560px with the copy nearly filling it, so the space under the button
  was just whatever centring slack happened to be left over (9px), and
  it changed on its own every time the headline's font-size shifted
  with the viewport. Now the gap above the button and the gap below it
  are both written down. Keep them equal. :]
*/
.hero {
  position: relative;
  min-height: 480px;
  display: flex;
  align-items: center;
  overflow: hidden;
}

/*
  Placeholder ground. Drop assets/hero-office.jpg into the repo and this
  picks it up -- the layered gradient stays underneath as a fallback and
  as the scrim that keeps the headline readable over the photo.
*/
/* Photo only. The scrim used to be a second background layer here, but
   it needs its own element to animate independently. */
.hero-media {
  position: absolute;
  inset: 0;
  background-color: #6E5A3F;
  background-image: url("assets/hero-portland.jpg");
  background-size: cover;
  background-position: center center;
}

/*
  The scrim sweeps in from the left over the bare photo. It is the same
  gradient the photo carried before, just on its own layer so it can
  arrive a beat later -- the page opens on Portland, then the darkening
  and the words come in together.

  The 8% offset is on a full-bleed layer, so it reads as a wash crossing
  the image rather than a box sliding.
*/
/*
  The -18% right inset runs the scrim past the hero's right edge. Sliding
  it left by 8% used to drag its own right edge into view as a visible
  seam; with the layer overhanging, that edge stays outside the frame
  through the whole slide and .hero's overflow clips it.
*/
.hero-scrim {
  position: absolute;
  inset: 0 -18% 0 0;
  background: linear-gradient(100deg, rgba(34,25,14,0.82) 0%, rgba(34,25,14,0.58) 30%, rgba(34,25,14,0.20) 55%, rgba(34,25,14,0.04) 86%, rgba(34,25,14,0) 100%);
  animation: hero-scrim-in 1.15s cubic-bezier(0.22, 0.7, 0.3, 1) both;
}

@keyframes hero-scrim-in {
  from { opacity: 0; transform: translateX(-8%); }
  to   { opacity: 1; transform: translateX(0); }
}

/*
  Headline, lede and button follow the scrim in, staggered so they read
  as one movement rather than three. Each is a modest 26px -- the text is
  large, and a long slide on a headline this size looks like a slideshow.
*/
@keyframes hero-copy-in {
  from { opacity: 0; transform: translateX(-26px); }
  to   { opacity: 1; transform: translateX(0); }
}

.hero h1,
.hero .lede,
.hero .cta-row {
  animation: hero-copy-in 0.85s cubic-bezier(0.22, 0.7, 0.3, 1) both;
}

.hero h1      { animation-delay: 0.30s; }
.hero .lede   { animation-delay: 0.44s; }
.hero .cta-row { animation-delay: 0.58s; }

/*
  width:100% is load-bearing. .hero is a flex container, so without it
  this .wrap shrinks to its content and the auto margins centre it,
  pushing the headline off the page's left edge. Filling the width lets
  max-width + auto margins behave as they do in the header, so the
  headline lines up with the logo.
*/
.hero-inner { position: relative; z-index: 1; width: 100%; }

.hero h1 {
  font-size: clamp(2.4rem, 5.2vw, 4.1rem);
  color: var(--cream);
  max-width: 13ch;
  margin: 0 0 1.6rem;
  text-wrap: balance;
}

/*
  text-wrap:balance evens the line lengths so the right edge stops
  stair-stepping. It only applies to short blocks (browsers cap it at a
  handful of lines), which is why it is here and not on body copy.
*/
.hero .lede {
  color: #FBF6EC;
  font-size: 1.05rem;
  font-weight: 400;
  max-width: 40ch;
  margin: 0 0 2.2rem;
  text-wrap: balance;
  text-shadow: 0 1px 12px rgba(28,20,10,0.5);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1.6rem;
  align-items: center;
  /* matches .hero .lede's margin-bottom, which is the gap above the
     button -- that's what centres it in the space under the text */
  margin-bottom: 2.2rem;
}

/*
  Padding is asymmetric on purpose. Centring the line box is not the
  same as centring the lettering: "Schedule a Consultation" has no
  descenders, so the descender depth the line box reserves sits under
  the label as dead space and the text reads low.

  The correction is small -- about 1px. Worth writing down how it is
  derived, because eyeballing it and guessing at the size of the
  reserved space is what produced a 3px overcorrection here before.
  With Lato at 0.92rem in a 25.02px line box:

    half-leading  = (line-height - (fontAscent + fontDescent)) / 2
                  = (25.02 - (15 + 3)) / 2            = 3.51px
    baseline      = padding-top + half-leading + fontAscent
    ink top gap   = baseline - inkAscent(11)
    ink bottom gap= height - baseline            (inkDescent is 0)

  Setting those two gaps equal, with the total padding held at 27.2px
  so the button keeps its height, gives 13.10px top / 14.10px bottom.
  Note half-leading subtracts the font's ascent+descent, not the
  font-size -- using font-size there is what threw the earlier figure.
*/
.btn {
  display: inline-block;
  background: #281F12;
  color: var(--cream);
  text-decoration: none;
  padding: 0.819rem 1.9rem 0.881rem;
  border-radius: 2px;
  font-size: 0.92rem;
  letter-spacing: 0.02em;
  position: relative;
  transition:
    background 0.25s ease,
    color 0.25s ease,
    transform 0.28s cubic-bezier(0.22, 0.7, 0.3, 1),
    box-shadow 0.28s ease;
}

/* Inverts the other way to everything else -- this one starts dark, so
   it lights up rather than down. The lift is smaller than the cards' at
   3px: it sits over the hero photo, where a large movement competes with
   the copy arriving beside it. */
.btn:hover, .btn:focus {
  background: var(--cream);
  color: var(--ink);
  transform: translateY(-3px) scale(1.03);
  box-shadow: 0 14px 26px rgba(18, 12, 4, 0.42);
}

.hero .contact-link {
  color: #EFE3C8;
  text-decoration: none;
  font-size: 0.95rem;
  text-shadow: 0 1px 12px rgba(28,20,10,0.5);
}
.hero .contact-link:hover { text-decoration: underline; }

/*
  The "53+ Years" stat used to sit beside the "Practice Areas" heading,
  tied to it by a rule that ran from the text to the stat's left border
  (see .rule-heading below). Moved here at the client's request, with
  that heading now hidden -- so the card gets its own self-contained
  background rather than the plain border-left it used against beige,
  since it now sits over a photo of unpredictable brightness. The scrim
  fades toward transparent on the right (see .hero-scrim above), which
  is exactly where this card sits, so it cannot lean on the scrim for
  contrast the way the headline does.

  Same markup and class as the original stat block (.stat-label,
  .stat-figure, .countup, .stat-note) -- only this wrapper is new, so
  the count-up script and its .closest('.stat') hover replay still
  find it without any script changes.
*/
.hero .stat {
  position: absolute;
  top: 2.25rem;
  right: 2rem;
  z-index: 2;
  min-width: 0;
  width: 200px;
  background: rgba(24, 18, 10, 0.58);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(250, 244, 231, 0.16);
  border-left: 1px solid rgba(250, 244, 231, 0.16);
  border-radius: 2px;
  padding: 1rem 1.3rem;
  box-shadow: 0 14px 30px rgba(10, 7, 3, 0.35);
}
/*
  0.85, not the ~0.6 a plain caption color would use elsewhere on the
  site. The card's own background is only 58% opaque, so on the
  brightest crop of the photo a lighter label/note reads under 4.5:1 --
  measured by compositing real sampled pixels from hero-portland.jpg
  under the card, not assumed. 0.85 holds 6:1 at the worst sampled spot.
*/
.hero .stat-label { color: rgba(250, 244, 231, 0.85); }
.hero .stat-figure { color: var(--cream); }
.hero .stat-note {
  color: rgba(250, 244, 231, 0.85);
  border-top-color: rgba(250, 244, 231, 0.3);
}

/* Below 820px the hero drops to a shorter, padded block (see the hero
   rules in that breakpoint further down) and there is no reliable clear
   corner left for an overlay -- so the card rejoins normal flow under
   the button instead of floating. */
@media (max-width: 820px) {
  .hero .stat {
    position: static;
    width: auto;
    margin-top: 2rem;
  }
}


/* ---------- Sections ---------- */

.section { padding: 5.5rem 0; }

/* Rule on the hero seam, matching the section dividers elsewhere. The
   top padding is tighter than the standard 5.5rem -- the band opened
   with too much empty ground under the hero. */
/*
  Symmetric padding. The 3rem top was set when a heading sat above the
  cards; with that heading now hidden the cards start straight after
  the border, and the inherited 5.5rem bottom left noticeably more air
  under the grid than above it.
*/
.section-practice {
  background: var(--beige);
  border-top: 2px solid var(--rule);
  padding-top: 3rem;
  padding-bottom: 3rem;
  position: relative;
  /* Clips the decoration, which is deliberately drawn past the section's
     edges so the arcs read as passing through rather than starting and
     stopping inside it. */
  overflow: hidden;
}

/* ---------- Decorative line work ---------- */

/*
  Sits behind everything and takes no pointer events, so it cannot
  intercept a hover on the cards above it. aria-hidden in the markup
  keeps it out of the accessibility tree entirely.
*/
.deco-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}

/*
  Hidden on narrow screens. "slice" scales the viewBox to cover the box,
  so a section far taller than it is wide blows the artwork up until only
  a fragment shows -- the arcs stop reading as arcs and become stray
  diagonals. It is also where the copy runs full width, so lines that sit
  clear of the text on desktop would cross straight under it here.
  Decoration is the right thing to drop rather than fight.
*/
@media (max-width: 820px) {
  .deco-lines { display: none; }
}

/* Content rides above the decoration. Without this the cards would sit
   in the same stacking layer as the arcs and the paint order would come
   down to document order. */
.section-practice > .wrap,
.section-about > .wrap { position: relative; z-index: 1; }

/* The heading that used to live in .section-head is now .sr-only (see
   index.html) -- without this, its collapsed box still carried the
   usual 2.6rem margin-bottom, leaving a gap the cards had nothing to
   fill it with. */
.section-practice .section-head { margin-bottom: 0; }

/* Tighter than the other sections: the portrait fills the row, so the
   usual 5.5rem left a band of empty ground above and below it.

   No border-top: the sand ground is enough of a change from the beige
   above it, and a rule there was doing work nothing had asked for.

   The bottom padding is smaller than the top: the credentials record now
   ends this section, and its own rule already closes it off, so a full
   3.25rem underneath left an empty strip. */
.section-about {
  background: var(--sand);
  padding: 3.25rem 0 1.6rem;
  position: relative;
  overflow: hidden;
}

.section h2 { font-size: clamp(1.9rem, 3.6vw, 2.8rem); }

.centered { text-align: center; }

/*
  Heading with a rule that runs out of the text and meets the stat
  block's vertical divider. The grid gap is deliberately 0 so the rule
  terminates exactly on that border rather than stopping short of it;
  breathing room after the text comes from .rule-heading's own gap.
*/
/*
  align-items:center matters as much as the rule's own position. The stat
  block is taller than the heading, so with start alignment the rule met
  the vertical divider about a quarter of the way down it -- a T-junction
  that reads as off-centre no matter how well the rule sits on the text.
  Centring the two columns puts the junction at the divider's midpoint.
*/
.section-head {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0;
  align-items: center;
  margin-bottom: 2.6rem;
}

/*
  align-items:baseline puts the rule's bottom edge on the text baseline
  (a flex item with no content has its baseline synthesised from its
  border box). From there one translate of half the cap height lands it
  on the optical centre of the capitals.

  Anchoring to the baseline rather than the line box is what makes this
  hold: centring on the box drifts with line-height, because the box
  reserves descender space that "Practice Areas" never uses. Playfair's
  cap height measures 0.781em, so half is 0.39em. The +1px gives back
  half the rule's own 2px height, which would otherwise sit it high.
*/
.rule-heading {
  display: flex;
  align-items: baseline;
  gap: 1.5rem;
  white-space: nowrap;
  padding-right: 0;
}
.rule-heading::after {
  content: "";
  height: 2px;
  background: var(--rule);
  flex: 1;
  transform: translateY(calc(-0.39em + 1px));
}

.stat { border-left: 2px solid var(--rule); padding-left: 1.6rem; min-width: 270px; }

.stat-label {
  font-size: 0.76rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #6D6247;
  margin: 0 0 0.3rem;
}

/*
  No top margin, at the client's request. A 25px one centred "53+ Years"
  in the stat block so the heading rule ran straight at the figure rather
  than into the gap beneath it -- but it opened up the space under "Time
  in Service" too much. Tight spacing wins; the rule now meets the figure
  about 8px low again.
*/
/*
  lining-nums is doing real work here, not tidying. Playfair Display
  ships old-style figures by default: measured at this size, "5" tops
  out 5px below "Y" and drops another 5px *below the baseline*, so
  "53+" visibly sagged against "Years". Lining figures are cap-height
  with no descender, which lines the numerals up with the word.
  tabular-nums stays for the reason below.
*/
.stat-figure {
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 700;
  font-size: 2.1rem;
  color: var(--ink);
  line-height: 1.1;
  margin: 0 0 0.4rem;
  font-variant-numeric: lining-nums tabular-nums;
  font-feature-settings: "lnum" 1, "tnum" 1;
}

/*
  Fixed width while counting. Proportional digits are different widths,
  so without this "+ Years" would jitter sideways on every tick as the
  number climbs 0 -> 9 -> 10 -> 53.
*/
.countup {
  display: inline-block;
  min-width: 2ch;
  text-align: right;
}

/*
  The "+" is only true once the count reaches 53, so it is held back
  until then -- "12+ Years" mid-count would be a wrong claim, briefly.
  Visible by default and hidden only while counting, so with JavaScript
  off the figure still reads "53+ Years".

  Hidden with opacity, not display, so the line does not reflow when it
  appears.
*/
/*
  Lora italic rather than Playfair. Playfair's plus is a stiff
  geometric cross that reads as a maths operator beside the numerals;
  Lora's is lighter and slightly angled, and Lora italic is already
  the site's accent face (the pull quote, the lifted word on the
  contact hero). Sized down and nudged up so it reads as a modifier
  on the figure rather than a third character of equal weight.
*/
.countup-plus {
  font-family: 'Lora', Georgia, serif;
  font-style: italic;
  font-weight: 500;
  font-size: 0.78em;
  vertical-align: 0.12em;
  margin-left: 0.02em;
  transition: opacity 0.35s ease;
}

.stat-figure.is-counting .countup-plus { opacity: 0; }

.stat-note {
  font-size: 0.86rem;
  color: #6A5D46;
  border-top: 2px solid var(--line);
  padding-top: 0.5rem;
  margin: 0;
}

.grid { display: grid; gap: 2.6rem 3rem; }

.practice-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.75rem; }

/*
  Cards need equal heights or the boxes stagger when descriptions run to
  different line counts. align-items:stretch on the grid plus height:100%
  here keeps every box in a row the same depth.
*/
.area {
  background: var(--sand);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 1.6rem 1.7rem;
  height: 100%;
  position: relative;
  transition:
    background 0.25s ease,
    border-color 0.25s ease,
    color 0.25s ease,
    transform 0.28s cubic-bezier(0.22, 0.7, 0.3, 1),
    box-shadow 0.28s ease;
}

/*
  Invert on hover. focus-within is included so the same feedback appears
  for keyboard users, who never fire hover. The heading needs its own
  rule because .area h3 sets an explicit colour that would otherwise win
  over the inherited one.
*/
/*
  z-index lifts the hovered card over its neighbours, or the scaled edges
  would slide underneath the cards beside it. The scale is kept to 1.035
  -- the grid gap is 1.75rem, and more than this closes it entirely.
*/
.area:hover, .area:focus-within {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--cream);
  transform: translateY(-8px) scale(1.035);
  box-shadow: 0 18px 34px rgba(32, 24, 14, 0.28);
  z-index: 2;
}
.area:hover h3, .area:focus-within h3 { color: var(--cream); }

/*
  One flash of the inverted state as the grid arrives, to advertise that
  the cards respond to hover at all -- otherwise nobody discovers it.

  The heading needs its own keyframes because .area h3 sets an explicit
  colour that an inherited one cannot override. Both read the same
  --flash-delay, which is set per card below and inherits to the heading,
  so the two stay in step without repeating the delays twice.
*/
@keyframes area-flash {
  0%, 100% { background-color: var(--sand); border-color: var(--line); color: var(--body); }
  35%, 55% { background-color: var(--ink);  border-color: var(--ink);  color: var(--cream); }
}

@keyframes area-flash-heading {
  0%, 100% { color: var(--ink); }
  35%, 55% { color: var(--cream); }
}

.area.is-flash    { animation: area-flash 1.25s ease-in-out var(--flash-delay, 0s); }
.area.is-flash h3 { animation: area-flash-heading 1.25s ease-in-out var(--flash-delay, 0s); }

.practice-grid .area:nth-child(1) { --flash-delay: 0s; }
.practice-grid .area:nth-child(2) { --flash-delay: 0.07s; }
.practice-grid .area:nth-child(3) { --flash-delay: 0.14s; }
.practice-grid .area:nth-child(4) { --flash-delay: 0.21s; }
.practice-grid .area:nth-child(5) { --flash-delay: 0.28s; }
.practice-grid .area:nth-child(6) { --flash-delay: 0.35s; }

/*
  Everything animated appears in place, already at rest. This matters
  more than usual here: these animations use fill mode "both", which
  holds the from-state (opacity 0) until they run -- so anything left out
  of this list would stay invisible rather than merely still. The
  count-up opts itself out in the script.
*/
@media (prefers-reduced-motion: reduce) {
  .area, .pull-quote, .quote, .btn { transition: none; }
  /* The colour inversion still happens -- that is a state change, not
     motion. Only the lift and scale are dropped. */
  .area:hover, .area:focus-within,
  .pull-quote:hover,
  .btn:hover, .btn:focus { transform: none; box-shadow: none; }
  .brand-line,
  .hero-scrim,
  .hero h1,
  .hero .lede,
  .hero .cta-row { animation: none; }
  /* Stars sit there fully formed. The script skips arming them, but this
     covers the case where the setting changes after load. */
  .stars.is-armed .star { opacity: 1; transform: none; }
}

.area h3 {
  font-size: 1.22rem;
  font-weight: 700;
  margin-bottom: 0.7rem;
  transition: color 0.25s ease;
}
.area p { font-size: 0.95rem; margin: 0; }

/* ---------- About ---------- */

/*
  align-items:stretch (the default) is what fills the dead space beside
  the text: the frame grows to the row's full height, and the image
  covers it. Keeping the column at a fixed width means the text block
  does not move -- only the portrait grows downward.
*/
.about {
  display: grid;
  grid-template-columns: 330px 1fr;
  gap: 3.5rem;
  align-items: stretch;
}

.portrait-frame {
  border: 1px solid var(--ink);
  padding: 6px;
  display: flex;
}

/*
  object-fit:cover crops rather than stretches. The source is only
  292x388, so the taller frame scales it up and trims the sides; the
  subject is centred, so the crop takes background, not him.
*/
.portrait {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
}

.about h2 { margin-bottom: 1.6rem; }

/* Lifts and inverts like the practice cards. The travel is smaller --
   4px against their 8px, 1.015 against 1.035 -- because this block is far
   wider, and the same movement on something this size reads as heavy. */
.pull-quote {
  background: var(--cream);
  border: 1px solid var(--line);
  margin: 0 0 1.8rem;
  padding: 1.5rem 1.7rem;
  position: relative;
  transition:
    background 0.25s ease,
    border-color 0.25s ease,
    transform 0.28s cubic-bezier(0.22, 0.7, 0.3, 1),
    box-shadow 0.28s ease;
}

.pull-quote:hover {
  background: var(--ink);
  border-color: var(--ink);
  transform: translateY(-4px) scale(1.015);
  box-shadow: 0 14px 28px rgba(32, 24, 14, 0.24);
  z-index: 2;
}
.pull-quote:hover p { color: var(--cream); }

/* Its own keyframes rather than the cards': this block sits on --cream,
   not --sand, so sharing theirs would flash it back to the wrong ground. */
@keyframes pullquote-flash {
  0%, 100% { background-color: var(--cream); border-color: var(--line); }
  35%, 55% { background-color: var(--ink);   border-color: var(--ink);  }
}

@keyframes pullquote-flash-text {
  0%, 100% { color: var(--ink); }
  35%, 55% { color: var(--cream); }
}

.pull-quote.is-flash   { animation: pullquote-flash 1.25s ease-in-out; }
.pull-quote.is-flash p { animation: pullquote-flash-text 1.25s ease-in-out; }

/*
  Lora rather than Playfair here. Playfair's italic is a true calligraphic
  cut -- steeply sloped with swashed letterforms -- which turns showy at
  paragraph length. Lora's italic is far more restrained while staying a
  serif, so the quote still reads as Neil speaking rather than as display
  type. 600 keeps the weight the client asked for without going heavy.
*/
.pull-quote p {
  font-family: 'Lora', Georgia, serif;
  font-style: italic;
  font-weight: 600;
  font-size: 0.98rem;
  line-height: 1.7;
  color: var(--ink);
  margin: 0;
}

.about-body p { font-size: 0.95rem; }

/* ---------- Credentials bar (trial) ---------- */

/*
  Thinner than the concept page's version: the figure drops from ~2.9rem
  to 1.75rem and the padding roughly halves, which takes the band from
  about 200px to about 120px. It sits between two light sections, so it
  reads as a rule across the page rather than as a section of its own --
  which is why it is this height and not taller.
*/
.cred-bar {
  background: #262016;
  padding: 1.6rem 0 1.75rem;
}

.cred-bar-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 1.4rem 2.4rem;
}

.cred-bar-item {
  border-left: 1px solid rgba(231, 201, 143, 0.28);
  padding-left: 1.15rem;
}

.cred-bar-fig {
  display: block;
  font-family: 'Playfair Display', Georgia, serif;
  font-weight: 600;
  font-size: 1.75rem;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--cream);
  font-variant-numeric: tabular-nums;
  margin-bottom: 0.4rem;
}

.cred-bar-lab {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #E7C98F;
  margin: 0 0 0.2rem;
}

.cred-bar-note {
  font-size: 0.78rem;
  line-height: 1.45;
  color: #A99C85;
  margin: 0;
}

/*
  Counting figures reserve their final width. Without this the whole
  band jitters sideways for the length of the count, because "7" and
  "1972" are different widths and the label beneath is centred on
  nothing stable. min-width holds the space the finished number will
  take; tabular figures keep each tick the same width as the last.
*/
.cbc {
  display: inline-block;
  min-width: 3.4ch;
  font-variant-numeric: tabular-nums;
}
/* "1" never grows, so it does not need the reservation and looks
   stranded beside its label with it. */
.cred-bar-item:last-child .cbc { min-width: 0; }

/*
  Labels and notes are hidden only once the script arms them, so with
  JavaScript off, or under reduced motion where the script returns
  early, the band is complete and still.
*/
.cred-bar.is-armed .cred-bar-lab,
.cred-bar.is-armed .cred-bar-note { opacity: 0; }

.cred-bar.is-done .cred-bar-lab,
.cred-bar.is-done .cred-bar-note {
  opacity: 1;
  transition: opacity 0.55s ease;
}
/* The note trails its label rather than arriving with it, so the two
   read as one settling movement instead of a single block switching on. */
.cred-bar.is-done .cred-bar-note { transition-delay: 0.13s; }

@media (prefers-reduced-motion: reduce) {
  .cred-bar.is-armed .cred-bar-lab,
  .cred-bar.is-armed .cred-bar-note { opacity: 1; }
}

/* ---------- Credentials block, full width (trial) ---------- */

/* Compressed from the first pass: the record is reference material read
   in glances, not prose read line by line, so it can sit tighter than
   body copy without becoming harder to use. */
.creds-full {
  margin-top: 1.7rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}

.creds-full-head {
  font-family: 'Lato', sans-serif;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #6A5D46;
  margin: 0 0 0.95rem;
}

/* Two columns at width. One would push Courts and Rating well past a
   comfortable line length now that the block spans the whole page.

   min(340px, 100%) rather than a bare 340px: a plain minimum larger than
   the container becomes a floor the track cannot go under, so on a phone
   the rows sat 9px wider than the list holding them. Capping the minimum
   at the container's own width lets it collapse cleanly instead. */
.creds-full-list {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 1rem 3.5rem;
}

/* Term left, description right, so the four labels form a column the eye
   can run down -- the point of setting this as a record rather than
   leaving it as the paragraph it was. */
.cred-row {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: 0.9rem;
  align-items: baseline;
}

.creds-full dt {
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #775E31;
}

.creds-full dd {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--body);
}

@media (max-width: 620px) {
  .cred-row { grid-template-columns: 1fr; gap: 0.15rem; }
}

/* ---------- Testimonials ---------- */

/*
  Rules top and bottom, matching the section-heading rule's 2px/--rule
  so the page has one divider style rather than several.
*/
.section-testimonials {
  position: relative;
  overflow: hidden;
  background: var(--beige);
  border-top: 2px solid var(--rule);
  border-bottom: 2px solid var(--rule);
}

/*
  The backdrop lives in its own layer so it can be moved independently of
  the content. Overhang top and bottom because this sits mid-page and
  drifts both ways as it crosses the viewport.
*/
/*
  background-position is pinned to the top because the Portland Oregon
  stag sign sits in the upper third of the photo; centring it pushed the
  sign above the visible window entirely. This keeps it in frame for most
  of the scroll, though not all of it -- see the note on the parallax
  script about why "always visible" and a longer travel are at odds.
*/
.testimonial-media {
  position: absolute;
  inset: -170px 0;
  will-change: transform;
  background-image:
    linear-gradient(rgba(237,227,210,0.86), rgba(237,227,210,0.86)),
    url("assets/portland.jpg");
  background-size: cover;
  background-position: center top;
}

.section-testimonials > .wrap { position: relative; z-index: 1; }

.section-testimonials h2 { margin-bottom: 3rem; }

.quote-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* position:relative anchors the star row to this card's bottom edge.
   Extra bottom padding keeps the attribution clear of it. */
.quote {
  background: var(--sand);
  border: 1px solid var(--line);
  margin: 0;
  padding: 1.8rem 1.6rem 2.3rem;
  text-align: center;
  position: relative;
  transition: background 0.25s ease, border-color 0.25s ease;
}

/* Inverts, but never lifts or scales -- these carry the star row on their
   bottom edge, and moving the card would drag the stars off the line they
   share with the other two. */
.quote:hover {
  background: var(--ink);
  border-color: var(--ink);
}
.quote:hover p    { color: var(--cream); }
.quote:hover cite { color: #C9BCA0; }

/*
  The stars straddle the card's edge: their top half is over the card,
  their bottom half over the section behind it. So on inversion they
  cannot simply take one colour -- ink would vanish into the dark card,
  cream would wash out against the light ground below. Cream plus a dark
  drop-shadow reads on both halves at once.
*/
.quote:hover .star {
  fill: var(--cream);
  filter: drop-shadow(0 1px 1.5px rgba(40, 30, 16, 0.55));
}

@keyframes quote-flash {
  0%, 100% { background-color: var(--sand); border-color: var(--line); }
  35%, 55% { background-color: var(--ink);  border-color: var(--ink);  }
}

@keyframes quote-flash-text {
  0%, 100% { color: var(--ink); }
  35%, 55% { color: var(--cream); }
}

@keyframes quote-flash-cite {
  0%, 100% { color: #6D6247; }
  35%, 55% { color: #C9BCA0; }
}

@keyframes quote-flash-star {
  0%, 100% { fill: var(--ink);   filter: none; }
  35%, 55% { fill: var(--cream); filter: drop-shadow(0 1px 1.5px rgba(40, 30, 16, 0.55)); }
}

.quote.is-flash          { animation: quote-flash 1.25s ease-in-out var(--flash-delay, 0s); }
.quote.is-flash p        { animation: quote-flash-text 1.25s ease-in-out var(--flash-delay, 0s); }
.quote.is-flash cite     { animation: quote-flash-cite 1.25s ease-in-out var(--flash-delay, 0s); }
/* fill and filter only -- opacity and transform stay with the scroll
   script, which is still driving these same elements. */
.quote.is-flash .star    { animation: quote-flash-star 1.25s ease-in-out var(--flash-delay, 0s); }

.quote-grid .quote:nth-child(1) { --flash-delay: 0s; }
.quote-grid .quote:nth-child(2) { --flash-delay: 0.09s; }
.quote-grid .quote:nth-child(3) { --flash-delay: 0.18s; }

.quote p {
  font-style: italic;
  font-size: 0.95rem;
  color: var(--ink);
  margin-bottom: 1.2rem;
}

.quote cite {
  font-style: normal;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #6D6247;
  display: block;
}

/* ---------- Review stars ---------- */

/*
  Pinned to the card's bottom edge and pulled half outside it, so the row
  straddles the border. Grid cards in a row are equal height, so anchoring
  to the bottom puts every row on one line across all three -- following
  the quote in normal flow did not, because the quotes differ in length
  and the attribution therefore sits at a different height in each card.
*/
.stars {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  transform: translateY(50%);
  display: flex;
  justify-content: center;
  gap: 0.42rem;
  margin: 0;
}

.star {
  width: 26px;
  height: 26px;
  fill: var(--ink);
  display: block;
  will-change: opacity, transform;
}

/*
  Stars are visible by default and only hidden once the script arms them,
  so they are present with JavaScript off rather than never arriving.
*/
.stars.is-armed .star { opacity: 0; transform: scale(0.4); }

/*
  No keyframes here any more. The stars are driven straight from scroll
  position by the script, which sets opacity and scale inline -- a timed
  animation plays at its own pace once triggered and is easy to scroll
  straight past, whereas this is tied to the wheel: slow scrolling fills
  them slowly, and scrolling back up empties them again.
*/

/* ---------- Map + office card ---------- */

/* No bottom padding: the card sits inside the map, so the closing band
   meets the map's edge directly. */
.section-map { position: relative; background: var(--beige); }

.map-embed { line-height: 0; }

/*
  grayscale(0.72) keeps the map muted while leaving the pin enough red to
  read as brick. Full grayscale blacks the pin out -- the map is an
  iframe, so a filter over the whole frame is the only lever and it
  cannot spare the marker.

  The frame is shown whole, never clipped -- clipping is what cropped
  Google's place card off the top-left corner.

  760px is the height that lets the office card sit wholly inside the map
  while the place card stays intact. The marker is fixed at the frame's
  midpoint, so a whole card has to fit below that midpoint; nothing here
  moves the pin, which is what previously required the clip. The cost is
  a tall frame -- these two are the trade: card inside the map, or a
  shorter map.
*/
.map-embed { line-height: 0; }

.map-embed iframe {
  width: 100%;
  height: 760px;
  border: 0;
  display: block;
  filter: grayscale(0.72) contrast(0.96);
}

/*
  Frosted panel over the map. backdrop-filter blurs whatever sits behind
  it, so the map reads as texture without competing with the text.

  Alpha can sit this low only because the text is --ink rather than
  --body. The backdrop shifts as the map scrolls, and over its darkest
  areas 0.66 gives 3.51:1 against --body -- under AA -- but 6.37:1
  against --ink. Lightening this text would break that, so if the copy
  colour changes, re-check the alpha.
*/
.office-card {
  background: rgba(243,234,215,0.66);
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
  border: 1px solid var(--line);
  color: var(--ink);
  text-align: center;
  padding: 2.1rem 1.9rem;
  width: 372px;
  max-width: calc(100% - 2.8rem);
  /*
    Out of flow so the section is exactly as tall as the map and the card
    floats near its foot. In flow the card's height still counted after
    the pull-up, so the frame hung past the section into the closing band.
  */
  position: absolute;
  left: 50%;
  bottom: 32px;
  transform: translateX(-50%);
  margin: 0;
  z-index: 1;
}

/* Generous above the title and below the last line, tight between the
   three blocks -- the padding holds the outer breathing room. */
.office-card h2 { font-size: 1.8rem; margin-bottom: 0.85rem; }

.office-card p { font-size: 0.92rem; margin-bottom: 0.25rem; }

.office-name { font-weight: 700; color: var(--ink); }

.office-hours-label { margin-top: 0.85rem; }

.office-email { color: var(--ink); }

/* ---------- Closing band ---------- */

/* No top margin: the band butts straight against the map's bottom edge,
   with no strip of ground between them. */
.closing {
  background: var(--dark);
  text-align: center;
  padding: 4.5rem 0;
  margin-top: 0;
}

.closing h2 {
  color: var(--cream);
  font-weight: 700;
  font-size: clamp(1.5rem, 3vw, 2.05rem);
  line-height: 1.4;
  margin-bottom: 1.6rem;
}

.closing-rule {
  border: 0;
  border-top: 2px solid #70624B;
  width: 60px;
  margin: 0 auto 1.4rem;
}

.closing-note {
  font-size: 0.8rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #A39578;
  margin: 0;
}

/* ---------- Footer ---------- */

/* Disclaimer on top, then a single thin bar: logo, address, socials,
   copyright on one row. */
/* Padding comes down as the logo goes up, so the bar keeps its height.
   The logo is the tallest thing in this row, so it alone sets how deep
   the footer sits. */
.site-footer { background: var(--dark-soft); padding: 0.2rem 0 0.2rem; }

.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
}

/*
  Uses the trimmed logo file, not logo-white.png. The original is
  1120x768 but its lettering only fills 53% of that -- the rest is
  transparent padding, so most of the box was empty and enlarging it
  mostly enlarged the emptiness. logo-white-tight.png is the same mark
  cropped to its ink, which is why this width looks so much bigger than
  the old 96px while the bar is no taller.

  logo-white-left.png goes one step further: in the original artwork
  "Neil T." is set flush right over "Jorgenson", which didn't match the
  header, where the same two lines are HTML text stacked flush left. This
  copy nudges the top line over so both start at the same x. Generated
  from logo-white-tight.png, not hand-drawn -- same letterforms, just
  moved.
*/
.footer-logo { width: 155px; opacity: 0.92; }

.social {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/*
  Monochrome rather than brand colours: six saturated logos on a warm
  dark bar fight the palette. currentColor means hover only needs to
  change the colour.
*/
.social a {
  display: block;
  color: #C9BCA0;
  transition: color 0.2s ease;
}
.social a:hover, .social a:focus { color: var(--cream); }

.social svg {
  width: 17px;
  height: 17px;
  display: block;
  fill: currentColor;
}

.footer-address, .footer-copy {
  font-size: 0.82rem;
  color: #CABCA0;
  margin: 0;
}

/* Kept light enough for AA against --dark-soft: this is the legal
   disclaimer and has to be readable, not decorative. It sits above the
   footer bar -- the bar is thin, but this notice earns its space. */
.site-footer .fine {
  font-size: 0.68rem;
  color: #B6A78A;
  max-width: 82ch;
  margin: 0 auto 0.6rem;
  padding: 0 1.4rem;
  text-align: center;
  line-height: 1.5;
}

/* ---------- Mobile ---------- */

@media (max-width: 900px) {
  .section-head { grid-template-columns: 1fr; gap: 2rem; }
  .rule-heading { white-space: normal; }
  .stat { border-left: 0; padding-left: 0; border-top: 1px solid var(--rule); padding-top: 1.2rem; }
  /* Single column: stop stretching, or the portrait grows absurdly tall
     with nothing beside it to match. */
  .about { grid-template-columns: 1fr; gap: 2.5rem; align-items: start; }
  .portrait-frame { max-width: 340px; }
  .portrait { height: auto; object-fit: fill; }
}

@media (max-width: 820px) {
  .wrap { padding: 0 1.4rem; }
  .header-inner { justify-content: center; gap: 1rem; }
  .nav { gap: 1.3rem; justify-content: center; width: 100%; }
  .nav a.nav-call { margin-left: 0; padding: 0.55rem 0.9rem; font-size: 0.82rem; }
  .hero { min-height: 480px; padding: 4rem 0; }
  .hero h1 { max-width: 100%; }
  .hero-media { background-position: 68% center, 68% center, center; }
  .section { padding: 3.5rem 0; }
  /* Back into flow on narrow screens: a tall map with the card floating
     inside it does not survive the drop to one column. */
  .map-embed iframe { height: 380px; }
  .office-card {
    position: static;
    transform: none;
    width: auto;
    max-width: none;
    margin: -90px 1.4rem 0;
  }
  .closing { margin-top: 3rem; padding: 3.25rem 0; }
  .footer-inner { justify-content: center; text-align: center; }
}
