/* ===========================================================================
   TOKI Hair Atelier — concept site (Saang Studio)

   Sibling of the LACQ nail concept, and deliberately NOT a reskin of it. A nail
   salon sells a set plus stacking add-ons; a hair salon sells a service whose
   price depends on two different axes — stylist tier for cutting, hair length
   for colour — plus work you can only show as a before/after pair. That shape
   difference is the whole point: a stylist has to see their own business here.

   HONESTY CONTRACT (req009): TOKI is a fictional business. Disclosed in FOUR
   places, both locales — ribbon, <title>/og:site_name, success panel, footer.

   NO NETWORK (req005): nothing here fetches. The booking wizard is canned.
   =========================================================================== */

const { useState, useEffect, useRef, useMemo, useCallback } = React;

const REDUCED = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);

/* ---- config ------------------------------------------------------------- */
const BRAND = "TOKI";
const BRAND_FULL = "TOKI Hair Atelier";
const DEFAULT_LANG = "en";
const PHONE_TEL = "tel:0000000000";       // placeholder — never a real number
const LINE_ID = null;
const SITE_URL = "https://toki-hair-concept.vercel.app";
const GA_ID = null;
const OG_LOCALE = { en: "en_US", th: "th_TH" };
const CLOSED_DOW = 2;                     // Tuesday (LACQ closes Mondays)

