// Service detail page — shared component, driven by SERVICE_PAGES config

const SERVICE_PAGES = {
  "01": {
    n: "01",
    name: "Score & Part Preparation",
    tag: "Core",
    lead: "The core of the studio. Engraving, layout, formatting and parts extraction — turning a composer's manuscript or sketch into a clean, performance-ready score and a complete set of parts.",
    who: "Composers · Arrangers · Ensembles",
    process: [
    ["Receive", "Manuscript, MusicXML, Sibelius, Dorico — or a marked-up PDF."],
    ["Engrave", "Set in the studio's house style: spacing, beaming, clef and key choices, page turns considered."],
    ["Extract", "Parts pulled with cues, multirests, system breaks where the player needs them."],
    ["Proof", "Two passes — internal and against the composer's source. "],
    ["Deliver", "PDFs ready for print, or set up for the studio's print room (see 07)."]],

    examples: [
    { kind: "pdf", label: "Chamber score — bar 1–16", caption: "Engraved score sample · Quintet, Op. 4", style: "score" },
    { kind: "pdf", label: "Extracted part — Violin I", caption: "Same project, single part with cues", style: "part" },
    { kind: "pdf", label: "Composer manuscript → engraved", caption: "Before / after, bar 32–48", style: "before-after" }],

    addons: [
    { label: "Orchestration premium", note: "For projects requiring creative arrangement decisions beyond pure layout." }],

    faq: [
      { q: "What file formats do you accept for music engraving?",
        a: "Sibelius 8 Ultimate (.sib), Dorico (.dorico), MusicXML (.musicxml / .mxl), and marked-up PDFs. Handwritten manuscripts are accepted as scans or clear photographs at 300 DPI or higher." },
      { q: "How should I name files when submitting a score?",
        a: "Use the format Composer_Title_Movement_v##.ext — e.g. Smith_Quintet_I_v02.sib. Parts should be labelled by instrument and number (Violin_I.pdf, Cello.pdf). Avoid spaces, slashes and special characters; underscores and hyphens are safe everywhere." },
      { q: "What's the turnaround time for score and part preparation?",
        a: "Standard turnaround is 7–10 working days for chamber works (up to roughly 200 bars, 8 staves). Larger orchestral scores run 2–4 weeks. Rush turnaround is available at +50%." },
      { q: "How much does professional music engraving cost in the UK?",
        a: "Pricing is per page of finished score, scaled by ensemble size and source legibility. A typical chamber score runs £18–£28 per page; orchestral scores £35–£55. A firm quote is issued once score materials are reviewed — use the Studio estimator for an indicative figure." },
      { q: "Sibelius or Dorico — which does the studio engrave in?",
        a: "Both. Dorico is preferred for new engravings (better default spacing and condensing); Sibelius is used when the source file is already in Sibelius and a round-trip is required. Final delivery is software-agnostic PDF plus the editable file in either format on request." },
      { q: "Do you extract parts from an existing score, or do I need to send parts separately?",
        a: "Parts are extracted from the score in-house. Send only the score; parts will be pulled with appropriate cues, multirests, and page-turn-friendly system breaks." },
      { q: "Can you engrave from a handwritten manuscript?",
        a: "Yes — see service 03 (Notation Input / Digitisation) if the work is purely a re-typesetting job, or service 01 if engraving is part of a wider preparation including layout and parts." },
      { q: "What's the difference between music engraving and music notation input?",
        a: "Engraving is the typographic craft — spacing, beaming, page turns, house style. Notation input is the data-entry layer — getting the notes off the page and into the software. Service 01 includes both; service 03 is input-only for archival or publication purposes." }
    ]

  },

  "02": {
    n: "02",
    name: "Orchestration & Arranging",
    tag: "Reduction",
    lead: "Original composition, arrangement and orchestration for any ensemble. Including condensing an ensemble or orchestral score down to a playable piano or musical-director score, faithful to the original.",
    who: "Musical directors · Arrangers · Composers",
    process: [
    ["Brief", "Discuss intent — full orchestration from a lead sheet, reduction for rehearsal, or arrangement for a different ensemble."],
    ["Sketch", "Voicings and texture decisions sketched and shared for sign-off."],
    ["Score", "Engraved in full, with idiomatic part-writing for each instrument."],
    ["Mockup", "Optional virtual-instrument mockup (see 06) so the brief can be heard before rehearsal."],
    ["Deliver", "Score, parts, and any agreed audio reference."]],

    examples: [
    { kind: "pdf", label: "Lead sheet → string quartet", caption: "Arrangement of a song for SATB strings, bar 1–24", style: "score" },
    { kind: "audio", label: "Same arrangement, mockup", caption: "0:42 · Virtual strings, demo", duration: "0:42" },
    { kind: "pdf", label: "Orchestral reduction", caption: "Symphonic excerpt condensed to MD piano", style: "part" }],

    faq: [
      { q: "What's the difference between orchestration and arrangement?",
        a: "Arrangement adapts an existing piece for a different ensemble or context (a song re-cast for string quartet, a piano work for orchestra). Orchestration is the more specific craft of distributing musical material across instruments — voicings, doublings, idiomatic writing. The studio handles both, often together." },
      { q: "How is arrangement priced?",
        a: "Work is priced at a creative rate as a baseline, scaled by ensemble size and complexity. A firm quote is issued after a brief discussion of scope." },
      { q: "Can you reduce a full orchestral score to a piano or musical-director part?",
        a: "Yes. Orchestral reduction for rehearsal pianist or musical director is a common commission — voicings condensed to two staves while preserving cues and ensemble information. Common for theatre, opera workshops and rehearsal pianists." },
      { q: "How should I send source material for an arrangement?",
        a: "A clean lead sheet (PDF) plus a reference recording (MP3 or audio link) is ideal. For arrangements of existing engravings, the original Sibelius / Dorico / MusicXML file is preferred. Specify target ensemble, key, duration, and any non-negotiable elements upfront." },
      { q: "Can I hear the arrangement before rehearsal?",
        a: "Yes — a virtual-instrument mockup is available as an add-on (see service 06). Common for film, theatre, and any context where the arrangement needs sign-off before contracted players are booked." },
      { q: "Do you write original music for film, theatre and concert?",
        a: "Yes — original composition is offered through this service for clients who want a single point of contact for both the writing and the score preparation. You can hear examples of original work on the composer page." }
    ]

  },

  "03": {
    n: "03",
    name: "Notation Input / Digitisation",
    tag: "Input",
    lead: "Taking existing music — printed, photocopied, or handwritten manuscript — and producing a clean, engraved digital score, ready for performance, publication, or further editing.",
    who: "Publishers · Estates · Composers",
    process: [
    ["Receive", "Scans, photocopies, photographs of manuscript, or original parts."],
    ["Decipher", "Where the source is unclear, queries are logged with timecodes / bar numbers and sent back."],
    ["Input", "Re-typeset in modern notation software — articulations, dynamics, lyrics, all carried over."],
    ["Proof", "Bar-by-bar against the source; queries resolved before delivery."],
    ["Deliver", "Editable score (Sibelius / Dorico / MusicXML) plus print-ready PDF."]],

    examples: [
    { kind: "pdf", label: "Manuscript → engraved", caption: "Handwritten source, top — engraved version, bottom", style: "before-after" },
    { kind: "pdf", label: "Old print → modern engraving", caption: "Public-domain reprint, re-set in the house style", style: "score" }],

    faq: [
      { q: "Can you digitise handwritten music into Sibelius or Dorico?",
        a: "Yes. Handwritten manuscripts — composer sketches, archival sources, or photocopied parts — are re-typeset into modern notation software. Final delivery is editable Sibelius, Dorico or MusicXML, plus a print-ready PDF." },
      { q: "What scan quality do you need for manuscript digitisation?",
        a: "300 DPI greyscale or higher, with the full page in frame and no skew. Phone photos are accepted if the lighting is even and the staves are sharp at 100% zoom. Faded or pencil-on-cream sources can usually still be read — query case-by-case." },
      { q: "Do you handle public-domain or out-of-copyright works?",
        a: "Yes — re-engraving public-domain editions in a clean, modern house style is a regular commission for publishers, ensembles and educators. Provenance is logged and a clean editorial pass is included by default." },
      { q: "Can lyrics, chord symbols and figured bass be transcribed?",
        a: "Yes. Lyrics with hyphenation, chord symbols, figured bass and rehearsal letters are carried over. Anything ambiguous in the source is flagged and queried before delivery." },
      { q: "What's the difference between OMR (optical music recognition) and notation input?",
        a: "OMR is automated — fast but error-prone, especially on handwritten or dense sources. The studio uses notation input (manual entry by an engraver) for accuracy. OMR may be used as a first pass on clean printed sources where it saves time." }
    ]

  },

  "04": {
    n: "04",
    name: "Transposition",
    tag: "Adjustment",
    lead: "Key changes or instrumental transposition — across individual parts or a full score — proofed against the original notation. Flat rate per bar, per instrument.",
    who: "Vocalists · Wind & brass players · MDs",
    process: [
    ["Receive", "Source score or part, with the target key or instrument specified."],
    ["Transpose", "Carried out in notation software with idiomatic respelling and range checks."],
    ["Proof", "Cross-checked against the source bar-for-bar; written queries on any range or notation issues."],
    ["Deliver", "Transposed PDF; editable file on request."]],

    examples: [
    { kind: "pdf", label: "Aria — down a minor third", caption: "Vocal line and piano accompaniment, bar 1–32", style: "before-after" },
    { kind: "pdf", label: "Clarinet in A → in B♭", caption: "Single part, transposed and re-spelled", style: "part" }],

    faq: [
      { q: "How much does music transposition cost per page?",
        a: "Transposition is priced per bar, per instrument — typically £0.30–£0.60 per bar for a single line, scaled by ensemble size. Whole-score transpositions are quoted as a flat rate. The estimator returns an indicative figure once bar count and ensemble are entered." },
      { q: "Can you transpose a vocal score to a different key?",
        a: "Yes — vocal score transposition for singers is one of the most common requests. The vocal line, piano accompaniment, and any chord symbols are all moved to the target key with idiomatic respelling (no double-flats unless harmonically necessary)." },
      { q: "Can you transpose for transposing instruments — clarinet in A to B♭, horn in F, etc.?",
        a: "Yes. The studio handles all standard transposing instruments — clarinets, horns, trumpets, English horn, alto / tenor / baritone saxophone — with correct concert-pitch versus written-pitch handling. Range checks are included." },
      { q: "What's the turnaround for a transposition?",
        a: "Single parts: 24–48 hours. Vocal scores: 3–5 working days. Full-ensemble transpositions: 1–2 weeks depending on score length." },
      { q: "Do you proof transpositions against the original?",
        a: "Yes — bar-by-bar cross-check against the source, with written queries logged on any range or notation issues before delivery." }
    ]

  },

  "05": {
    n: "05",
    name: "Audio Transcription",
    tag: "Listening",
    lead: "Listening to a recording and turning it into notation or MIDI. Useful for capturing improvisations, transcribing arrangements for which no score exists, or producing performance materials from a reference track. Priced per bar, scaled by the number of instruments and voices in the source.",
    who: "Songwriters · Improvisers · Music directors · Educators",
    process: [
    ["Receive", "Audio reference — MP3, WAV, video link, or stems. Tempo and time signature, if known."],
    ["Listen", "Multiple passes — establishing form first, then harmony, melody, inner parts and rhythmic detail."],
    ["Transcribe", "Notation entered in Sibelius or Dorico, with cue references back to timestamps in the audio."],
    ["Proof", "Cross-checked against the recording; ambiguous passages flagged with written queries."],
    ["Deliver", "Notation PDF, MIDI file, or both — plus the editable source file on request."]],

    examples: [
    { kind: "audio", label: "String quartet — Allegro", caption: "1:08 · Chamber strings, fully transcribed", duration: "1:08" },
    { kind: "audio", label: "Cinematic cue", caption: "0:54 · Orchestra + percussion, full score", duration: "0:54" },
    { kind: "audio", label: "Jazz combo lead sheet", caption: "1:20 · Piano trio, with chord symbols", duration: "1:20" }],

    faq: [
      { q: "How much does it cost to transcribe a song into sheet music?",
        a: "Audio transcription is priced per bar, using a tiered rate that scales with the number of instruments or voices in the source — a solo line transcribes faster than a six-part chamber piece. Indicative range: £6.46–£13.95 per bar of effective polyphony. Use the Studio estimator for a quick figure once duration, tempo and ensemble are known." },
      { q: "What audio formats are accepted?",
        a: "MP3, WAV, FLAC and AIFF. Video links (YouTube, Vimeo, private hosting) are also fine — the studio will pull the audio. For best results, send the highest-quality source available; lossy MP3s below 192 kbps are workable but make ambiguous inner voices harder to resolve." },
      { q: "Can you transcribe from a noisy or live recording?",
        a: "Yes, with caveats. Clean studio recordings are fastest. Live or rehearsal recordings take longer and may need timestamped queries on passages that are inaudible. Recording quality is captured as a note alongside the quote but does not change the per-bar rate." },
      { q: "Can I get just the MIDI, without notation?",
        a: "Yes — MIDI-only output is offered at a slight discount (it skips the engraving pass). Useful for sample-library mockups, DAW-based reworks, or as a starting point for arrangement work. Both notation and MIDI together is also an option." },
      { q: "What's the turnaround for an audio transcription?",
        a: "A 60–90 second clip with a small ensemble: 3–5 working days. A 3–4 minute song with full band or chamber ensemble: 1–2 weeks. Rush turnaround is available at +50%." },
      { q: "Do you transcribe full albums or just individual tracks?",
        a: "Both. Multi-track projects are quoted as a bundle — the bundle discount applies once the second track is added." }
    ]

  },

  "06": {
    n: "06",
    name: "Demo / Mockup Audio",
    tag: "Production",
    lead: "A professional-quality audio mockup using virtual instruments — for pitching, rehearsal, or simply to hear a piece before it's recorded. Priced per minute of finished audio. Orchestral projects capped at £375.",
    who: "Film · Theatre · Commissioners",
    process: [
    ["Receive", "Score, MIDI, or sketch — whatever the source material is."],
    ["Programme", "Instruments routed and articulated by hand; tempo map, dynamics, phrasing all worked in."],
    ["Mix", "Balance, reverb, and spatial placement applied — not a flat MIDI playback."],
    ["Deliver", "WAV and MP3, plus an optional stem bounce for further work."]],

    examples: [
    { kind: "audio", label: "String quartet — Allegro", caption: "1:08 · Virtual chamber strings", duration: "1:08" },
    { kind: "audio", label: "Cinematic cue", caption: "0:54 · Orchestra + percussion", duration: "0:54" },
    { kind: "audio", label: "Pop arrangement, demo", caption: "0:36 · Band + horns mockup", duration: "0:36" }],

    faq: [
      { q: "What virtual instruments do you use for music mockups?",
        a: "A studio-standard combination of Spitfire (BBC SO Pro, Symphonic Strings), Cinematic Studio Series, Brass, and Native Instruments Komplete for synth and percussion. Selected per project to match the brief." },
      { q: "How much does a music mockup cost per minute?",
        a: "Priced per minute of finished audio — chamber and small-ensemble work runs £80–£150 per minute, orchestral mockups £180–£280 per minute. Orchestral projects are capped at £375 total. Indicative figures only — final quote follows score review." },
      { q: "What's the difference between a MIDI mockup and a finished demo?",
        a: "A flat MIDI playback is what notation software produces — useful for proofing, but harsh and unmusical. A finished mockup involves hand-articulated parts, tempo shaping, dynamics, mixing and reverb — closer to a sonic representation of the live ensemble." },
      { q: "Can I get stems for further work in my own DAW?",
        a: "Yes — stem bounces (per section, or per instrument on request) are available as an add-on. Useful for film/TV briefs where the demo will be edited to picture downstream." },
      { q: "How do I send a score for a mockup?",
        a: "MIDI export from Sibelius / Dorico is preferred (cleanest tempo and articulation data). PDF + MusicXML works too. Specify target duration, target tempo if it differs from the score, and any reference recordings for tone." }
    ]

  },

  "07": {
    n: "07",
    name: "Printing, Binding & Delivery",
    tag: "Print",
    lead: "In-house production. Conductor scores (A3) and parts (A4) quoted separately — white or cream 120gsm, with binding. A dynamic parts list lets you specify copies per instrument; we print, bind and despatch.",
    who: "Conductors · Orchestras · Festivals",
    process: [
    ["Specify", "Format (A3 / A4), paper (white or cream 120gsm), copies, sided / double-sided."],
    ["Bind", "None, staple, tape or coil — chosen per item."],
    ["Pack", "Conductor and parts boxed together; check sheet enclosed."],
    ["Despatch", "Local courier, standard post, or same-day where available."]],

    examples: [
    // Vertical step videos. Sample clips below are placeholders — swap each
    // videoUrl for the real vertical recording of that step.
    { kind: "video", step: "01 · Print", label: "Printing the sheets", caption: "120gsm laid down, double-sided",
      videoUrl: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" },
    { kind: "video", step: "02 · Gather", label: "Folding & gathering", caption: "Signatures collated in order",
      videoUrl: "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4" },
    { kind: "video", step: "03 · Bind", label: "Binding by hand", caption: "Coil, tape or staple per item",
      videoUrl: "https://test-videos.co.uk/vids/jellyfish/mp4/h264/360/Jellyfish_360_10s_1MB.mp4" },
    { kind: "video", step: "04 · Despatch", label: "Packing & despatch", caption: "Boxed with check sheet enclosed",
      videoUrl: "https://test-videos.co.uk/vids/sintel/mp4/h264/360/Sintel_360_10s_1MB.mp4" }],

    faq: [
      { q: "What paper weight do you print conductor scores and parts on?",
        a: "120gsm uncoated, in either bright white or warm cream. 120gsm is heavy enough to take stand-light handling without curl, light enough to fold for tape binding. " },
      { q: "What binding options are available for orchestral parts?",
        a: "Staple (1–32 pages), tape-bind (33–80 pages, lay-flat), coil / spiral (any length, lay-flat — preferred for conductor scores), or none (loose-leaf for ring-binders). Each item in a project can be bound differently." },
      { q: "What size do you print conductor scores?",
        a: "A3 (297×420mm) is standard for conductor scores; A4 (210×297mm) for parts. " },
      { q: "Do you ship internationally?",
        a: "Yes. UK orders ship via standard or next-day courier; international orders ship tracked. A check sheet listing every item and its specification is enclosed with every despatch." },
      { q: "Can you print only — without preparing the score?",
        a: "Yes — send print-ready PDFs and the print room will produce, bind and despatch. Files should be at print-resolution (vector PDF preferred), set to the correct page size, with no crop marks." }
    ]

  }
};

// Auto-linker for FAQ answer strings inside a service page.
function autoLinkAnswer(text, go) {
  if (typeof text !== "string") return text;
  const subs = [
    { match: /\(see (\d{2})\)/g, target: (m) => `service:${m[1]}` },
    { match: /service (\d{2})/g, target: (m) => `service:${m[1]}` },
    { match: /Submit page/g, target: "submit" },
    { match: /Studio estimator|the estimator|The estimator|the Studio estimator/g, target: "studio" }
  ];
  let nodes = [text];
  subs.forEach(({ match, target }) => {
    const next = [];
    nodes.forEach(node => {
      if (typeof node !== "string") { next.push(node); return; }
      let last = 0, m, i = 0;
      const re = new RegExp(match.source, "g");
      while ((m = re.exec(node)) !== null) {
        if (m.index > last) next.push(node.slice(last, m.index));
        const t = typeof target === "function" ? target(m) : target;
        next.push(
          <button
            key={`sla-${m.index}-${i++}`}
            type="button"
            className="faq-inline-link"
            onClick={() => go(t)}
          >{m[0]}</button>
        );
        last = m.index + m[0].length;
        if (m[0].length === 0) re.lastIndex++;
      }
      if (last < node.length) next.push(node.slice(last));
    });
    nodes = next;
  });
  return <>{nodes}</>;
}

// Renders a process-step description, turning "(see NN)" into a clickable link.
function renderStepDesc(text, go) {
  const parts = [];
  const re = /\(see (\d{2})\)/g;
  let last = 0;
  let m;
  let key = 0;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) parts.push(text.slice(last, m.index));
    const id = m[1];
    parts.push(
      <span key={key++}>(see <a
        className="svc-step-link"
        onClick={(e) => { e.preventDefault(); go(`service:${id}`); }}
        href="#"
      >{id}</a>)</span>
    );
    last = m.index + m[0].length;
  }
  if (last < text.length) parts.push(text.slice(last));
  return parts.length ? parts : text;
}

