/* ==========================================================================
   Pofu Hsieh, D.D.S. Website proposal prepared by Roanlight.
   Built on the Aster design system. Used by index.html, services.html,
   about.html, contact.html.

   One hue only: forest green. Everything else is a neutral.
   Display face: Plus Jakarta Sans (700/800). Text face: Inter (400/600).

   Two things differ from the Aster template on purpose:
     1. The reviews and testimonials styles are deleted. This practice has no
        positive public reviews, so the site carries no reviews section at all
        and there is no styling left here that would make one easy to add.
     2. A .tbc class exists for unconfirmed facts. See the comment beside it.
   ========================================================================== */

:root{
  /* type */
  --font-display:'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
  --font-body:'Inter', system-ui, -apple-system, sans-serif;

  /* the single theme hue and its shades */
  --forest:#1F4A3C;
  --forest-deep:#143026;
  --forest-mid:#33654F;
  --forest-soft:#5C8574;
  --forest-wash:#EDF3F0;
  --forest-line:#CBDCD4;

  /* neutrals */
  --white:#FFFFFF;
  --paper:#F6F7F6;
  --ink:#161A18;
  --ink-soft:#5C645F;
  --ink-faint:#8A918C;
  --line:#E3E6E4;

  /* Diffused, low opacity, multi layer. Tinted to the forest hue rather than
     black, so a shadow never reads as a grey smudge on the paper. */
  --shadow-xs:
    0 1px 2px rgba(20,48,38,0.04),
    0 3px 8px -3px rgba(20,48,38,0.06);
  --shadow:
    0 1px 2px rgba(20,48,38,0.03),
    0 8px 20px -10px rgba(20,48,38,0.10),
    0 26px 52px -28px rgba(20,48,38,0.14);
  --shadow-lift:
    0 2px 4px rgba(20,48,38,0.03),
    0 16px 34px -14px rgba(20,48,38,0.12),
    0 46px 90px -40px rgba(20,48,38,0.20);

  /* RADIUS SYSTEM. One documented rule, applied everywhere:
       pill  buttons and tags
       sm    inputs and small inline controls
       lg    cards, panels, form blocks
       xl    photographs and large media, section panels
     The only shapes sharper than --r-sm are hairlines and the flower mark,
     whose petals need a 3px tail to read as petals at 26px. */
  --r-sm:12px;
  --r-md:16px;
  --r-lg:24px;
  --r-xl:32px;
  --r-pill:999px;
  --radius:var(--r-md); /* legacy alias, kept so nothing falls back to sharp */

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-soft: cubic-bezier(0.32, 0.72, 0, 1);
  /* expo.out equivalent: leaves fast, lands almost still. The curve that makes
     an entrance read as "settling" rather than "sliding". */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}

*{ box-sizing:border-box; }
html{ scroll-behavior:smooth; }

body{
  margin:0;
  background:var(--white);
  color:var(--ink);
  font-family:var(--font-body);
  font-weight:400;
  font-size:16px;
  line-height:1.65;
  -webkit-font-smoothing:antialiased;
}

img,svg{ display:block; max-width:100%; }
a{ color:inherit; }

.wrap{
  max-width:1180px;
  margin:0 auto;
  padding:0 32px;
}
.wrap-narrow{ max-width:820px; }

h1,h2,h3,h4,.serif{
  font-family:var(--font-display);
  font-weight:700;
  letter-spacing:-0.022em;
  margin:0;
  line-height:1.12;
}

.eyebrow{
  font-family:var(--font-body);
  text-transform:uppercase;
  font-size:0.7rem;
  letter-spacing:0.2em;
  color:var(--forest);
  font-weight:600;
  margin:0;
}

/* ---------- Header ---------- */
header{
  position:sticky; top:0; z-index:50;
  background:rgba(255,255,255,0.9);
  backdrop-filter:blur(12px);
  border-bottom:1px solid var(--line);
}
nav.wrap{
  display:flex; align-items:center; justify-content:space-between;
  padding-top:18px; padding-bottom:18px;
}
/* The name IS the mark. The drawn flower this replaces came from the template
   this site was built on and meant nothing here, so the wordmark now carries
   the identity on its own. No gap left behind: the mark is gone, not hidden. */
.brand{
  display:flex; align-items:center;
  font-family:var(--font-display);
  font-size:1.34rem;
  font-weight:800;
  text-decoration:none;
  letter-spacing:-0.024em;
}
.nav-links{
  display:flex; align-items:center; gap:34px;
  list-style:none; margin:0; padding:0;
}
.nav-links a{
  text-decoration:none;
  font-size:0.9rem;
  color:var(--ink-soft);
  position:relative;
  padding-bottom:4px;
}
.nav-links a::after{
  content:'';
  position:absolute; left:0; bottom:0;
  width:0; height:1px;
  background:var(--forest);
  transition:width 0.28s var(--ease);
}
.nav-links a:hover{ color:var(--ink); }
.nav-links a:hover::after{ width:100%; }
.nav-links a.active{ color:var(--forest); }
.nav-links a.active::after{ width:100%; }
.nav-cta{ display:flex; align-items:center; gap:14px; }
.nav-phone{
  font-size:0.86rem; color:var(--ink-soft); text-decoration:none;
  display:none;
}
.nav-phone:hover{ color:var(--forest); }