/* ---- i18n --------------------------------------------------------------- */
const DICT = {
  en: {
    ribbon: ["Concept site.", "TOKI is a fictional salon, built by Saang Studio to show what a hair salon's website can do."],
    nav: { work: "Work", menu: "Prices", team: "Stylists", visit: "Visit", book: "Book" },
    hero: {
      kicker: "Phrom Phong · consultation first",
      sub: "Precision cutting, Japanese straightening, and colour engineered for hair that's been coloured before. Every colour starts with a free consultation.",
      cta: "Book a chair", cta2: "See the work",
      caption: "Cut by Kenta",
    },
    work: {
      eyebrow: "The work", title: "Before, and after.",
      intro: "Colour correction is the whole job, and a single square in a grid can't show it. Here's what actually happened, and how long it took.",
      before: "Before", after: "After",
    },
    menu: {
      eyebrow: "Prices", title: "Two things set the price.",
      intro: "Cutting is priced by who cuts it. Colour is priced by how much hair there is. Nothing here needs a DM to find out.",
      cutting: "Cutting", colour: "Colour & chemistry", other: "Treatments",
      tiers: { stylist: "Stylist", senior: "Senior", director: "Director" },
      lengths: { short: "Short", medium: "Medium", long: "Long" },
      quote: "Quoted at consultation", free: "Free", mins: "min", from: "from",
      lengthNote: "Short is above the shoulder, medium to the collarbone, long past it.",
    },
    team: {
      eyebrow: "The stylists", title: "You book a person.",
      intro: "Tier sets the price for cutting. Colour is priced by length whoever does it.",
    },
    book: {
      eyebrow: "Booking", title: "Build your visit.",
      steps: ["Service", "Stylist", "When", "You"],
      back: "Back", next: "Continue", submit: "Request this booking",
      s1: "What are you having?", s1length: "How long is your hair?",
      s2: "Who with?", anyArtist: "No preference", anyArtistNote: "We'll give you whoever's free — priced at Stylist tier.",
      s3: "Which day?", s3slots: "And what time?", closedNote: "We're closed Tuesdays.",
      pickDay: "Pick a day to see times.",
      s4: "Where do we reach you?",
      name: "Your name", contact: "Phone or LINE ID", notes: "Anything we should know?",
      notesPh: "What's on your hair now, when you last coloured it, what you're growing out.",
      optional: "optional",
      required: "We need this one.",
      rail: "Your visit", railEmpty: "Pick a service to see the total.",
      duration: "About", from: "from", quote: "Quoted at consultation",
      legend: { open: "Free", hold: "Just taken", taken: "Booked" },
      okTitle: "Request received — in the demo.",
      okBody1: "TOKI Hair Atelier is a fictional salon. Saang Studio built this site to show how booking feels on a real salon's website. Nothing was sent, no data left your browser, and no appointment exists.",
      okBody2: "On a live site this request reaches the salon's LINE and CRM in seconds.",
      okCta: "Want this for your salon?",
      okRecap: "What you picked",
    },
    hygiene: {
      eyebrow: "Before we colour", title: "The consultation, and the patch test.",
      items: [
        ["Fifteen minutes, free", "Every colour starts with a consult. We look at what's on your hair now, what it'll take, and what it'll cost — before you're in the chair."],
        ["A patch test, 48 hours ahead", "First time colouring with us? We need a patch test two days before. It's quick, and it's the reason we can say yes to bleach safely."],
        ["Why prices say 'from'", "Two people asking for the same balayage need different amounts of product and time. The consult turns 'from' into a number."],
      ],
    },
    care: {
      eyebrow: "Timing", title: "Block out the right afternoon.",
      items: [
        ["Cut · 60 minutes", "Wash, cut and a rough dry. Add a blow-dry if you want to leave finished."],
        ["Root touch-up · 90 minutes", "In and out on a lunch break, if the lunch break is a long one."],
        ["Balayage · 3.5 hours", "Lightening, toning and a bond treatment. Bring something to read."],
        ["Straightening · 4 hours", "Japanese straightening is a long sit. We'll tell you honestly if your hair isn't ready."],
      ],
    },
    visit: {
      eyebrow: "Visit", title: "Sukhumvit 39, Phrom Phong.",
      hours: "Hours", hoursBody: "Wed–Mon, 10:00–20:00. Closed Tuesdays.",
      finding: "Finding us", findingBody: "Off Sukhumvit 39, a short walk from EmQuartier. Ring the bell — the door stays shut between appointments.",
      addressNote: "District only — TOKI is a concept, so there's no real address to send you to.",
      mapNote: "Tap a pin.",
      pins: { salon: "TOKI (concept — no real address)", bts: "BTS Phrom Phong", mall: "EmQuartier" },
      demoNote: "This is a concept site, so there's nowhere to navigate to. On a real build this button opens Google Maps.",
      directions: "Directions",
      faqTitle: "Before you book",
      faq: [
        ["How much for balayage?", "It depends on length — from ฿4,500 short to ฿7,800 long, and the consult turns that into a real number. That's the honest answer, and it's why it isn't in an Instagram bio."],
        ["I box-dyed my hair. Can you fix it?", "Usually. Colour correction is quoted at consultation because it depends entirely on what's on your hair and how much of it there is — anyone quoting you a flat price hasn't looked."],
        ["Do I have to see the Director?", "No. Cutting is priced by tier and all three tiers cut well; the Director's chair costs more because it books out further. Colour costs the same whoever does it."],
        ["Is this a real salon?", "No. TOKI is a concept built by Saang Studio to show what a hair salon's website can do. Nothing here can be booked."],
      ],
    },
    footer: {
      by: "A concept site by", byLink: "Saang Studio",
      disclosure: "TOKI Hair Atelier is a fictional business. No such salon exists, nothing on this page can be booked, and no prices here are any real salon's rates.",
      want: "Want one of these for your salon?",
      wantCta: "saangstudio.com/hair-salons",
    },
    sticky: { call: "Call", book: "Book" },
    money: (n) => "฿" + n.toLocaleString("en-US"),
  },
  th: {
    ribbon: ["เว็บตัวอย่าง", "TOKI เป็นร้านสมมติ Saang Studio ทำขึ้นเพื่อให้เห็นว่าเว็บไซต์ร้านทำผมทำอะไรได้บ้าง"],
    nav: { work: "ผลงาน", menu: "ราคา", team: "ช่าง", visit: "การเดินทาง", book: "จอง" },
    hero: {
      kicker: "พร้อมพงษ์ · ปรึกษาก่อนทุกครั้ง",
      sub: "ตัดแม่นทรง ยืดญี่ปุ่น และทำสีที่ออกแบบมาสำหรับผมที่เคยผ่านสีมาแล้ว งานสีทุกงานเริ่มจากการปรึกษาฟรี",
      cta: "จองคิว", cta2: "ดูผลงาน",
      caption: "ตัดโดยเคนตะ",
    },
    work: {
      eyebrow: "ผลงาน", title: "ก่อน และ หลัง",
      intro: "งานแก้สีคือหัวใจของร้าน และรูปเดียวในกริดเล่าไม่ได้ นี่คือสิ่งที่เกิดขึ้นจริง พร้อมเวลาที่ใช้",
      before: "ก่อน", after: "หลัง",
    },
    menu: {
      eyebrow: "ราคา", title: "ราคาขึ้นอยู่กับสองอย่าง",
      intro: "ค่าตัดคิดตามระดับช่าง ค่าสีคิดตามความยาวผม ไม่ต้องทักมาถามให้เสียเวลา",
      cutting: "ตัดผม", colour: "ทำสีและเคมี", other: "ทรีตเมนต์",
      tiers: { stylist: "ช่าง", senior: "ซีเนียร์", director: "ไดเรกเตอร์" },
      lengths: { short: "ผมสั้น", medium: "ผมกลาง", long: "ผมยาว" },
      quote: "ประเมินหน้างาน", free: "ฟรี", mins: "นาที", from: "เริ่มต้น",
      lengthNote: "ผมสั้นคือเหนือบ่า ผมกลางถึงกระดูกไหปลาร้า ผมยาวคือยาวกว่านั้น",
    },
    team: {
      eyebrow: "ช่างของเรา", title: "คุณเลือกช่างได้",
      intro: "ระดับช่างมีผลกับค่าตัด ส่วนค่าทำสีคิดตามความยาวผม ไม่ว่าช่างคนไหนทำ",
    },
    book: {
      eyebrow: "จองคิว", title: "จัดคิวของคุณ",
      steps: ["บริการ", "ช่าง", "วันเวลา", "ข้อมูล"],
      back: "ย้อนกลับ", next: "ถัดไป", submit: "ส่งคำขอจอง",
      s1: "จะทำอะไรดี", s1length: "ผมยาวประมาณไหน",
      s2: "อยากทำกับช่างคนไหน", anyArtist: "ช่างคนไหนก็ได้", anyArtistNote: "เราจะจัดช่างที่ว่างให้ คิดราคาระดับช่าง",
      s3: "วันไหนดี", s3slots: "แล้วกี่โมง", closedNote: "วันอังคารร้านปิด",
      pickDay: "เลือกวันก่อน แล้วจะเห็นเวลาที่ว่าง",
      s4: "ติดต่อคุณยังไง",
      name: "ชื่อคุณ", contact: "เบอร์โทร หรือ LINE ID", notes: "มีอะไรอยากบอกช่างไหม",
      notesPh: "ตอนนี้ผมทำอะไรมาบ้าง ทำสีล่าสุดเมื่อไหร่ กำลังปล่อยยาวอยู่หรือเปล่า",
      optional: "ไม่บังคับ",
      required: "กรอกช่องนี้ด้วยนะ",
      rail: "คิวของคุณ", railEmpty: "เลือกบริการเพื่อดูราคารวม",
      duration: "ประมาณ", from: "เริ่มต้น", quote: "ประเมินหน้างาน",
      legend: { open: "ว่าง", hold: "เพิ่งถูกจอง", taken: "เต็ม" },
      okTitle: "รับคำขอแล้ว — ในเดโม",
      okBody1: "TOKI Hair Atelier เป็นร้านสมมติ Saang Studio สร้างเว็บไซต์นี้ขึ้นเพื่อแสดงให้เห็นว่าระบบจองบนเว็บไซต์ร้านจริงทำงานอย่างไร ไม่มีข้อมูลใดถูกส่งออกจากเบราว์เซอร์ของคุณ และไม่มีการจองเกิดขึ้นจริง",
      okBody2: "บนเว็บไซต์จริง คำขอนี้จะเข้าไปที่ LINE และระบบ CRM ของร้านภายในไม่กี่วินาที",
      okCta: "อยากได้แบบนี้สำหรับร้านคุณไหม",
      okRecap: "สิ่งที่คุณเลือก",
    },
    hygiene: {
      eyebrow: "ก่อนทำสี", title: "ปรึกษาก่อน และทดสอบการแพ้",
      items: [
        ["สิบห้านาที ฟรี", "งานสีทุกงานเริ่มจากการปรึกษา เราดูว่าตอนนี้ผมคุณมีอะไรอยู่ ต้องทำอะไรบ้าง และราคาเท่าไหร่ ก่อนคุณจะนั่งลงบนเก้าอี้"],
        ["ทดสอบการแพ้ ล่วงหน้า 48 ชั่วโมง", "ถ้าทำสีกับเราครั้งแรก ต้องทดสอบการแพ้ก่อนสองวัน ใช้เวลาไม่นาน และเป็นเหตุผลที่เรากล้ารับงานฟอกอย่างปลอดภัย"],
        ["ทำไมราคาถึงเขียนว่า “เริ่มต้น”", "คนสองคนที่ขอบาลายาจแบบเดียวกัน ใช้ปริมาณยาและเวลาไม่เท่ากัน การปรึกษาจะเปลี่ยน “เริ่มต้น” ให้เป็นตัวเลขจริง"],
      ],
    },
    care: {
      eyebrow: "เวลาที่ใช้", title: "เผื่อเวลาให้ถูกช่วง",
      items: [
        ["ตัด · 60 นาที", "สระ ตัด และเป่าหยาบ ถ้าอยากออกจากร้านแบบเซ็ตเสร็จ เพิ่มเป่าจัดทรงได้"],
        ["เก็บโคน · 90 นาที", "พักเที่ยงยาวๆ ก็ทันอยู่"],
        ["บาลายาจ · 3.5 ชั่วโมง", "ฟอก ลงโทนเนอร์ และทรีตเมนต์ซ่อมพันธะ เอาอะไรมาอ่านด้วยก็ดี"],
        ["ยืดญี่ปุ่น · 4 ชั่วโมง", "ต้องนั่งนาน ถ้าสภาพผมยังไม่พร้อม เราจะบอกตรงๆ"],
      ],
    },
    visit: {
      eyebrow: "การเดินทาง", title: "สุขุมวิท 39 พร้อมพงษ์",
      hours: "เวลาเปิด", hoursBody: "พุธ–จันทร์ 10:00–20:00 ปิดวันอังคาร",
      finding: "หาร้านยังไง", findingBody: "อยู่ในซอยสุขุมวิท 39 เดินจาก EmQuartier ไม่ไกล กดกริ่งเรียกได้เลย ประตูปิดไว้ระหว่างคิว",
      addressNote: "บอกแค่ย่าน เพราะ TOKI เป็นร้านสมมติ ไม่มีที่อยู่จริงให้ไป",
      mapNote: "กดที่หมุด",
      pins: { salon: "TOKI (ร้านสมมติ ไม่มีที่อยู่จริง)", bts: "BTS พร้อมพงษ์", mall: "EmQuartier" },
      demoNote: "นี่คือเว็บตัวอย่าง จึงไม่มีที่ให้นำทางไป ถ้าเป็นเว็บจริง ปุ่มนี้จะเปิด Google Maps",
      directions: "เส้นทาง",
      faqTitle: "ก่อนจอง",
      faq: [
        ["บาลายาจราคาเท่าไหร่", "ขึ้นอยู่กับความยาวผม เริ่มต้น ฿4,500 สำหรับผมสั้น ถึง ฿7,800 สำหรับผมยาว แล้วการปรึกษาจะเปลี่ยนเป็นตัวเลขจริง นี่คือคำตอบที่ตรงที่สุด และเป็นเหตุผลที่ใส่ไว้ในไบโอ Instagram ไม่ได้"],
        ["ย้อมกล่องมาเอง แก้ได้ไหม", "ส่วนใหญ่ได้ งานแก้สีต้องประเมินหน้างาน เพราะขึ้นอยู่กับว่าผมคุณมีอะไรอยู่และมีมากแค่ไหน ใครที่บอกราคาตายตัวได้เลย แปลว่ายังไม่ได้ดูผมคุณ"],
        ["ต้องทำกับไดเรกเตอร์ไหม", "ไม่ต้อง ค่าตัดคิดตามระดับช่าง และช่างทั้งสามระดับตัดดีทั้งหมด เก้าอี้ไดเรกเตอร์แพงกว่าเพราะคิวเต็มไกลกว่า ส่วนค่าทำสีเท่ากันไม่ว่าใครทำ"],
        ["ร้านนี้มีอยู่จริงไหม", "ไม่มี TOKI เป็นร้านสมมติที่ Saang Studio ทำขึ้นเพื่อให้เห็นว่าเว็บไซต์ร้านทำผมทำอะไรได้บ้าง จองจริงไม่ได้"],
      ],
    },
    footer: {
      by: "เว็บตัวอย่างโดย", byLink: "Saang Studio",
      disclosure: "TOKI Hair Atelier เป็นร้านสมมติ ไม่มีร้านนี้อยู่จริง จองอะไรบนหน้านี้ไม่ได้ และราคาทั้งหมดไม่ใช่ราคาของร้านใดร้านหนึ่ง",
      want: "อยากได้แบบนี้สำหรับร้านคุณไหม",
      wantCta: "saangstudio.com/hair-salons",
    },
    sticky: { call: "โทร", book: "จอง" },
    money: (n) => "฿" + n.toLocaleString("en-US"),
  },
};