// Fullscreen viewer context — any example card calls this to open the lightbox.
const LightboxCtx = React.createContext(() => {});

function ServicePage({ id, go }) {
  const s = SERVICE_PAGES[id];
  const [liveExamples, setLiveExamples] = React.useState(null);
  const [lightbox, setLightbox] = React.useState(null);

  // Pull real examples from Supabase. Falls back silently to placeholders.
  React.useEffect(() => {
    if (!s || !window.fetchExamples) return;
    let cancelled = false;
    window.fetchExamples(s.n).then(rows => {
      if (cancelled || !rows || !rows.length) return;
      // Normalise DB snake_case to the local camelCase shape the cards expect.
      const normalised = rows.map(r => ({
        kind:     r.kind,
        label:    r.label,
        caption:  r.caption,
        duration: r.duration,
        audioUrl: r.audio_url,
        imageUrl: r.image_url,
        pdfUrl:   r.pdf_url,
        videoUrl: r.video_url,
        poster:   r.poster_url || r.image_url,
        step:     r.step,
        swatch:   r.swatch || "white",
        style:    r.style || "score",
      }));
      setLiveExamples(normalised);
    });
    return () => { cancelled = true; };
  }, [id]);

  if (!s) {
    return (
      <div className="page"><div className="wrap" style={{ padding: "120px 0" }}>
        <div className="mono">Service not found.</div>
        <button className="btn-primary" style={{ marginTop: 24 }} onClick={() => go("studio")}>← Back to studio</button>
      </div></div>);

  }

  const examples = liveExamples ?? s.examples;

  return (
    <LightboxCtx.Provider value={setLightbox}>
    <div className="page" data-screen-label={`05 Service ${s.n} — ${s.name}`}>
      <div className="wrap">

        {/* Crumb */}
        <div className="svc-crumb">
          <button className="svc-crumb-link" onClick={() => go("studio")}>← Studio</button>
          <span className="mono-sm svc-crumb-tag">{s.n} — {s.name.toUpperCase()}</span>
        </div>

        {/* Hero */}
        <div className="svc-hero">
          <div className="svc-hero-num mono">{s.n}</div>
          <h1 className="svc-hero-title">{s.name}</h1>
          <p className="svc-hero-lead">{s.lead}</p>
          <div className="svc-hero-who mono-sm">For — {s.who}</div>
        </div>

        {/* Examples */}
        <section className="section">
          <div className="section-head">
            <div className="mono">EXAMPLES</div>
            <div>
              <h2>From the studio.</h2>
              <div className="mono-sm" style={{ marginTop: 10, opacity: 0.55 }}>
                Click any example to enlarge.
              </div>
            </div>
          </div>
          <ExamplesCarousel examples={examples} />
        </section>

        {/* Process */}
        <section className="section">
          <div className="section-head">
            <div className="mono">PROCESS</div>
            <h2>How it runs.</h2>
          </div>
          <ol className="svc-process">
            {s.process.map(([t, d], i) =>
            <li key={i} className="svc-step">
                <span className="mono svc-step-num">{String(i + 1).padStart(2, "0")}</span>
                <div className="svc-step-body">
                  <div className="svc-step-title">{t}</div>
                  <div className="svc-step-desc">{renderStepDesc(d, go)}</div>
                </div>
              </li>
            )}
          </ol>
        </section>

        {/* Add-ons (only on 01) */}
        {s.addons &&
        <section className="section">
            <div className="section-head">
              <div className="mono">ADD-ONS</div>
              <h2>Optional, on request.</h2>
            </div>
            <div className="svc-addons">
              {s.addons.map((a, i) =>
            <div key={i} className="svc-addon">
                  <div className="svc-addon-label">{a.label}</div>
                  <div className="svc-addon-note">{a.note}</div>
                </div>
            )}
            </div>
          </section>
        }

        {/* Examples (moved above Process) */}

        {/* FAQ */}
        {s.faq &&
        <section className="section">
            <div className="section-head">
              <div className="mono">FAQ</div>
              <h2 className="serif">Common questions.</h2>
            </div>
            <div className="svc-faq" itemScope itemType="https://schema.org/FAQPage">
              {s.faq.map((f, i) =>
            <details key={i} className="svc-faq-item" itemProp="mainEntity" itemScope itemType="https://schema.org/Question">
                  <summary className="svc-faq-q">
                    <span className="svc-faq-num mono-sm">{String(i + 1).padStart(2, "0")}</span>
                    <span itemProp="name">{f.q}</span>
                    <span className="svc-faq-mark" aria-hidden="true">+</span>
                  </summary>
                  <div className="svc-faq-a" itemProp="acceptedAnswer" itemScope itemType="https://schema.org/Answer">
                    <p itemProp="text">{autoLinkAnswer(f.a, go)}</p>
                  </div>
                </details>
            )}
            </div>
            <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({
              "@context": "https://schema.org",
              "@type": "FAQPage",
              "mainEntity": s.faq.map(f => ({
                "@type": "Question",
                "name": f.q,
                "acceptedAnswer": { "@type": "Answer", "text": f.a }
              }))
            }) }} />
          </section>
        }

        {/* CTA */}
        <section className="section" style={{ borderBottom: "none" }}>
          <div className="svc-cta">
            <div>
              <div className="mono" style={{ marginBottom: 12 }}>NEXT</div>
              <h2 className="svc-cta-title">
                Estimate your<br />
                <span style={{ color: "var(--fg-muted)" }}>{s.name.toLowerCase()}</span>.
              </h2>
            </div>
            <div className="svc-cta-actions">
              <button className="btn-primary" onClick={() => go("studio", "estimator")}>
                <span>Use estimator</span>
                <span className="arrow"></span>
              </button>
              <button className="btn-secondary" onClick={() => go("submit")}>Submit a score →</button>
            </div>
          </div>
        </section>

        <SiteFooter go={go} />
      </div>
    </div>
    <Lightbox item={lightbox} onClose={() => setLightbox(null)} />
    </LightboxCtx.Provider>);

}