.nav-toggle{
  display:none;
  width:38px; height:38px;
  border:1px solid var(--line);
  background:var(--white);
  border-radius:var(--r-md);
  cursor:pointer;
  align-items:center;
  justify-content:center;
  flex-direction:column;
  gap:4px;
}
.nav-toggle span{
  display:block;
  width:18px; height:1.5px;
  background:var(--ink);
  transition:transform 0.2s var(--ease), opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1){ transform:translateY(5.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2){ opacity:0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3){ transform:translateY(-5.5px) rotate(-45deg); }

.mobile-nav{
  display:none;
  flex-direction:column;
  gap:2px;
  padding:6px 32px 22px;
  border-top:1px solid var(--line);
}
.mobile-nav.open{ display:flex; }
.mobile-nav a{
  text-decoration:none;
  color:var(--ink-soft);
  font-size:0.98rem;
  padding:12px 0;
  border-bottom:1px solid var(--line);
}
.mobile-nav a.active{ color:var(--forest); }
.mobile-nav .btn{ margin-top:14px; align-self:flex-start; }

@media(min-width:900px){ .nav-phone{ display:inline; } }
@media(max-width:860px){
  .nav-links{ display:none; }
  .nav-toggle{ display:flex; }
}
@media(min-width:861px){ .mobile-nav{ display:none !important; } }

/* ---------- Buttons ---------- */
.btn{
  display:inline-flex; align-items:center; justify-content:center;
  gap:8px;
  padding:13px 26px;
  font-family:var(--font-body);
  font-size:0.86rem;
  font-weight:600;
  letter-spacing:0.01em;
  text-decoration:none;
  border-radius:var(--r-pill);
  border:1px solid transparent;
  cursor:pointer;
  transition:transform 0.32s var(--ease-soft), box-shadow 0.32s var(--ease-soft), background 0.3s var(--ease-soft), color 0.3s var(--ease-soft), border-color 0.3s var(--ease-soft);
  white-space:nowrap;
}
.btn-solid{
  background:var(--forest);
  color:var(--white);
}
.btn-solid:hover{
  background:var(--forest-deep);
  transform:translateY(-2px);
  box-shadow:var(--shadow);
}
.btn-outline{
  background:transparent;
  border-color:var(--forest);
  color:var(--forest);
}
.btn-outline:hover{
  background:var(--forest);
  color:var(--white);
  transform:translateY(-2px);
}
.btn-sm{ padding:10px 20px; font-size:0.8rem; }
.btn-lg{ padding:17px 34px; font-size:0.95rem; }
/* physical press, so a tap acknowledges itself */
.btn:active{ transform:translateY(0) scale(0.985); }
.btn[disabled], .btn.is-disabled{ opacity:0.55; cursor:not-allowed; }
.btn[disabled]:hover, .btn.is-disabled:hover{ transform:none; box-shadow:none; }

/* ---------- Hero ---------- */
.hero{
  position:relative;
  padding:76px 0 84px;
  overflow:hidden;
}
.hero-grid{
  display:grid;
  grid-template-columns:1.02fr 0.98fr;
  gap:64px;
  align-items:center;
}
@media(max-width:940px){
  .hero-grid{ grid-template-columns:1fr; gap:44px; }
}
.hero-copy{ max-width:600px; }
.hero h1{
  font-size:clamp(2.7rem, 6vw, 4.5rem);
  font-weight:800;
  letter-spacing:-0.032em;
  line-height:1.06;
  margin-top:20px;
}
.hero h1 em{
  font-style:normal;
  font-weight:800;
  color:var(--forest);
}
.hero p.lede{
  margin-top:22px;
  font-size:1.1rem;
  color:var(--ink-soft);
  max-width:520px;
}

/* ---------- Home hero: the name is the hero ----------
   The practice asked for the doctor's name as the dominant element and a few
   short highlights, nothing paragraph shaped. So the H1 is the name in both
   scripts, the highlights are four fragments, and there is one action. */
.hero-name{
  display:flex;
  flex-direction:column;
  gap:0.18em;
  margin-top:0;
}
.hero-name-en{
  display:block;
  font-size:clamp(2.6rem, 6.6vw, 5rem);
  font-weight:800;
  letter-spacing:-0.038em;
  line-height:1.02;
  text-wrap:balance;
}
.hero-name-zh{
  display:block;
  font-family:var(--font-display);
  font-size:clamp(1.5rem, 3.1vw, 2.3rem);
  font-weight:700;
  letter-spacing:0.18em;
  line-height:1.1;
  color:var(--forest);
}
.hero .hero-actions{ margin-top:34px; }
.hero-actions{
  display:flex; flex-wrap:wrap; gap:14px;
  margin-top:32px;
}

/* parallax frame: the image inside is taller than the frame and slides */
.parallax-frame{
  position:relative;
  overflow:hidden;
  border-radius:var(--r-xl);
  background:var(--forest-wash);
}
.parallax-frame img{
  width:100%;
  height:118%;
  object-fit:cover;
  position:relative;
  top:-9%;
  will-change:transform;
}
.hero-figure{
  position:relative;
}
.hero-figure .parallax-frame{
  aspect-ratio:4/5;
  /* capped so the name, the highlights and the one action all sit above the
     fold on a laptop rather than being pushed down by a very tall photo */
  max-height:clamp(400px, 60vh, 620px);
  box-shadow:var(--shadow-lift);
}
/* a hairline inside the photo's radius keeps the edge from dissolving into
   the paper on light images, without a visible grey box around it */
.parallax-frame::after,
.unmask::before{ border-radius:inherit; }
.parallax-frame::after{
  content:'';
  position:absolute; inset:0;
  border-radius:inherit;
  box-shadow:inset 0 0 0 1px rgba(20,48,38,0.06);
  pointer-events:none;
  z-index:2;
}
.hero-figure figcaption{
  margin-top:14px;
  font-size:0.78rem;
  color:var(--ink-faint);
  letter-spacing:0.02em;
}
.hero-rule{
  height:1px;
  background:var(--forest-line);
  margin-top:64px;
}

/* interior page hero */
.page-hero{ padding:64px 0 56px; }
.page-hero h1{ font-size:clamp(2.2rem, 5vw, 3.5rem); font-weight:800; letter-spacing:-0.03em; margin-top:18px; }
.page-hero p.lede{ margin-top:18px; }
.page-hero .hero-grid{ grid-template-columns:1.05fr 0.95fr; gap:56px; }
.page-hero .parallax-frame{ aspect-ratio:5/4; box-shadow:var(--shadow); }

/* ---------- Trust strip ---------- */
.trust-strip{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));
  gap:24px 26px;
  margin-top:40px;
  padding-top:26px;
  border-top:1px solid var(--line);
}
.trust-item{ max-width:220px; }
.trust-item strong{
  display:block;
  font-family:var(--font-display);
  font-size:1.6rem;
  font-weight:800;
  letter-spacing:-0.02em;
  color:var(--forest);
  line-height:1.2;
}
.trust-item span{
  display:block;
  font-size:0.84rem;
  color:var(--ink-soft);
  margin-top:2px;
}