/* ---- data — the ONLY source of prices (req010 asserts this) --------------
   Two axes, on purpose. `tier` services are priced by who does them; `length`
   services by how much hair there is. That two-axis shape is what makes this
   read as a hair menu rather than a generic price list.
   Bands and sources: PRD.md §3. Cite the band, never a salon. */
const TIERS = ["stylist", "senior", "director"];
const LENGTHS = ["short", "medium", "long"];

const SERVICES = [
  { id: "cut",     group: "cutting", mode: "tier",   min: 60,
    tiers: { stylist: 900, senior: 1400, director: 2200 },
    en: ["Cut", "Wash, cut and a rough dry."], th: ["ตัดผม", "สระ ตัด และเป่าหยาบ"] },
  { id: "cutblow", group: "cutting", mode: "tier",   min: 90,
    tiers: { stylist: 1300, senior: 1800, director: 2600 },
    en: ["Cut & blow-dry", "Leave finished rather than damp."], th: ["ตัด + เป่าจัดทรง", "ออกจากร้านแบบเซ็ตเสร็จ"] },
  { id: "blowdry", group: "colour",  mode: "length", min: 45,
    lengths: { short: 400, medium: 550, long: 750 },
    en: ["Wash & blow-dry", "No cut, just finished."], th: ["สระ + เป่า", "ไม่ตัด แค่จัดทรง"] },
  { id: "roots",   group: "colour",  mode: "length", min: 90,
    lengths: { short: 1900, medium: 2200, long: 2600 },
    en: ["Root touch-up", "Regrowth only, matched to your existing colour."], th: ["เก็บโคนผม", "เฉพาะโคนที่งอก ให้เข้ากับสีเดิม"] },
  { id: "colour",  group: "colour",  mode: "length", min: 150,
    lengths: { short: 2600, medium: 3400, long: 4600 },
    en: ["Full colour", "Roots to ends, one tone."], th: ["ทำสีทั้งหัว", "โคนถึงปลาย โทนเดียว"] },
  { id: "balayage", group: "colour", mode: "length", min: 210,
    lengths: { short: 4500, medium: 6000, long: 7800 },
    en: ["Balayage & highlights", "Hand-painted lift, toned and treated."], th: ["บาลายาจ & ไฮไลต์", "เพนต์มือ ลงโทนเนอร์ และทรีตเมนต์"] },
  { id: "straighten", group: "colour", mode: "length", min: 240,
    lengths: { short: 4500, medium: 6500, long: 9000 },
    en: ["Japanese straightening", "A long sit. Permanent, so we're honest about whether your hair is ready."], th: ["ยืดญี่ปุ่น", "ต้องนั่งนาน ทำแล้วอยู่ถาวร เราจะบอกตรงๆ ว่าผมพร้อมไหม"] },
  // The honest mechanism: a real menu says "quoted" where a number would be a lie.
  { id: "correction", group: "colour", mode: "quote", min: 240,
    en: ["Colour correction", "Box dye, banding, a grow-out gone wrong."], th: ["แก้สีผม", "ย้อมกล่อง สีเป็นชั้น หรือสีที่ปล่อยยาวแล้วเพี้ยน"] },
  { id: "bond",    group: "other", mode: "flat", min: 30, thb: 1500,
    en: ["Bond treatment", "Added to any lightening service."], th: ["ทรีตเมนต์ซ่อมพันธะ", "เสริมกับงานฟอกได้ทุกแบบ"] },
  { id: "scalp",   group: "other", mode: "flat", min: 45, thb: 1200,
    en: ["Scalp spa", "Deep cleanse and a long massage."], th: ["สปาหนังศีรษะ", "ทำความสะอาดล้ำลึกและนวดยาวๆ"] },
  { id: "consult", group: "other", mode: "free", min: 15, thb: 0,
    en: ["Consultation", "Fifteen minutes, before any colour."], th: ["ปรึกษา", "สิบห้านาที ก่อนทำสีทุกครั้ง"] },
];