// ── Example cards ────────────────────────────────────────────────────────────
function ExamplesCarousel({ examples }) {
  const trackRef = React.useRef(null);
  const [atStart, setAtStart] = React.useState(true);
  const [atEnd, setAtEnd] = React.useState(false);
  const [overflowing, setOverflowing] = React.useState(false);
  const [armTop, setArmTop] = React.useState(150);

  const update = React.useCallback(() => {
    const el = trackRef.current;
    if (!el) return;
    const max = el.scrollWidth - el.clientWidth;
    setOverflowing(max > 4);
    setAtStart(el.scrollLeft <= 4);
    setAtEnd(el.scrollLeft >= max - 4);
    // Centre the arrows on the document preview (the frame), not the caption.
    const frame = el.querySelector(".ex-pdf-frame, .ex-print-frame, .ex-audio-frame");
    if (frame) setArmTop(frame.offsetHeight / 2);
  }, []);

  React.useEffect(() => {
    update();
    const el = trackRef.current;
    if (!el) return;
    el.addEventListener("scroll", update, { passive: true });
    window.addEventListener("resize", update);
    return () => { el.removeEventListener("scroll", update); window.removeEventListener("resize", update); };
  }, [update, examples]);

  const scrollByCard = (dir) => {
    const el = trackRef.current;
    if (!el) return;
    const card = el.querySelector(".ex-card");
    const step = card ? card.offsetWidth + 24 : el.clientWidth * 0.8;
    el.scrollBy({ left: dir * step, behavior: "smooth" });
  };

  return (
    <div className={`svc-ex-carousel ${overflowing ? "is-overflowing" : ""}`}>
      <button className="svc-ex-arrow left" style={{ top: armTop }} onClick={() => scrollByCard(-1)} disabled={atStart}
        aria-label="Previous examples">←</button>
      <div className="svc-examples" ref={trackRef}>
        {examples.map((ex, i) => <ExampleCard key={i} ex={ex} index={i} />)}
      </div>
      <button className="svc-ex-arrow right" style={{ top: armTop }} onClick={() => scrollByCard(1)} disabled={atEnd}
        aria-label="Next examples">→</button>
    </div>
  );
}