/* ---------- Entrance motion ----------
   The choreography layer lives at the BOTTOM of this file, immediately before
   the reduced motion block. It has to: several component rules below use the
   `transition` shorthand for their hover states, and a shorthand resets
   transition-delay and transition-property. If the motion block sat here, the
   .service-card hover rule further down would silently wipe out the stagger
   and the opacity fade on every card in the grid. Cascade order is the fix,
   not a specificity hack. Read the block at the bottom for the full system. */

/* ---------- SIGNATURE MOVE: the curtain unmask ----------
   The reveal edge used to be a clip-path on the photograph itself. clip-path is
   not GPU composited in most browsers, so every frame repainted a full size
   image, which is what made large photos sit still and then jump. The same
   top-to-bottom wipe is now a solid panel sitting over the image that slides
   down and out of an overflow:hidden box, which is a pure transform and runs on
   the compositor. The photo underneath does a short opacity and scale settle. */
.unmask{
  overflow:hidden;
  position:relative;
  isolation:isolate;
  background:var(--forest-wash);
  --step:55ms;
}
.unmask img{
  opacity:0;
  transform:scale(1.045);
  transition:opacity 0.45s var(--ease-soft), transform 0.7s var(--ease-soft);
  transition-delay:calc(var(--stagger, 0) * var(--step, 55ms));
  will-change:opacity, transform;
}
.unmask.in img{
  opacity:1;
  transform:scale(1);
}
/* the curtain */
.unmask::before{
  content:'';
  position:absolute;
  inset:0;
  z-index:1;
  background:var(--forest-wash);
  transform:translateY(0);
  transition:transform 0.7s var(--ease-soft);
  transition-delay:calc(var(--stagger, 0) * var(--step, 55ms));
  pointer-events:none;
}
.unmask.in::before{ transform:translateY(100%); }
/* a hairline that draws itself alongside the unmask, on scaleX rather than
   width so it is a transform too */
.unmask::after{
  content:'';
  position:absolute;
  left:0; bottom:0;
  width:100%; height:2px;
  background:var(--forest);
  transform:scaleX(0);
  transform-origin:left center;
  transition:transform 0.7s var(--ease-soft);
  transition-delay:calc(var(--stagger, 0) * var(--step, 55ms));
  z-index:2;
}
.unmask.in::after{ transform:scaleX(1); }
.unmask.done img{ will-change:auto; }

/* ---------- Section shells ---------- */
section{ padding:clamp(68px, 8.5vw, 116px) 0; }
.section-wash{ background:var(--paper); }
.section-forest{
  background:var(--forest);
  color:var(--white);
}
.section-forest .eyebrow{ color:#A9C9BC; }
.section-forest h2, .section-forest h3{ color:var(--white); }
.section-forest p{ color:#D3E1DA; }

.section-head{
  max-width:660px;
  margin:0 auto 60px;
  text-align:center;
}
.section-head.align-left{ margin-left:0; margin-right:0; text-align:left; }
.section-head h2{
  font-size:clamp(2rem, 3.8vw, 2.9rem);
  margin-top:14px;
}
.section-head p{
  margin-top:16px;
  color:var(--ink-soft);
  font-size:1.02rem;
}

/* ---------- Services ---------- */
.services-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(230px, 1fr));
  gap:22px;
}
.service-card{
  background:var(--white);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:34px 30px;
  box-shadow:var(--shadow-xs);
  transition:transform 0.4s var(--ease-soft), box-shadow 0.4s var(--ease-soft), border-color 0.4s var(--ease-soft);
}
.service-card:hover{
  transform:translateY(-5px);
  box-shadow:var(--shadow);
  border-color:var(--forest-line);
}
.service-icon{
  width:36px; height:36px;
  margin-bottom:20px;
  stroke:var(--forest);
  fill:none;
  stroke-width:1.4;
}
.service-card h3{ font-size:1.32rem; }
.service-card p{
  margin-top:10px;
  font-size:0.93rem;
  color:var(--ink-soft);
}