const STYLISTS = [
  { id: "kenta", tier: "director", initials: "K", en: ["Kenta", "Director", "Trained in Tokyo. Precision cutting and Japanese straightening."], th: ["เคนตะ", "ไดเรกเตอร์", "เรียนมาจากโตเกียว ถนัดตัดแม่นทรงและยืดญี่ปุ่น"] },
  { id: "aoi",   tier: "senior",   initials: "A", en: ["Aoi", "Senior", "Colour correction. Nine years of undoing other people's box dye."], th: ["อาโออิ", "ซีเนียร์", "งานแก้สี เก้าปีกับการแก้สีย้อมกล่องของคนอื่น"] },
  { id: "bank",  tier: "stylist",  initials: "B", en: ["Bank", "Stylist", "Soft balayage on previously-coloured Asian hair."], th: ["แบงค์", "ช่าง", "บาลายาจโทนนุ่มบนผมเอเชียที่เคยผ่านสีมาแล้ว"] },
  { id: "nan",   tier: "stylist",  initials: "N", en: ["Nan", "Stylist", "Short cuts and fringes. Fast hands, careful eye."], th: ["แนน", "ช่าง", "ผมสั้นและหน้าม้า มือไว ตาละเอียด"] },
];

// Before/after pairs — the hair-specific proof. A flat gallery can't show
// colour correction, which is the thing this salon actually sells.
const PAIRS = [
  { id: "p1", stylist: "aoi",   hours: "4.5", en: ["Box-dyed black → ash brown balayage", "Two lightening passes, toned cool, bond treatment through."], th: ["ย้อมกล่องสีดำ → บาลายาจน้ำตาลประกายเถ้า", "ฟอกสองรอบ ลงโทนเย็น และทรีตเมนต์ซ่อมพันธะ"] },
  { id: "p2", stylist: "kenta", hours: "4",   en: ["Grown-out perm → Japanese straightening", "Straightened, then cut into a blunt collarbone line."], th: ["ดัดที่ปล่อยยาว → ยืดญี่ปุ่น", "ยืดแล้วตัดเป็นทรงตรงระดับไหปลาร้า"] },
  { id: "p3", stylist: "aoi",   hours: "3",   en: ["Banded roots → single-tone correction", "Colour evened root to end, no line left showing."], th: ["โคนเป็นชั้น → แก้ให้เป็นโทนเดียว", "ปรับสีให้เสมอกันตั้งแต่โคนถึงปลาย ไม่เหลือรอยต่อ"] },
  { id: "p4", stylist: "bank",  hours: "3.5", en: ["Faded copper → warm caramel", "Refreshed and deepened, glossed to finish."], th: ["สีคอปเปอร์ที่ซีด → คาราเมลโทนอุ่น", "เติมสีให้เข้มขึ้นและเคลือบเงาปิดท้าย"] },
];

window.__DATA = { SERVICES, STYLISTS, PAIRS, TIERS, LENGTHS, CLOSED_DOW };

/* ---- helpers ------------------------------------------------------------ */
function initialLang() {
  const q = new URLSearchParams(location.search).get("lang");
  if (q === "en" || q === "th") return q;
  try { const s = localStorage.getItem("toki_lang"); if (s === "en" || s === "th") return s; } catch (e) {}
  return DEFAULT_LANG;
}

function enhanceSeo(lang) {
  document.documentElement.lang = lang;
  const set = (sel, attr, val) => { const el = document.querySelector(sel); if (el) el.setAttribute(attr, val); };
  const url = lang === DEFAULT_LANG ? SITE_URL + "/" : SITE_URL + "/?lang=" + lang;
  set('link[rel="canonical"]', "href", url);
  set('meta[property="og:url"]', "content", url);
  set('meta[property="og:locale"]', "content", OG_LOCALE[lang]);
}

/** PURE — same (dateIso, stylistId, slotIndex) always gives the same state. */
function slotState(dateIso, stylistId, i) {
  const seed = [...(dateIso + "|" + stylistId)].reduce((a, c) => (a * 31 + c.charCodeAt(0)) >>> 0, 7);
  const v = ((seed + i * 2654435761) >>> 0) % 100;
  return v < 34 ? "taken" : v < 42 ? "hold" : "open";
}

const SLOT_START = 10 * 60, SLOT_END = 20 * 60, SLOT_STEP = 30;
const SLOTS = (() => { const o = []; for (let m = SLOT_START; m < SLOT_END; m += SLOT_STEP) o.push(m); return o; })();
const hhmm = (m) => String(Math.floor(m / 60)).padStart(2, "0") + ":" + String(m % 60).padStart(2, "0");
const iso = (d) => d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
function nextDays(n) {
  const out = []; const base = new Date(); base.setHours(0, 0, 0, 0);
  for (let i = 0; i < n; i++) { const d = new Date(base); d.setDate(base.getDate() + i); out.push(d); }
  return out;
}
const svcById = (id) => SERVICES.find((s) => s.id === id);
const tierOf = (stylistId) => { const s = STYLISTS.find((x) => x.id === stylistId); return s ? s.tier : "stylist"; };