function ExampleCard({ ex, index }) {
  if (ex.kind === "pdf") return <PdfExample ex={ex} />;
  if (ex.kind === "audio") return <AudioExample ex={ex} />;
  if (ex.kind === "video") return <VideoExample ex={ex} />;
  if (ex.kind === "print") return <PrintExample ex={ex} />;
  return null;
}

// Renders page 1 of a PDF straight onto a canvas via pdf.js. Used on the live
// site when a PDF example has no pre-made preview image.
function PdfCanvas({ url, label }) {
  const canvasRef = React.useRef(null);
  const [state, setState] = React.useState("loading"); // loading | ok | error

  React.useEffect(() => {
    let cancelled = false;
    if (!window.pdfjsLib) { setState("error"); return; }
    (async () => {
      try {
        const pdf = await window.pdfjsLib.getDocument(url).promise;
        const page = await pdf.getPage(1);
        if (cancelled) return;
        const canvas = canvasRef.current;
        if (!canvas) return;
        const base = page.getViewport({ scale: 1 });
        // Render at high resolution (account for retina) so it stays crisp.
        const dpr = Math.min(window.devicePixelRatio || 1, 3);
        const targetW = 1000; // CSS px the card is shown at, generously
        const scale = Math.min(8, (targetW * dpr) / base.width);
        const viewport = page.getViewport({ scale });
        canvas.width = Math.ceil(viewport.width);
        canvas.height = Math.ceil(viewport.height);
        const ctx = canvas.getContext("2d");
        ctx.fillStyle = "#fff";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        await page.render({ canvasContext: ctx, viewport }).promise;
        if (!cancelled) setState("ok");
      } catch (e) {
        console.warn("PDF render failed:", e);
        if (!cancelled) setState("error");
      }
    })();
    return () => { cancelled = true; };
  }, [url]);

  if (state === "error") return <ScorePreview style="score" />;
  return (
    <canvas ref={canvasRef} className="ex-real-image" aria-label={label}
      style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
  );
}