/* photo-led service cards */
.photo-card{
  background:var(--white);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  overflow:hidden;
  box-shadow:var(--shadow-xs);
  transition:transform 0.4s var(--ease-soft), box-shadow 0.4s var(--ease-soft);
}
.photo-card:hover{ transform:translateY(-5px); box-shadow:var(--shadow); }
.photo-card .photo-card-img{ aspect-ratio:4/3; }
.photo-card .photo-card-img img{ width:100%; height:100%; object-fit:cover; }
.photo-card-body{ padding:24px 24px 28px; }
.photo-card-body h3{ font-size:1.3rem; }
.photo-card-body p{ margin-top:10px; font-size:0.93rem; color:var(--ink-soft); }

/* detail rows on services.html */
.service-detail{
  display:grid;
  grid-template-columns:0.9fr 1.1fr;
  gap:44px;
  align-items:center;
  padding:44px 0;
  border-top:1px solid var(--line);
}
.service-detail:last-of-type{ border-bottom:1px solid var(--line); }
@media(max-width:820px){
  .service-detail{ grid-template-columns:1fr; gap:26px; }
}
.service-detail .detail-img{ aspect-ratio:5/4; border-radius:var(--r-xl); overflow:hidden; }
.service-detail .detail-img img{ width:100%; height:100%; object-fit:cover; }
.service-detail.flip .detail-img{ order:2; }
@media(max-width:820px){ .service-detail.flip .detail-img{ order:0; } }
.service-detail h3{ font-size:1.7rem; margin-top:12px; }
.service-detail p{ margin-top:12px; color:var(--ink-soft); }
.service-tags{
  margin-top:18px;
  display:flex; flex-wrap:wrap; gap:8px;
}
.service-tags span{
  font-size:0.75rem;
  color:var(--forest);
  border:1px solid var(--forest-line);
  border-radius:var(--r-pill);
  padding:6px 14px;
  background:var(--forest-wash);
}

/* ---------- Editorial split ---------- */
.editorial .wrap{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:64px;
  align-items:center;
}
@media(max-width:820px){ .editorial .wrap{ grid-template-columns:1fr; gap:40px; } }
.editorial-figure{ margin:0; }
.editorial-figure .unmask{ aspect-ratio:4/5; border-radius:var(--r-xl); }
.editorial-figure .unmask img{ width:100%; height:100%; object-fit:cover; }
.editorial-figure figcaption{
  margin-top:12px;
  font-size:0.78rem;
  color:var(--ink-faint);
}
.editorial.flip .editorial-figure{ order:2; }
@media(max-width:820px){ .editorial.flip .editorial-figure{ order:0; } }

blockquote{
  font-family:var(--font-display);
  font-style:normal;
  font-weight:700;
  letter-spacing:-0.022em;
  font-size:1.7rem;
  line-height:1.34;
  margin:0 0 22px;
  color:var(--ink);
}
.section-forest blockquote{ color:var(--white); }
.editorial-body p{ color:var(--ink-soft); }
.editorial-body .eyebrow{ margin-bottom:16px; display:block; }

/* ---------- Gallery band ---------- */
.gallery-grid{
  display:grid;
  grid-template-columns:repeat(3, 1fr);
  gap:20px;
}
@media(max-width:820px){ .gallery-grid{ grid-template-columns:1fr; } }
.gallery-item{ margin:0; }
.gallery-item .unmask{ border-radius:var(--r-xl); }
.gallery-item .unmask img{ width:100%; height:100%; object-fit:cover; }
.gallery-item.tall .unmask{ aspect-ratio:3/4; }
.gallery-item .unmask{ aspect-ratio:3/4; }
.gallery-item figcaption{
  margin-top:14px;
  font-size:0.9rem;
  color:var(--ink-soft);
}
.gallery-item figcaption strong{
  display:block;
  font-family:var(--font-display);
  font-size:1.2rem;
  color:var(--ink);
  font-weight:700;
  letter-spacing:-0.018em;
  margin-bottom:4px;
}
.section-forest .gallery-item figcaption strong{ color:var(--white); }

/* ---------- Values ---------- */
.values-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(230px,1fr));
  gap:32px;
}
.value-card{ padding-top:20px; border-top:2px solid var(--forest); }
.value-card .eyebrow{ display:block; margin-bottom:10px; }
.value-card h3{ font-size:1.3rem; }
.value-card p{ margin-top:10px; color:var(--ink-soft); font-size:0.93rem; }

/* ---------- Team ---------- */
.team-grid{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(230px,1fr));
  gap:26px;
}
.team-card{
  border-top:1px solid var(--line);
  padding:22px 0 0;
}
.team-card h3{ font-size:1.32rem; }
.team-card .role{
  display:block;
  margin-top:8px;
  font-size:0.74rem;
  color:var(--forest);
  text-transform:uppercase;
  letter-spacing:0.12em;
  font-weight:600;
}
.team-card p.bio{
  margin-top:14px;
  font-size:0.92rem;
  color:var(--ink-soft);
}