/**
 * The two-axis price resolver. Returns null for "quoted at consultation",
 * which is the honest answer for colour correction and is exactly what a real
 * salon menu does.
 */
function priceOf(serviceId, length, tier) {
  const s = svcById(serviceId); if (!s) return 0;
  if (s.mode === "quote") return null;
  if (s.mode === "free") return 0;
  if (s.mode === "flat") return s.thb;
  if (s.mode === "tier") return s.tiers[tier || "stylist"];
  return s.lengths[length || "short"];
}
const durationText = (min) => { const h = Math.floor(min / 60), m = min % 60; return ((h ? h + " h " : "") + (m ? m + " m" : "")).trim(); };
function goto(id) { const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: REDUCED ? "auto" : "smooth", block: "start" }); }

/* ---- placeholder art (see assets/PROVENANCE.md for the shot list) ------- */
function Placeholder({ label, seedIndex = 0, tone = 0 }) {
  const bands = ["#4A6357", "#8FA69A", "#C9BBA8", "#262927"];
  const a = bands[seedIndex % bands.length], b = bands[(seedIndex + tone + 1) % bands.length];
  return (
    <svg viewBox="0 0 300 400" role="img" aria-label={label} preserveAspectRatio="xMidYMid slice">
      <rect width="300" height="400" fill="#262927" />
      <rect x="0" y="120" width="300" height="280" fill={a} opacity="0.34" />
      <circle cx="150" cy="150" r="96" fill={b} opacity="0.28" />
      <text x="20" y="378" fill="#EFEFEA" opacity="0.8"
            style={{ font: "300 15px var(--display)", letterSpacing: ".3em" }}>TOKI</text>
    </svg>
  );
}

function Reveal({ children, className = "", tag: Tag = "div", id }) {
  const ref = useRef(null);
  const [seen, setSeen] = useState(REDUCED);
  useEffect(() => {
    if (REDUCED || !ref.current) return;
    const io = new IntersectionObserver((es) => es.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } }), { rootMargin: "0px 0px -8% 0px" });
    io.observe(ref.current);
    const failsafe = setTimeout(() => setSeen(true), 2500);
    return () => { io.disconnect(); clearTimeout(failsafe); };
  }, []);
  return <Tag id={id} ref={ref} className={"reveal " + (seen ? "is-in " : "") + className}>{children}</Tag>;
}

/* ---- sections ----------------------------------------------------------- */
function Ribbon({ t }) {   // req009: disclosure #1 of 4
  return <div className="ribbon" data-concept-ribbon><strong>{t.ribbon[0]}</strong> {t.ribbon[1]}</div>;
}

function Nav({ t, lang, toggleLang }) {
  return (
    <nav className="nav">
      <a className="nav__name" href="#top" onClick={(e) => { e.preventDefault(); goto("top"); }}>{BRAND}</a>
      <div className="nav__links">
        {["work", "menu", "team", "visit"].map((k) => (
          <a key={k} href={"#" + k} onClick={(e) => { e.preventDefault(); goto(k); }}>{t.nav[k]}</a>
        ))}
      </div>
      <div className="nav__right">
        <button className="langtoggle" onClick={toggleLang} aria-label="Switch language">{lang === "en" ? "ไทย" : "EN"}</button>
        <button className="btn btn--accent" onClick={() => goto("book")}>{t.nav.book}</button>
      </div>
    </nav>
  );
}

function Hero({ t }) {
  return (
    <header className="hero" id="top">
      <div className="wrap hero__grid">
        <div>
          <p className="eyebrow">{t.hero.kicker}</p>
          <h1 className="hero__name">{BRAND_FULL}</h1>
          <p className="hero__sub">{t.hero.sub}</p>
          <div className="hero__cta">
            <button className="btn btn--accent" onClick={() => goto("book")}>{t.hero.cta}</button>
            <button className="btn btn--ghost" onClick={() => goto("work")}>{t.hero.cta2}</button>
          </div>
        </div>
        <div>
          <div className="hero__media"><Placeholder label={BRAND_FULL} seedIndex={0} /></div>
          {/* The hair-specific hook: the stylist is named in the caption. */}
          <p className="hero__caption">{t.hero.caption}</p>
        </div>
      </div>
    </header>
  );
}

function Work({ t, lang }) {
  return (
    <section className="section" id="work">
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{t.work.eyebrow}</p>
          <h2>{t.work.title}</h2>
          <p className="lead muted">{t.work.intro}</p>
        </Reveal>
        <div className="pairs" style={{ marginTop: 36 }}>
          {PAIRS.map((p, i) => {
            const stylist = STYLISTS.find((s) => s.id === p.stylist);
            return (
              <Reveal key={p.id}>
                <figure style={{ margin: 0 }} data-pair={p.id}>
                  <div className="pair__imgs">
                    <div className="pair__fig">
                      <Placeholder label={t.work.before} seedIndex={i} tone={0} />
                      <span className="pair__tag">{t.work.before}</span>
                    </div>
                    <div className="pair__fig">
                      <Placeholder label={t.work.after} seedIndex={i} tone={2} />
                      <span className="pair__tag">{t.work.after}</span>
                    </div>
                  </div>
                  <figcaption>
                    <h3 style={{ fontSize: 17, marginTop: 14, textTransform: "none", letterSpacing: 0 }}>{p[lang][0]}</h3>
                    <p className="pair__cap">{p[lang][1]} · {stylist[lang][0]} · {p.hours} h</p>
                  </figcaption>
                </figure>
              </Reveal>
            );
          })}
        </div>
      </div>
    </section>
  );
}