function PdfExample({ ex }) {
  const openLightbox = React.useContext(LightboxCtx);
  // A PDF accidentally saved into the image field (older rows) is detected and
  // routed through the canvas renderer instead of showing a broken <img>.
  const imageIsPdf = ex.imageUrl && /\.pdf(\?|$)/i.test(ex.imageUrl);
  const hasImage = !!ex.imageUrl && !imageIsPdf;
  const pdfSrc = ex.pdfUrl || (imageIsPdf ? ex.imageUrl : null);
  const viewable = pdfSrc || hasImage;
  const open = !viewable ? undefined : () => openLightbox(
    pdfSrc
      ? { type: "pdf", src: pdfSrc, label: ex.label, caption: ex.caption }
      : { type: "image", src: ex.imageUrl, label: ex.label, caption: ex.caption }
  );
  return (
    <figure className="ex-card ex-pdf">
      <button type="button" className={`ex-pdf-frame ${viewable ? "ex-zoomable" : ""}`}
        onClick={open} disabled={!viewable} aria-label={viewable ? `View ${ex.label} full screen` : undefined}>
        {hasImage ? (
          <img src={ex.imageUrl} alt={ex.label} className="ex-real-image" loading="lazy" />
        ) : pdfSrc ? (
          <PdfCanvas url={pdfSrc} label={ex.label} />
        ) : (
          <ScorePreview style={ex.style} />
        )}
        <div className="ex-pdf-corner mono-sm">PDF</div>
        {viewable && <div className="ex-zoom-hint mono-sm">View full screen</div>}
      </button>
      <figcaption className="ex-cap">
        <div className="ex-cap-label">{ex.label}</div>
        <div className="ex-cap-note">{ex.caption}</div>
        {ex.pdfUrl && (
          <a href={ex.pdfUrl} target="_blank" rel="noopener noreferrer" className="ex-cap-link mono-sm">
            Open full PDF →
          </a>
        )}
      </figcaption>
    </figure>);

}