/* ---------- Reviews: deliberately absent ----------
   The Aster template carried a reviews grid, review cards and a star rating
   component. All of it is removed here. This practice's public rating is
   currently 1.0 across every platform it appears on, so there is nothing
   honest to put in a reviews section, and a site that names a real business
   must not carry invented praise. If reviews are ever added, they get built
   from real, attributable ones, not from this file. */

/* ---------- CTA band ---------- */
.cta-band{
  background:var(--forest);
  color:var(--white);
  border-radius:var(--r-xl);
  margin:0 32px;
  padding:76px 48px;
  box-shadow:var(--shadow-lift);
  text-align:center;
}
.cta-band h2{
  color:var(--white);
  font-size:clamp(1.9rem, 3.8vw, 2.7rem);
}
.cta-band h2 em{ font-style:normal; font-weight:800; color:#B7D6C8; }
.cta-band p{ margin-top:14px; color:#CFE0D8; }
.cta-band .hero-actions{ justify-content:center; margin-top:30px; }
.cta-band .btn-solid{ background:var(--white); color:var(--forest); }
.cta-band .btn-solid:hover{ background:#E6EFEA; color:var(--forest-deep); }
.cta-band .btn-outline{ border-color:rgba(255,255,255,0.5); color:var(--white); }
.cta-band .btn-outline:hover{ background:var(--white); color:var(--forest); }

/* ---------- Contact page ---------- */
.contact-grid{
  display:grid;
  grid-template-columns:1fr 1fr 1.15fr;
  gap:24px;
  align-items:start;
}
@media(max-width:900px){ .contact-grid{ grid-template-columns:1fr; } }

.hours-card, .contact-card{
  background:var(--white);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:32px;
  box-shadow:var(--shadow-xs);
}
.hours-card h3, .contact-card h3{ font-size:1.3rem; margin-bottom:18px; }
.hours-list{ list-style:none; margin:0; padding:0; }
.hours-list li{
  display:flex; justify-content:space-between; gap:12px;
  padding:10px 0;
  border-bottom:1px solid var(--line);
  font-size:0.92rem;
}
.hours-list li:last-child{ border-bottom:none; }
.hours-list span:first-child{ color:var(--ink-soft); }
.hours-list span:last-child{ font-weight:500; }
.hours-list li.closed span:last-child{ color:var(--forest); }
/* Closed days are set back rather than emphasised: a reader scanning for when
   the office is OPEN should not have their eye caught by the two days it is not. */

/* Notes under the hours table. The alternating Saturday is the one detail a
   patient can act on wrongly and lose a trip over, so it gets its own line
   rather than a parenthetical inside the row. */
.hours-note{
  margin:14px 0 0;
  font-size:0.88rem;
  line-height:1.5;
  color:var(--ink-soft);
}
.hours-note-open{
  margin-top:10px;
  color:var(--forest);
  font-weight:500;
}

.hours-list .is-closed{ color:var(--ink-faint); font-weight:400; }
.contact-card ul{ list-style:none; margin:0; padding:0; }
.contact-card li{
  padding:9px 0;
  font-size:0.92rem;
  color:var(--ink-soft);
  display:flex;
  gap:10px;
}
.contact-card li strong{ color:var(--ink); font-weight:600; min-width:76px; }

/* honest replacement for the old fake map: a photo of the front desk plus
   written directions, no pretend cartography */
.find-us{
  margin:0;
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  overflow:hidden;
  background:var(--white);
  box-shadow:var(--shadow-xs);
}
.find-us .unmask{ aspect-ratio:4/3; }
.find-us .unmask img{ width:100%; height:100%; object-fit:cover; }
.find-us .find-us-body{
  padding:22px 24px 26px;
}
.find-us h3{ font-size:1.25rem; }
.find-us p{ margin:10px 0 0; font-size:0.9rem; color:var(--ink-soft); }

/* ---------- Form ---------- */
.form-card{
  background:var(--white);
  border:1px solid var(--line);
  border-radius:var(--r-lg);
  padding:44px;
  box-shadow:var(--shadow-xs);
}
.form-card .form-note{
  font-size:0.82rem;
  color:var(--forest-deep);
  background:var(--forest-wash);
  border:1px solid var(--forest-line);
  border-radius:var(--r-md);
  padding:12px 16px;
  margin:0 0 28px;
}
.form-row{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:20px;
}
@media(max-width:600px){ .form-row{ grid-template-columns:1fr; } }
.field{ margin-bottom:20px; }
.field label{
  display:block;
  font-size:0.8rem;
  font-weight:600;
  color:var(--ink);
  margin-bottom:8px;
}
.field input,
.field select,
.field textarea{
  width:100%;
  font-family:var(--font-body);
  font-size:0.93rem;
  color:var(--ink);
  background:var(--paper);
  border:1px solid var(--line);
  border-radius:var(--r-sm);
  padding:13px 15px;
}
.field input:focus,
.field select:focus,
.field textarea:focus{
  outline:none;
  border-color:var(--forest-soft);
  box-shadow:0 0 0 3px rgba(31,74,60,0.12);
}
.field textarea{ resize:vertical; min-height:100px; }
.form-actions{
  margin-top:8px;
  display:flex; align-items:center; gap:16px; flex-wrap:wrap;
}
.form-hint{ font-size:0.8rem; color:var(--ink-faint); }

/* ---------- Footer ---------- */
footer{
  border-top:1px solid var(--line);
  padding:64px 0 32px;
}
.footer-grid{
  display:grid;
  grid-template-columns:1.4fr 1fr 1fr 1fr;
  gap:40px;
}
@media(max-width:760px){ .footer-grid{ grid-template-columns:1fr 1fr; } }
.footer-grid h4{
  font-family:var(--font-body);
  font-size:0.72rem;
  text-transform:uppercase;
  letter-spacing:0.16em;
  color:var(--ink-faint);
  margin:0 0 16px;
  font-weight:600;
}
.footer-grid ul{ list-style:none; margin:0; padding:0; }
.footer-grid li{ margin-bottom:10px; }
.footer-grid a{ text-decoration:none; color:var(--ink-soft); font-size:0.9rem; }
.footer-grid a:hover{ color:var(--forest); }
.footer-brand p{ color:var(--ink-soft); font-size:0.9rem; max-width:290px; margin-top:12px; }
.footer-bottom{
  margin-top:56px;
  padding-top:24px;
  border-top:1px solid var(--line);
  display:flex; flex-wrap:wrap; gap:16px;
  align-items:center; justify-content:space-between;
  font-size:0.78rem;
  color:var(--ink-faint);
}
.footer-note{ max-width:640px; margin:0; }
.footer-bottom p{ margin:0; }

/* room for the fixed proposal badge */
footer{ padding-bottom:82px; }

/* ==========================================================================
   Additions for this practice
   ========================================================================== */

/* ---------- .tbc, unconfirmed facts ----------
   WHY THIS EXISTS. This site names a real dentist and a real business. Some
   details could not be verified from any reliable source: office hours differ
   between listings, no email address is published anywhere, the practice names
   no insurance carriers, and it is unclear whether new patients are being
   taken. Rather than guess and put a wrong fact in front of a patient, every
   one of those is wrapped in .tbc and written in double square brackets. The
   amber highlight and dotted underline are meant to be impossible to miss in
   review, so nothing unconfirmed can quietly ship. When a fact is confirmed,
   delete the span along with the brackets. When the page contains no .tbc
   spans left, it is safe to publish. */
.tbc{
  background:#FFF3D4;
  color:#6B4A05;
  border-bottom:1px dotted #B8860B;
  border-radius:var(--r-sm);
  padding:1px 5px;
  font-weight:500;
  font-style:normal;
}
.tbc::before{
  content:"unconfirmed: ";
  font-size:0.72em;
  text-transform:uppercase;
  letter-spacing:0.1em;
  font-weight:600;
  color:#946308;
}
.hours-list .tbc{ text-align:right; }

/* ---------- Bilingual brand lockup in the header and footer ---------- */
.brand-names{
  display:flex;
  flex-direction:column;
  line-height:1.1;
}
.brand-en{ font-size:1.16rem; letter-spacing:-0.022em; }
.brand-zh{
  font-family:var(--font-body);
  font-size:0.86rem;
  font-weight:600;
  color:var(--forest);
  letter-spacing:0.14em;
  margin-top:2px;
}
@media(max-width:420px){
  .brand-en{ font-size:1rem; }
  .brand-zh{ font-size:0.78rem; }
}

/* ---------- The one line of Chinese on the page ----------
   Flushing is heavily Mandarin speaking, so the languages spoken get stated in
   Chinese as well as English rather than being left in a footer in English. */
.lang-line{
  font-family:var(--font-display);
  font-size:1.24rem;
  font-weight:700;
  letter-spacing:0.16em;
  color:var(--forest);
  margin:20px 0 0;
}
.lang-line-light{ color:#B7D6C8; margin-top:24px; }

/* Chinese proper nouns sitting inside English sentences read better with a
   touch more tracking than the Latin text around them. A three character name
   must also never break across two lines, which it will do by default. */
[lang="zh-Hant"]{ letter-spacing:0.04em; }
h1 [lang="zh-Hant"], h2 [lang="zh-Hant"], .brand-zh{ white-space:nowrap; }

/* ---------- Proposal badge override ----------
   The badge mechanism itself is untouched, it still comes from
   /demos/_shared/badge.css. This only makes room for the longer wording,
   "Website proposal, not live", which is wider than "Roanlight demo". */
.rl-badge{ font-size:0.75rem; padding-right:0.95rem; }
@media(max-width:380px){
  .rl-badge{ font-size:0.68rem; }
}

/* ---------- Contact page tweaks ---------- */
.contact-card li strong{ min-width:72px; }
.hours-card p{ line-height:1.55; }

::selection{ background:var(--forest); color:#fff; }

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

/* ==========================================================================
   THE CHOREOGRAPHY LAYER
   ==========================================================================

   WHAT THIS IS. Motion here is per ELEMENT, not per section. A section does
   not fade up as one lump. The eyebrow, the heading, the paragraph, each card,
   each row, each photograph and each button is its own beat, and the beats run
   in reading order with a short stagger between them.

   THE VOCABULARY. Seven moves. Each element type gets the one that suits what
   it is, because one fade-up applied to everything is exactly what makes
   motion read as cheap.

     .m-eyebrow   small labels, eyebrows, footer column headings
                  fade with a short horizontal drift from the left, 12px.
                  Small type should not travel vertically, it looks like it
                  fell off the line above.

     .m-head      headings, and blockquotes, which are set in the display face
                  a line mask: the text sits inside an overflow-hidden box and
                  rises out of it from 102% down. This is the "mask reveal"
                  from the GSAP SplitText preset, done with one wrapper span
                  per line instead of one element per character. On the hero
                  the two name lines are masked separately, so the name really
                  does arrive line by line.
                  NOTE the markup contract: .m-head is the mask, and the thing
                  that moves is its direct child span. Text must be wrapped.

     .m-text      body paragraphs, ledes, figcaptions
                  the quiet one: fade with a 14px rise. Prose should not
                  perform.

     .m-card      cards and panels: service cards, value cards, contact and
                  hours cards, the form card, the CTA band
                  rise 24px and settle with a hair of scale, 0.985 to 1. The
                  scale is what turns "a panel appeared" into "a card was
                  dealt". Cards animate whole. Their contents deliberately do
                  NOT get their own beats: a card is one object.

     .m-row       list rows in the footer columns
                  fade with a 10px drift from the left, tight 40ms steps, so a
                  column reads as a list being written down.

     .m-chip      pills and tags: the hero highlights, the service tags
                  pop in from 0.94 scale with a 8px rise, 40ms steps. Small,
                  many, and fast.

     .m-btn       buttons and action rows
                  rise 12px from 0.97 scale. Slightly more presence than a
                  chip because it is the thing you are meant to press.

     .m-stat      the big number on about.html
                  scales up from 0.82 with its origin pinned to the baseline,
                  so the figure grows out of its own label rather than
                  floating in.

     .m-rule      standalone dividers, e.g. the hero rule
                  draws along its length, scaleX 0 to 1 from the left.

     .unmask      photographs
                  untouched. The existing curtain: a solid panel slides down
                  and out of an overflow-hidden box while the photo settles in
                  opacity and scale, and a hairline draws along the bottom on
                  scaleX. See the section below for why it is built that way.

   TIMING. Every move is between 0.38s and 0.7s. Stagger step is per type,
   40ms for the small fast things and 55 to 60ms for everything else, and the
   index is CAPPED AT 6 in script.js so an eleven card grid does not crawl: the
   tail arrives together rather than trailing off. Worst case a section is
   fully settled in about 0.9s, and nothing is still moving by the time the
   reader has read it.

   GROUPING. script.js numbers the beats. A [data-choreo] container starts a
   count in document order; a nested [data-beat] container (a grid, a tag row)
   restarts it at zero, so a card grid deals from its own first card rather
   than continuing the heading's count.

   CONSTRAINTS THIS LAYER OBEYS.
     - transform and opacity only. Nothing here animates width, height, top,
       left, margin, clip-path or filter. The static overflow:hidden on .m-head
       and .unmask is layout, not animation.
     - will-change is set per move and dropped by .done once the element has
       settled, so we are not holding a compositor layer per node for the life
       of the page.
     - one shared IntersectionObserver for all of it, see script.js.
     - every class below has a counterpart in the reduced motion block at the
       bottom of this file. If you add a move, add it there too.
     - EVERYTHING HERE STARTS AT OPACITY 0. That makes the failsafe in
       script.js load bearing, not decorative. Any new class added to this
       block must also be added to MOVE_SELECTOR in script.js, or it will not
       be swept and it will stay invisible forever.
   ========================================================================== */

/* the shared resting state */
.reveal,
.m-eyebrow,
.m-text,
.m-card,
.m-row,
.m-chip,
.m-btn,
.m-stat,
.m-head > span{
  opacity:0;
  will-change:opacity, transform;
  transition-delay:calc(var(--stagger, 0) * var(--step, 55ms));
}

/* generic fallback move, kept so a bare .reveal still behaves */
.reveal{
  transform:translateY(18px);
  transition-property:opacity, transform;
  transition-duration:0.5s, 0.55s;
  transition-timing-function:var(--ease-out-expo);
}
.reveal.in{ opacity:1; transform:none; }

/* eyebrows and small labels: horizontal drift, never vertical */
.m-eyebrow{
  transform:translateX(-12px);
  transition-property:opacity, transform;
  transition-duration:0.42s, 0.48s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-eyebrow.in{ opacity:1; transform:none; }

/* headings: the line mask. The element is the box, the span is the line. */
.m-head{
  overflow:hidden;
  /* the mask box would otherwise clip descenders and the tail of a comma */
  padding-bottom:0.14em;
  margin-bottom:-0.14em;
}
.m-head > span{
  display:block;
  transform:translateY(102%);
  transition-property:opacity, transform;
  transition-duration:0.5s, 0.62s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-head.in > span{ opacity:1; transform:none; }

/* body copy: the quiet rise */
.m-text{
  transform:translateY(14px);
  transition-property:opacity, transform;
  transition-duration:0.5s, 0.55s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-text.in{ opacity:1; transform:none; }

/* cards: dealt, not revealed */
.m-card{
  transform:translateY(24px) scale(0.985);
  /* box-shadow and border-color are in this list only because .m-card now sits
     later in the cascade than .service-card and .photo-card, whose hover
     transitions would otherwise be dropped. The entrance uses the first two. */
  transition-property:opacity, transform, box-shadow, border-color;
  transition-duration:0.5s, 0.6s, 0.4s, 0.4s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-card.in{ opacity:1; transform:none; }

/* list rows: fast, tight, left to right */
.m-row{
  transform:translateX(-10px);
  transition-property:opacity, transform;
  transition-duration:0.38s, 0.44s;
  transition-timing-function:var(--ease-out-expo);
  --step:40ms;
}
.m-row.in{ opacity:1; transform:none; }

/* pills and tags */
.m-chip{
  transform:translateY(8px) scale(0.94);
  transition-property:opacity, transform;
  transition-duration:0.36s, 0.42s;
  transition-timing-function:var(--ease-out-expo);
  --step:40ms;
}
.m-chip.in{ opacity:1; transform:none; }

/* buttons */
.m-btn{
  transform:translateY(12px) scale(0.97);
  /* same reason as .m-card: .m-btn now outranks .btn, so the button's own
     hover properties have to be carried here as well. */
  transition-property:opacity, transform, box-shadow, background-color, border-color, color;
  transition-duration:0.42s, 0.5s, 0.32s, 0.3s, 0.3s, 0.3s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-btn.in{ opacity:1; transform:none; }

/* the one big number: grows off its own baseline */
.m-stat{
  transform:scale(0.82);
  transform-origin:0 100%;
  transition-property:opacity, transform;
  transition-duration:0.5s, 0.66s;
  transition-timing-function:var(--ease-out-expo);
  --step:55ms;
}
.m-stat.in{ opacity:1; transform:none; }

/* dividers draw along their length */
.m-rule{
  transform:scaleX(0);
  transform-origin:left center;
  transition:transform 0.7s var(--ease-out-expo);
  transition-delay:calc(var(--stagger, 0) * var(--step, 55ms));
  will-change:transform;
}
.m-rule.in{ transform:none; }

/* will-change is dropped once the move is over. Left on permanently across
   dozens of nodes it costs a compositor layer each, for nothing. */
.reveal.done,
.m-eyebrow.done,
.m-text.done,
.m-card.done,
.m-row.done,
.m-chip.done,
.m-btn.done,
.m-stat.done,
.m-rule.done,
.m-head.done > span{ will-change:auto; }

/* ---------- Reduced motion: everything arrives already finished ----------
   Every move in the choreography layer above has its counterpart here. The
   rule is not "turn the transition off", it is "put the element in its
   finished state": opacity 1, no transform, no delay, no will-change. A
   reader with reduced motion set gets a complete, legible, static page with
   nothing hidden, clipped, offset or half drawn.
   If you add a move above, add it here. */
@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior:auto; }

  .reveal,
  .m-eyebrow,
  .m-text,
  .m-card,
  .m-row,
  .m-chip,
  .m-btn,
  .m-stat,
  .m-rule,
  .m-head > span{
    opacity:1;
    transform:none;
    transition:none;
    transition-delay:0s;
    will-change:auto;
  }
  /* the heading mask box stops masking, so a descender or a second line can
     never be cut off */
  .m-head{
    overflow:visible;
    padding-bottom:0;
    margin-bottom:0;
  }

  /* The curtain is driven fully off the image and the photo sits at its
     finished opacity and scale, so every section renders complete. */
  .unmask img{
    opacity:1;
    transform:none;
    transition:none;
    transition-delay:0s;
    will-change:auto;
  }
  .unmask::before{ transform:translateY(100%); transition:none; transition-delay:0s; }
  .unmask::after{ transform:scaleX(1); transition:none; transition-delay:0s; }
  .parallax-frame img{ transform:none !important; }
  .btn, .service-card, .photo-card{ transition:none; }
  .btn:hover, .service-card:hover, .photo-card:hover{ transform:none; }
}





/* ---------- Hero lede ----------
   One warm sentence where a credential row used to be. The facts that matter to
   somebody deciding whether to call are that the practice has been here a long
   time, that they will see the dentist rather than whoever is free, and that
   they can actually get an appointment. Two lines carry all three. */
.hero-lede{
  margin:24px 0 0;
  max-width:44ch;
  font-size:clamp(1.02rem, 1.5vw, 1.15rem);
  line-height:1.65;
  color:var(--ink-soft);
}

/* ---------- The 37, given its due ----------
   The single most persuasive fact this practice owns, so it is set as a figure
   rather than buried in a sentence. Deliberately not a "stat card": one number,
   once, on the page where the dentist is introduced. */
.big-stat{
  display:flex;
  align-items:baseline;
  gap:14px;
  margin:0 0 18px;
}
.big-stat-num{
  font-family:var(--font-display);
  font-weight:800;
  font-size:clamp(3.6rem, 8vw, 5.6rem);
  line-height:0.86;
  letter-spacing:-0.045em;
  color:var(--forest);
}
.big-stat-label{
  font-family:var(--font-display);
  font-weight:700;
  font-size:clamp(1rem, 1.6vw, 1.22rem);
  line-height:1.2;
  letter-spacing:-0.01em;
  color:var(--ink);
  max-width:7ch;
}

/* ---------- Dark section overrides ----------
   These have to sit AFTER the component rules they correct. `.section-forest p`
   was declared next to the other forest styles, but `.section-head p` appears
   later in the file with identical specificity (0,1,1), so on every dark band
   the body copy silently fell back to --ink-soft, a dark grey, on forest green.
   Scoping to two classes puts the question beyond cascade order rather than
   relying on where in the file a rule happens to land. */
.section-forest .section-head p,
.section-forest .lede,
.section-forest p{
  color:#DCE8E2;
}
.section-forest .section-head .eyebrow,
.section-forest .eyebrow{
  color:#9FC6B5;
}
.section-forest .lang-line,
.section-forest .lang-line-light{
  color:var(--white);
  opacity:0.94;
}
.section-forest a{ color:var(--white); }