/** The tier × length matrix — two different axes in one menu. */
function Menu({ t, lang }) {
  const cutting = SERVICES.filter((s) => s.group === "cutting");
  const colour = SERVICES.filter((s) => s.group === "colour");
  const other = SERVICES.filter((s) => s.group === "other");
  const cell = (s, key, axis) => {
    if (s.mode === "quote") return <td key={key} className="matrix__quote" colSpan={3}>{t.menu.quote}</td>;
    const v = axis === "tier" ? s.tiers[key] : s.lengths[key];
    return <td key={key}>{t.money(v)}</td>;
  };
  return (
    <section className="section section--dark" id="menu">
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{t.menu.eyebrow}</p>
          <h2>{t.menu.title}</h2>
          <p className="lead muted">{t.menu.intro}</p>
        </Reveal>

        <h3 style={{ fontSize: 14, margin: "40px 0 10px" }}>{t.menu.cutting}</h3>
        <div className="matrix-wrap">
          <table className="matrix" data-matrix="tier">
            <thead><tr><th /> {TIERS.map((k) => <th key={k}>{t.menu.tiers[k]}</th>)}</tr></thead>
            <tbody>
              {cutting.map((s) => (
                <tr key={s.id} data-price-row={s.id}>
                  <th scope="row"><span className="matrix__name">{s[lang][0]}</span><span className="matrix__meta">{s[lang][1]} · {s.min} {t.menu.mins}</span></th>
                  {TIERS.map((k) => cell(s, k, "tier"))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>

        <h3 style={{ fontSize: 14, margin: "40px 0 10px" }}>{t.menu.colour}</h3>
        <div className="matrix-wrap">
          <table className="matrix" data-matrix="length">
            <thead><tr><th /> {LENGTHS.map((k) => <th key={k}>{t.menu.lengths[k]}</th>)}</tr></thead>
            <tbody>
              {colour.map((s) => (
                <tr key={s.id} data-price-row={s.id}>
                  <th scope="row"><span className="matrix__name">{s[lang][0]}</span><span className="matrix__meta">{s[lang][1]} · {s.min} {t.menu.mins}</span></th>
                  {s.mode === "quote" ? cell(s, "q", "length") : LENGTHS.map((k) => cell(s, k, "length"))}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <p className="muted" style={{ fontSize: 14, marginTop: 10 }}>{t.menu.lengthNote}</p>

        <h3 style={{ fontSize: 14, margin: "40px 0 10px" }}>{t.menu.other}</h3>
        <div className="matrix-wrap">
          <table className="matrix">
            <tbody>
              {other.map((s) => (
                <tr key={s.id} data-price-row={s.id}>
                  <th scope="row"><span className="matrix__name">{s[lang][0]}</span><span className="matrix__meta">{s[lang][1]} · {s.min} {t.menu.mins}</span></th>
                  <td>{s.mode === "free" ? t.menu.free : t.money(s.thb)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  );
}

function Team({ t, lang }) {
  return (
    <section className="section" id="team">
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{t.team.eyebrow}</p>
          <h2>{t.team.title}</h2>
          <p className="lead muted">{t.team.intro}</p>
        </Reveal>
        <div className="roster" style={{ marginTop: 34 }}>
          {STYLISTS.map((s, i) => (
            <Reveal key={s.id}>
              {/* Face-cropped by design — see PRD §5. */}
              <div className="artist__media"><Placeholder label={s[lang][0]} seedIndex={i + 1} /></div>
              <div className="artist__mono">{s.initials}</div>
              <h3 style={{ fontSize: 19, textTransform: "none", letterSpacing: 0 }}>{s[lang][0]}</h3>
              <span className="artist__tier">{t.menu.tiers[s.tier]}</span>
              <p className="muted" style={{ marginTop: 10, fontSize: 15 }}>{s[lang][2]}</p>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
}

function Band({ id, block, dark }) {
  return (
    <section className={"section" + (dark ? " section--dark" : "")} id={id}>
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{block.eyebrow}</p>
          <h2>{block.title}</h2>
        </Reveal>
        <div className={"grid " + (block.items.length === 4 ? "grid--2" : "grid--3")} style={{ marginTop: 30 }}>
          {block.items.map(([h, b]) => (
            <Reveal className="card" key={h}>
              <h3 style={{ fontSize: 18, marginBottom: 8, textTransform: "none", letterSpacing: 0 }}>{h}</h3>
              <p className="muted" style={{ margin: 0, fontSize: 15 }}>{b}</p>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
}

/* ---- booking wizard: canned, posts nowhere (req005) --------------------- */
function SummaryRail({ t, lang, state }) {
  const s = svcById(state.serviceId);
  if (!s) return <aside className="rail"><h3>{t.book.rail}</h3><p className="rail__empty">{t.book.railEmpty}</p></aside>;
  const tier = tierOf(state.stylistId);
  const thb = priceOf(state.serviceId, state.length, tier);
  const stylist = STYLISTS.find((x) => x.id === state.stylistId);
  return (
    <aside className="rail" data-rail>
      <h3>{t.book.rail}</h3>
      <div className="rail__line"><span>{s[lang][0]}</span><span>{thb === null ? "" : t.money(thb)}</span></div>
      {s.mode === "length" && state.length && (
        <div className="rail__line"><span>{t.menu.lengths[state.length]}</span><span /></div>
      )}
      {s.mode === "tier" && <div className="rail__line"><span>{t.menu.tiers[tier]}</span><span /></div>}
      <hr className="rail__rule" />
      <div className="rail__line"><span>{t.book.duration}</span><span>{durationText(s.min)}</span></div>
      <div className="rail__line">
        <span>{thb === null ? "" : t.book.from}</span>
        <span className="rail__total" data-rail-total>{thb === null ? t.book.quote : t.money(thb)}</span>
      </div>
      {stylist && <div className="rail__line"><span>{stylist[lang][0]}</span><span /></div>}
      {state.date && <div className="rail__line"><span>{state.date}</span><span>{state.slot != null ? hhmm(state.slot) : ""}</span></div>}
    </aside>
  );
}

function Book({ t, lang }) {
  const [step, setStep] = useState(0);
  const [done, setDone] = useState(false);
  const [err, setErr] = useState({});
  const [state, setState] = useState({ serviceId: "", length: "", stylistId: "", date: "", slot: null, name: "", contact: "", notes: "" });
  const days = useMemo(() => nextDays(14), []);
  const set = (patch) => setState((s) => ({ ...s, ...patch }));
  const svc = svcById(state.serviceId);
  const needsLength = svc && svc.mode === "length";

  const canNext = [
    () => !!state.serviceId && (!needsLength || !!state.length),
    () => state.stylistId !== "",
    () => !!state.date && state.slot != null,
    () => true,
  ][step];

  function submit(e) {
    e.preventDefault();
    if (e.target.company_url && e.target.company_url.value) { setDone(true); return; }   // honeypot
    const next = {};
    if (!state.name.trim()) next.name = true;
    if (!state.contact.trim()) next.contact = true;
    setErr(next);
    if (Object.keys(next).length) return;
    setDone(true);   // canned. Nothing is sent anywhere.
  }

  if (done) {
    const tier = tierOf(state.stylistId);
    const thb = priceOf(state.serviceId, state.length, tier);
    const stylist = STYLISTS.find((x) => x.id === state.stylistId);
    return (
      <section className="section" id="book">
        <div className="wrap">
          <div className="book__panel done" data-booking-done>
            <div className="done__mark" aria-hidden="true">✓</div>
            {/* req009: disclosure #3 of 4. */}
            <h2 style={{ fontSize: 24 }}>{t.book.okTitle}</h2>
            <p className="muted">{t.book.okBody1}</p>
            <p className="muted">{t.book.okBody2}</p>
            <div className="done__recap">
              <p className="eyebrow" style={{ margin: "0 0 8px" }}>{t.book.okRecap}</p>
              <div className="rail__line"><span>{svc[lang][0]}{needsLength ? " · " + t.menu.lengths[state.length] : ""}</span>
                <span>{thb === null ? t.book.quote : t.money(thb)}</span></div>
              <div className="rail__line"><span>{stylist ? stylist[lang][0] + " · " + t.menu.tiers[tier] : t.book.anyArtist}</span>
                <span>{state.date} {state.slot != null ? hhmm(state.slot) : ""}</span></div>
              <div className="rail__line"><span>{t.book.duration} {durationText(svc.min)}</span><span /></div>
            </div>
            <p style={{ marginTop: 22 }}>
              <a className="btn btn--accent" href="https://saangstudio.com/en/hair-salons">{t.book.okCta}</a>
            </p>
          </div>
        </div>
      </section>
    );
  }

  return (
    <section className="section" id="book">
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{t.book.eyebrow}</p>
          <h2>{t.book.title}</h2>
        </Reveal>
        <div className="book" style={{ marginTop: 30 }}>
          <form className="book__panel" onSubmit={submit} noValidate>
            <ol className="book__steps">
              {t.book.steps.map((label, i) => (
                <li className="book__step" key={label} aria-current={i === step ? "step" : undefined}>{i + 1}. {label}{i < 3 ? " ·" : ""}</li>
              ))}
            </ol>

            {step === 0 && (
              <div data-step="1">
                <h3 style={{ fontSize: 18, marginBottom: 12, textTransform: "none", letterSpacing: 0 }}>{t.book.s1}</h3>
                {SERVICES.map((s) => (
                  <label className="opt" key={s.id}>
                    <input type="radio" name="service" value={s.id} checked={state.serviceId === s.id}
                           onChange={() => set({ serviceId: s.id, length: "" })} />
                    <span className="opt__body">
                      <span className="opt__name">{s[lang][0]}</span>
                      <span className="opt__meta"> · {s.min} {t.menu.mins}</span>
                      <div className="opt__meta">{s[lang][1]}</div>
                    </span>
                    <span className="opt__thb">
                      {s.mode === "quote" ? t.menu.quote
                        : s.mode === "free" ? t.menu.free
                        : s.mode === "flat" ? t.money(s.thb)
                        : t.menu.from + " " + t.money(s.mode === "tier" ? s.tiers.stylist : s.lengths.short)}
                    </span>
                  </label>
                ))}
                {needsLength && (
                  <div data-length-picker>
                    <h3 style={{ fontSize: 18, margin: "22px 0 12px", textTransform: "none", letterSpacing: 0 }}>{t.book.s1length}</h3>
                    {LENGTHS.map((k) => (
                      <label className="opt" key={k}>
                        <input type="radio" name="length" value={k} checked={state.length === k} onChange={() => set({ length: k })} />
                        <span className="opt__body"><span className="opt__name">{t.menu.lengths[k]}</span></span>
                        <span className="opt__thb">{t.money(svc.lengths[k])}</span>
                      </label>
                    ))}
                    <p className="opt__meta">{t.menu.lengthNote}</p>
                  </div>
                )}
              </div>
            )}

            {step === 1 && (
              <div data-step="2">
                <h3 style={{ fontSize: 18, marginBottom: 12, textTransform: "none", letterSpacing: 0 }}>{t.book.s2}</h3>
                {/* Picking a stylist also picks their tier, which re-prices a cut. */}
                {STYLISTS.map((s) => (
                  <label className="opt" key={s.id}>
                    <input type="radio" name="stylist" value={s.id} checked={state.stylistId === s.id} onChange={() => set({ stylistId: s.id })} />
                    <span className="opt__body">
                      <span className="opt__name">{s[lang][0]}</span>
                      <span className="opt__meta"> · {t.menu.tiers[s.tier]}</span>
                      <div className="opt__meta">{s[lang][2]}</div>
                    </span>
                    {svc && svc.mode === "tier" && <span className="opt__thb">{t.money(svc.tiers[s.tier])}</span>}
                  </label>
                ))}
                <label className="opt">
                  <input type="radio" name="stylist" value="any" checked={state.stylistId === "any"} onChange={() => set({ stylistId: "any" })} />
                  <span className="opt__body"><span className="opt__name">{t.book.anyArtist}</span><div className="opt__meta">{t.book.anyArtistNote}</div></span>
                </label>
              </div>
            )}

            {step === 2 && (
              <div data-step="3">
                <h3 style={{ fontSize: 18, marginBottom: 12, textTransform: "none", letterSpacing: 0 }}>{t.book.s3}</h3>
                <div className="datestrip">
                  {days.map((d) => {
                    const closed = d.getDay() === CLOSED_DOW;
                    const key = iso(d);
                    return (
                      <button type="button" key={key} className="datebtn" disabled={closed}
                              data-today={key === iso(new Date()) ? "true" : undefined}
                              aria-pressed={state.date === key}
                              onClick={() => set({ date: key, slot: null })}>
                        <span className="datebtn__dow">{d.toLocaleDateString(lang === "th" ? "th-TH" : "en-GB", { weekday: "short" })}</span>
                        <span className="datebtn__num">{d.getDate()}</span>
                      </button>
                    );
                  })}
                </div>
                <p className="opt__meta" style={{ marginTop: 8 }}>{t.book.closedNote}</p>
                <h3 style={{ fontSize: 18, margin: "20px 0 8px", textTransform: "none", letterSpacing: 0 }}>{t.book.s3slots}</h3>
                {!state.date ? <p className="muted">{t.book.pickDay}</p> : (
                  <>
                    <div className="slots" data-slots>
                      {SLOTS.map((m, i) => {
                        const st = slotState(state.date, state.stylistId || "any", i);
                        const now = new Date();
                        const past = state.date === iso(now) && m <= now.getHours() * 60 + now.getMinutes();
                        return (
                          <button type="button" key={m} disabled={st !== "open" || past}
                                  className={"slot" + (st === "hold" ? " slot--hold" : "")}
                                  data-slot-state={past ? "past" : st}
                                  aria-pressed={state.slot === m}
                                  onClick={() => set({ slot: m })}>{hhmm(m)}</button>
                        );
                      })}
                    </div>
                    <div className="slots__legend">
                      <span>● {t.book.legend.open}</span><span>◌ {t.book.legend.hold}</span><span>× {t.book.legend.taken}</span>
                    </div>
                  </>
                )}
              </div>
            )}

            {step === 3 && (
              <div data-step="4">
                <h3 style={{ fontSize: 18, marginBottom: 12, textTransform: "none", letterSpacing: 0 }}>{t.book.s4}</h3>
                <div className="field">
                  <label htmlFor="bk-name">{t.book.name}</label>
                  <input id="bk-name" name="name" value={state.name} onChange={(e) => set({ name: e.target.value })} />
                  {err.name && <div className="err">{t.book.required}</div>}
                </div>
                <div className="field">
                  <label htmlFor="bk-contact">{t.book.contact}</label>
                  <input id="bk-contact" name="contact" value={state.contact} onChange={(e) => set({ contact: e.target.value })} />
                  {err.contact && <div className="err">{t.book.required}</div>}
                </div>
                <div className="field">
                  <label htmlFor="bk-notes">{t.book.notes} <span className="opt__meta">({t.book.optional})</span></label>
                  <textarea id="bk-notes" name="notes" rows="3" placeholder={t.book.notesPh}
                            value={state.notes} onChange={(e) => set({ notes: e.target.value })} />
                </div>
                <div className="hp" aria-hidden="true">
                  <label htmlFor="company_url">Company URL</label>
                  <input id="company_url" name="company_url" tabIndex="-1" autoComplete="off" />
                </div>
              </div>
            )}

            <div className="book__nav">
              <button type="button" className="btn btn--ghost" disabled={step === 0} onClick={() => setStep(step - 1)}>{t.book.back}</button>
              {step < 3
                ? <button type="button" className="btn btn--accent" disabled={!canNext()} onClick={() => setStep(step + 1)}>{t.book.next}</button>
                : <button type="submit" className="btn btn--accent">{t.book.submit}</button>}
            </div>
          </form>
          <SummaryRail t={t} lang={lang} state={state} />
        </div>
      </div>
    </section>
  );
}

function Visit({ t }) {
  const [note, setNote] = useState("");
  const pins = [
    { id: "salon", x: 205, y: 118, label: t.visit.pins.salon },
    { id: "bts", x: 96, y: 208, label: t.visit.pins.bts },
    { id: "mall", x: 308, y: 198, label: t.visit.pins.mall },
  ];
  return (
    <section className="section" id="visit">
      <div className="wrap">
        <Reveal>
          <p className="eyebrow">{t.visit.eyebrow}</p>
          <h2>{t.visit.title}</h2>
          <p className="muted">{t.visit.addressNote}</p>
        </Reveal>
        <div className="grid grid--2" style={{ marginTop: 28 }}>
          <div>
            <div className="map">
              <svg viewBox="0 0 400 260" role="img" aria-label={t.visit.title}>
                <rect width="400" height="260" fill="#262927" />
                <path d="M0 212 H400" stroke="#3A3E3B" strokeWidth="14" />
                <path d="M205 0 V260" stroke="#3A3E3B" strokeWidth="9" />
                {pins.map((p) => (
                  <g key={p.id} className="pin" tabIndex="0" role="button" aria-label={p.label} data-pin={p.id}
                     onClick={() => setNote(p.label)}
                     onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setNote(p.label); } }}>
                    <circle cx={p.x} cy={p.y} r="11" fill={p.id === "salon" ? "#8FA69A" : "#B9BDB8"} />
                    <circle cx={p.x} cy={p.y} r="4" fill="#1B1D1C" />
                  </g>
                ))}
              </svg>
            </div>
            <p className="map__note" aria-live="polite">{note || t.visit.mapNote}</p>
            {/* No Maps href: a concept salon has no pin to navigate to. */}
            <button className="btn btn--ghost" data-directions onClick={() => setNote(t.visit.demoNote)}>{t.visit.directions}</button>
          </div>
          <div>
            <h3 style={{ fontSize: 18 }}>{t.visit.hours}</h3>
            <p className="muted">{t.visit.hoursBody}</p>
            <h3 style={{ fontSize: 18, marginTop: 18 }}>{t.visit.finding}</h3>
            <p className="muted">{t.visit.findingBody}</p>
            <h3 style={{ fontSize: 18, margin: "26px 0 6px" }}>{t.visit.faqTitle}</h3>
            <div className="faq">
              {t.visit.faq.map(([q, a]) => (
                <details key={q}><summary>{q}</summary><p className="muted" style={{ marginTop: 10 }}>{a}</p></details>
              ))}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

function SiteFooter({ t }) {
  return (
    <footer className="footer">
      <div className="wrap">
        <div className="footer__grid">
          <div>
            <div className="nav__name" style={{ fontSize: 22 }}>{BRAND}</div>
            <p className="muted" style={{ marginTop: 12 }}>{t.visit.hoursBody}</p>
          </div>
          <div>
            <p className="eyebrow">{t.footer.by}</p>
            <p><a href="https://saangstudio.com">{t.footer.byLink}</a></p>
          </div>
          <div>
            <p className="eyebrow">{t.footer.want}</p>
            <p><a href="https://saangstudio.com/en/hair-salons">{t.footer.wantCta}</a></p>
          </div>
        </div>
        {/* req009: disclosure #4 of 4. */}
        <p className="footer__note" data-concept-footer>{t.footer.disclosure}</p>
      </div>
    </footer>
  );
}

function StickyCta({ t }) {
  return (
    <div className="sticky">
      <a className="btn btn--ghost" href={PHONE_TEL}>{t.sticky.call}</a>
      <button className="btn btn--accent" onClick={() => goto("book")}>{t.sticky.book}</button>
    </div>
  );
}

function App() {
  const [lang, setLang] = useState(initialLang);
  const t = DICT[lang];
  useEffect(() => {
    enhanceSeo(lang);
    try { localStorage.setItem("toki_lang", lang); } catch (e) {}
  }, [lang]);
  useEffect(() => {
    if (REDUCED || !window.Lenis) return;
    const l = new window.Lenis({ duration: 1.05 });
    let id; const raf = (time) => { l.raf(time); id = requestAnimationFrame(raf); };
    id = requestAnimationFrame(raf);
    return () => { cancelAnimationFrame(id); l.destroy(); };
  }, []);
  const toggleLang = useCallback(() => setLang((l) => (l === "en" ? "th" : "en")), []);
  return (
    <>
      <Ribbon t={t} />
      <Nav t={t} lang={lang} toggleLang={toggleLang} />
      <Hero t={t} />
      <Work t={t} lang={lang} />
      <Menu t={t} lang={lang} />
      <Team t={t} lang={lang} />
      <Book t={t} lang={lang} />
      <Band id="hygiene" block={t.hygiene} dark />
      <Band id="care" block={t.care} />
      <Visit t={t} />
      <SiteFooter t={t} />
      <StickyCta t={t} />
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