function AudioExample({ ex }) {
  return (
    <figure className="ex-card ex-audio ex-audio--real">
      <div className="ex-audio-frame ex-audio-frame--real">
        {ex.audioUrl ? (
          <window.AudioPlayer src={ex.audioUrl} />
        ) : (
          <div className="ex-audio-coming mono-sm">Audio sample coming soon</div>
        )}
        <div className="ex-audio-corner mono-sm">{ex.duration ? ex.duration : "AUDIO"}</div>
      </div>
      <figcaption className="ex-cap">
        <div className="ex-cap-label">{ex.label}</div>
        <div className="ex-cap-note">{ex.caption}</div>
      </figcaption>
    </figure>);
}

function VideoExample({ ex }) {
  const ref = React.useRef(null);
  // Autoplay (muted) only while the card is on screen; pause when scrolled away
  // so several vertical clips don't all run at once.
  React.useEffect(() => {
    const v = ref.current;
    if (!v) return;
    let visible = true;
    const sync = () => { if (visible) v.play().catch(() => {}); else v.pause(); };
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { visible = e.isIntersecting; sync(); });
    }, { threshold: 0.3 });
    io.observe(v);
    // If the observer fired before the clip was ready, start once it can play.
    v.addEventListener("canplay", sync);
    return () => { io.disconnect(); v.removeEventListener("canplay", sync); };
  }, []);
  return (
    <figure className="ex-card ex-video">
      <div className="ex-video-frame">
        {ex.videoUrl ? (
          <video
            ref={ref}
            className="ex-video-el"
            src={ex.videoUrl}
            poster={ex.poster}
            muted
            loop
            playsInline
            autoPlay
            preload="metadata"
          />
        ) : (
          <div className="ex-video-coming mono-sm">Video coming soon</div>
        )}
        <div className="ex-video-corner mono-sm">{ex.step || "VIDEO"}</div>
      </div>
      <figcaption className="ex-cap">
        <div className="ex-cap-label">{ex.label}</div>
        <div className="ex-cap-note">{ex.caption}</div>
      </figcaption>
    </figure>);
}

function PrintExample({ ex }) {
  const openLightbox = React.useContext(LightboxCtx);
  const hasImage = !!ex.imageUrl;
  const open = hasImage ? () => openLightbox({ type: "image", src: ex.imageUrl, label: ex.label, caption: ex.caption }) : undefined;
  return (
    <figure className="ex-card ex-print">
      <button type="button" className={`ex-print-frame ex-print-${ex.swatch} ${hasImage ? "ex-zoomable" : ""}`}
        onClick={open} disabled={!hasImage} aria-label={hasImage ? `View ${ex.label} full screen` : undefined}>
        <div className="ex-print-stack">
          <span className="ex-print-page" />
          <span className="ex-print-page" />
          <span className="ex-print-page top">
            {hasImage ? (
              <img src={ex.imageUrl} alt={ex.label} className="ex-real-image" loading="lazy" />
            ) : (
              <ScorePreview style="part" tone={ex.swatch} />
            )}
          </span>
        </div>
        <div className="ex-print-corner mono-sm">PRINT</div>
        {hasImage && <div className="ex-zoom-hint mono-sm">View full screen</div>}
      </button>
      <figcaption className="ex-cap">
        <div className="ex-cap-label">{ex.label}</div>
        <div className="ex-cap-note">{ex.caption}</div>
      </figcaption>
    </figure>);

}

function toSeconds(d) {
  if (!d) return 30;
  const [m, s] = d.split(":").map(Number);
  return m * 60 + s;
}
function formatTime(sec) {
  const m = Math.floor(sec / 60);
  const s = Math.floor(sec % 60);
  return `${m}:${String(s).padStart(2, "0")}`;
}

// ── Waveform: deterministic by seed ──────────────────────────────────────────
function Waveform({ active, progress, seed = 0 }) {
  const bars = 56;
  const data = React.useMemo(() => {
    const arr = [];
    for (let i = 0; i < bars; i++) {
      const x = Math.sin((i + seed * 13) * 0.42) * Math.cos((i + seed * 7) * 0.13);
      const y = Math.sin((i + seed * 3) * 0.21);
      arr.push(0.25 + Math.abs(x * 0.6 + y * 0.3) * 0.75);
    }
    return arr;
  }, [seed]);

  return (
    <div className="ex-wave">
      {data.map((h, i) => {
        const played = i / bars <= progress;
        return (
          <span
            key={i}
            className={`ex-wave-bar ${played ? "played" : ""} ${active ? "live" : ""}`}
            style={{ height: `${h * 100}%`, animationDelay: `${i % 8 * 60}ms` }} />);


      })}
    </div>);

}

// ── Drawn score preview (svg) ─────────────────────────────────────────────────
function ScorePreview({ style = "score", tone = "white" }) {
  // Synthetic engraved-looking page. Seed-based so each example differs.
  const lines = style === "part" ? 6 : style === "before-after" ? 8 : 10;
  return (
    <svg viewBox="0 0 200 280" className={`ex-score ex-score-${tone}`} preserveAspectRatio="xMidYMid meet">
      {/* page */}
      <rect x="0" y="0" width="200" height="280" fill="currentColor" opacity="0" />
      {/* title block */}
      {style !== "part" &&
      <>
          <rect x="20" y="14" width="80" height="3" fill="currentColor" opacity="0.85" />
          <rect x="20" y="22" width="50" height="2" fill="currentColor" opacity="0.45" />
        </>
      }
      {/* staves */}
      {Array.from({ length: lines }).map((_, i) => {
        const y = (style === "part" ? 50 : 50) + i * (style === "part" ? 30 : 22);
        return (
          <g key={i} opacity="0.85">
            {/* 5 lines */}
            {[0, 2, 4, 6, 8].map((k) =>
            <line key={k} x1="18" x2="182" y1={y + k} y2={y + k} stroke="currentColor" strokeWidth="0.4" />
            )}
            {/* clef blob */}
            <path d={`M 22 ${y + 4} q -3 -2 -1 -5 q 3 -3 5 0 q 2 4 -2 6 q 0 4 1 7`} stroke="currentColor" strokeWidth="0.9" fill="none" />
            {/* bar lines */}
            {[44, 78, 112, 146, 180].map((x) =>
            <line key={x} x1={x} x2={x} y1={y - 0.5} y2={y + 8.5} stroke="currentColor" strokeWidth="0.4" />
            )}
            {/* notes — pseudo-random by index */}
            {Array.from({ length: 14 }).map((_, j) => {
              const x = 32 + j * 11;
              const ny = y + (Math.sin((i * 7 + j * 1.3) * 1.3) + 1) * 4;
              const stem = ny < y + 3.5;
              return (
                <g key={j}>
                  <ellipse cx={x} cy={ny} rx="1.5" ry="1.1" fill="currentColor" />
                  <line x1={x + (stem ? 1.4 : -1.4)} x2={x + (stem ? 1.4 : -1.4)}
                  y1={ny} y2={ny + (stem ? 8 : -8)} stroke="currentColor" strokeWidth="0.5" />
                </g>);

            })}
          </g>);

      })}
      {style === "before-after" &&
      <line x1="0" y1="140" x2="200" y2="140" stroke="currentColor" strokeWidth="0.4" strokeDasharray="2 2" opacity="0.4" />
      }
      {/* page number */}
      <text x="100" y="270" textAnchor="middle" fontSize="5" fill="currentColor" opacity="0.5"
      style={{ fontFamily: "var(--mono, monospace)" }}>— {style === "part" ? "VLN. I" : "p. 1"} —</text>
    </svg>);

}

// ── Fullscreen lightbox ──────────────────────────────────────────────────────
function PdfPageCanvas({ pdf, num }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const page = await pdf.getPage(num);
        if (cancelled || !ref.current) return;
        const dpr = Math.min(window.devicePixelRatio || 1, 3);
        const parent = ref.current.parentElement;
        const cssW = Math.min(1400, parent ? parent.clientWidth : 1100);
        const base = page.getViewport({ scale: 1 });
        const scale = (cssW * dpr) / base.width;
        const vp = page.getViewport({ scale });
        const canvas = ref.current;
        canvas.width = Math.ceil(vp.width);
        canvas.height = Math.ceil(vp.height);
        const ctx = canvas.getContext("2d");
        ctx.fillStyle = "#fff";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        await page.render({ canvasContext: ctx, viewport: vp }).promise;
      } catch (e) { console.warn("page render failed", e); }
    })();
    return () => { cancelled = true; };
  }, [pdf, num]);
  return <canvas ref={ref} className="lb-pdf-page" />;
}

function LightboxPdf({ url }) {
  const [pdf, setPdf] = React.useState(null);
  const [status, setStatus] = React.useState("loading");
  React.useEffect(() => {
    let cancelled = false;
    if (!window.pdfjsLib) { setStatus("error"); return; }
    window.pdfjsLib.getDocument(url).promise
      .then((doc) => { if (!cancelled) { setPdf(doc); setStatus("ok"); } })
      .catch((e) => { console.warn(e); if (!cancelled) setStatus("error"); });
    return () => { cancelled = true; };
  }, [url]);

  if (status === "loading") return <div className="lb-loading mono-sm">Loading score…</div>;
  if (status === "error") return (
    <div className="lb-loading mono-sm">
      Couldn’t render this file. <a href={url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--accent)" }}>Open it directly ↗</a>
    </div>
  );
  return (
    <div className="lb-pdf-pages">
      <PdfPageCanvas pdf={pdf} num={1} />
    </div>
  );
}

function Lightbox({ item, onClose }) {
  React.useEffect(() => {
    if (!item) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prev; };
  }, [item, onClose]);

  if (!item) return null;
  return (
    <div className="lb-overlay" onClick={onClose} role="dialog" aria-modal="true">
      <div className="lb-bar" onClick={(e) => e.stopPropagation()}>
        <div className="lb-title">
          <span className="lb-title-label">{item.label}</span>
          {item.caption && <span className="lb-title-cap mono-sm">{item.caption}</span>}
        </div>
        <div className="lb-actions">
          {item.type === "pdf" && (
            <a className="lb-download mono-sm" href={item.src} target="_blank" rel="noopener noreferrer">Open PDF ↗</a>
          )}
          <button className="lb-close" onClick={onClose} aria-label="Close">✕</button>
        </div>
      </div>
      <div className="lb-body" onClick={(e) => e.stopPropagation()}>
        {item.type === "image"
          ? <img src={item.src} alt={item.label} className="lb-image" />
          : <LightboxPdf url={item.src} />}
      </div>
    </div>
  );
}

window.ServicePage = ServicePage;
window.SERVICE_PAGES = SERVICE_PAGES;