"""Builds the Julien Bonjour site markup (EN + FR).

Outputs:
  push/<name>.html   body fragments sent to Oxygen html-to-page
  pages/<name>.html  full preview documents (header + page + footer + CSS/JS)
"""
import json
import os
from html import escape

ROOT = os.path.dirname(os.path.abspath(__file__))
IMG = {i["id"]: i for i in json.load(open(os.path.join(ROOT, "data", "images.json"), encoding="utf-8"))}

# Contact-sheet scene of every imported photo (wp id -> scene), from the curation pass.
SCENES_RAW = """233-275:Shoo 276-277:Back 278-283:Publ 284-298:Welc 299-306:Gett 307-320:Cere 321-328:Cock 329:Form 330-347:Coup 348-384:Dinn
385-394:Prep 395-403:Cere 404-406:Grou 407-410:Cock 411-420:Coup 421-426:Soir
455-462:Rehe 463-468:gett 469-476:cere 477-479:Cock 480-495:Coup 496-519:Part
427-454:Jour
520-532:Enga 533-535:Prep 536-544:Cere 545:Grou 546-557:Coup 558-577:Soir"""
SCENE = {}
for tok in SCENES_RAW.split():
    rng, sc = tok.split(":")
    a, _, b = rng.partition("-")
    for i in range(int(a), int(b or a) + 1):
        SCENE[i] = sc.lower()


def orient(i):
    im = IMG[i]
    r = im["w"] / im["h"]
    return "W" if r > 1.45 else ("L" if r > 1 else "P")


def src(i, size="large"):
    return IMG[i][size]


# --------------------------------------------------------------------------- copy
SITE = "https://julien-test.ovh"
EMAIL = "hello@julien-bonjour.fr"
INSTAGRAM = "https://www.instagram.com/julien_bonjour_photographe/"
FACEBOOK = "https://www.facebook.com/julienbonjourphotographe"
FAMILY_SITE = "https://julien-bonjour.fr/"
REVIEWS = "https://julien-bonjour.fr/avis/"
FPRI = ' fetchpriority="high"'
LAZY = ' loading="lazy"'
DPLUS = ' data-plus="1"'

URL = {
    "en": {"home": "/", "portfolio": "/portfolio/", "investment": "/investment/", "about": "/about/", "enquire": "/enquire/"},
    "fr": {"home": "/fr/", "portfolio": "/fr/portfolio/", "investment": "/fr/investissement/", "about": "/fr/a-propos/", "enquire": "/fr/contact/"},
}

PRESS = ["Martha Stewart Weddings", "Wedding Chicks", "Ruffled", "French Wedding Style", "Zankyou", "Bodas y Weddings",
         "Hochzeitsguide", "Amber Muse", "Oh So Beautiful Paper", "La Mariée aux Pieds Nus", "Un Beau Jour",
         "Wedding Secret", "Le Blog de Madame C", "Albe Éditions", "Adore"]

STORIES = [
    {
        "key": "B", "slug": "alex-anthony-lake-annecy", "names": "Alex & Anthony", "cover": 299, "thumb": 292, "row": 292,
        "place": {"en": "Lake Annecy, French Alps", "fr": "Lac d'Annecy, Alpes françaises"},
        "when": {"en": "September 2026", "fr": "Septembre 2026"},
        "what": {"en": "Two days", "fr": "Deux jours"},
        "high": {"en": "Riva boats · Palace · Aerial act", "fr": "Bateaux Riva · Palace · Acrobate aérienne"},
        "intro": {
            "en": "A welcome dinner on the water and vintage Riva boats gliding across the lake. The next day, a palace overlooking the mountains, and a party that ended beneath the chandeliers — with an aerial act suspended above the dance floor.",
            "fr": "Un dîner de bienvenue au bord de l'eau et des Riva qui glissent sur le lac. Le lendemain, un palace face aux montagnes, et une fête qui s'est terminée sous les lustres — avec une acrobate suspendue au-dessus de la piste.",
        },
        "caps": {"welc": 10, "gett": 5, "cere": 9, "cock": 5, "form": 0, "coup": 10, "dinn": 14},
        "must": [292, 293, 299, 349, 353, 356, 371, 378, 380],
    },
    {
        "key": "F", "slug": "liliane-alexandre-french-riviera", "names": "Liliane & Alexandre", "cover": 546, "thumb": 524, "row": 524,
        "place": {"en": "French Riviera", "fr": "Côte d'Azur"},
        "when": {"en": "August 2025", "fr": "Août 2025"},
        "what": {"en": "Engagement & wedding", "fr": "Fiançailles & mariage"},
        "high": {"en": "Cliff-top vows · Sparklers", "fr": "Cérémonie face à la mer · Fontaines d'étincelles"},
        "intro": {
            "en": "It began on the rocks of the Riviera, wind in the hair, for the engagement session — and ended on a terrace high above the sea, under ribbons of gold light and a curtain of sparklers.",
            "fr": "Tout a commencé sur les rochers de la Riviera, cheveux au vent, pour la séance de fiançailles — et s'est terminé sur une terrasse au-dessus de la mer, sous des rubans de lumière dorée et une pluie d'étincelles.",
        },
        "caps": {"enga": 11, "prep": 3, "cere": 9, "grou": 1, "coup": 10, "soir": 16},
        "must": [524, 536, 546, 558, 563, 571, 574, 577],
    },
    {
        "key": "D", "slug": "elisa-eivind-chateau-wedding", "names": "Elisa & Eivind", "cover": 492, "thumb": 491, "row": 491,
        "place": {"en": "Burgundy, France", "fr": "Bourgogne, France"},
        "when": {"en": "October 2024", "fr": "Octobre 2024"},
        "what": {"en": "Two days", "fr": "Deux jours"},
        "high": {"en": "Rehearsal dinner · 2CV · Party", "fr": "Dîner de répétition · 2CV · Fête"},
        "intro": {
            "en": "An autumn weekend in a French château: a rehearsal dinner beneath stone vaults, a turquoise 2CV waiting at the gates, golden leaves — and a dance floor that never emptied.",
            "fr": "Un week-end d'automne dans un château français : un dîner de répétition sous les voûtes, une 2CV turquoise qui attend devant les grilles, des feuilles dorées — et une piste de danse qui ne s'est jamais vidée.",
        },
        "caps": {"rehe": 6, "gett": 5, "cere": 8, "cock": 3, "coup": 13, "part": 16},
        "must": [484, 491, 492, 508, 512, 513],
    },
    {
        "key": "C", "slug": "annabel-dan-lake-annecy", "names": "Annabel & Dan", "cover": 392, "thumb": 393, "row": 393,
        "place": {"en": "Lake Annecy, French Alps", "fr": "Lac d'Annecy, Alpes françaises"},
        "when": {"en": "August 2025", "fr": "Août 2025"},
        "what": {"en": "Lakeside wedding", "fr": "Mariage au bord du lac"},
        "high": {"en": "Riva arrival · Lakeside vows", "fr": "Arrivée en Riva · Cérémonie face au lac"},
        "intro": {
            "en": "Bridesmaids in powder blue, the groom's party arriving by Riva, vows facing the lake and a dinner in the salons of a palace as the light turned gold.",
            "fr": "Des demoiselles d'honneur en bleu poudré, l'arrivée du marié et de ses témoins en Riva, des vœux face au lac et un dîner dans les salons d'un palace quand la lumière devient dorée.",
        },
        "caps": {},
        "must": [],
    },
    {
        "key": "A", "slug": "bastide-du-roy-editorial", "names": "Bastide du Roy", "cover": 257, "thumb": 247, "row": 247,
        "place": {"en": "Côte d'Azur", "fr": "Côte d'Azur"},
        "when": {"en": "April 2026", "fr": "Avril 2026"},
        "what": {"en": "Editorial", "fr": "Éditorial"},
        "high": {"en": "Olive groves · Pastel florals", "fr": "Oliviers centenaires · Fleurs pastel"},
        "intro": {
            "en": "An editorial among centuries-old olive trees and the gardens of a Provençal bastide: pastel florals, a cellist by the fountain, and colour rising above the pool.",
            "fr": "Un éditorial parmi les oliviers centenaires et les jardins d'une bastide provençale : fleurs pastel, une violoncelliste près de la fontaine et des couleurs qui s'élèvent au-dessus de la piscine.",
        },
        "caps": {"shoo": 44, "back": 2, "publ": 5},
        "must": [],
    },
]

CHAPTERS = {
    "welc": ("The welcome dinner", "Le dîner de bienvenue"),
    "gett": ("Getting ready", "Les préparatifs"),
    "prep": ("Getting ready", "Les préparatifs"),
    "cere": ("The ceremony", "La cérémonie"),
    "cock": ("Cocktail hour", "Le cocktail"),
    "grou": ("Cocktail hour", "Le cocktail"),
    "form": ("Cocktail hour", "Le cocktail"),
    "coup": ("Just the two of them", "Rien qu'à deux"),
    "dinn": ("Dinner & the party", "Le dîner & la fête"),
    "part": ("The party", "La fête"),
    "soir": ("Into the night", "Jusqu'au bout de la nuit"),
    "rehe": ("The rehearsal dinner", "Le dîner de répétition"),
    "enga": ("The engagement", "Les fiançailles"),
    "shoo": ("The editorial", "L'éditorial"),
    "back": ("Behind the scenes", "Les coulisses"),
    "publ": ("As published", "La publication"),
    "jour": ("The design", "La décoration"),
}

QUOTES = {
    "en": [
        ("They captured all the magic of our day with incredible accuracy. Their presence is so natural you almost forget they are there — and yet they miss nothing.", "Alice & Loïc"),
        ("The photographs are simply magnificent. They perfectly capture the emotions and the magic of every moment.", "Liliane & Alexandre · French Riviera"),
        ("The photo story is simply magnificent — you managed to elevate every single moment.", "Caleb & Myriam"),
    ],
    "fr": [
        ("Ils ont su retranscrire toute la magie de notre journée avec une justesse incroyable. Leur présence est tellement naturelle qu'on en oublierait presque leur présence, et pourtant, ils ne manquent rien.", "Alice & Loïc"),
        ("Les photos sont tout simplement magnifiques. Elles capturent parfaitement les émotions et la magie de chaque instant.", "Liliane & Alexandre · Côte d'Azur"),
        ("Le reportage photo est juste magnifique, vous avez réussi à sublimer chaque instant.", "Caleb & Myriam"),
    ],
}

T = {
    "en": {
        "nav": [("portfolio", "Portfolio"), ("investment", "Investment"), ("about", "About")],
        "menu": [("home", "Home"), ("portfolio", "Portfolio"), ("investment", "Investment"), ("about", "About"), ("enquire", "Enquire")],
        "cta": "Enquire", "other": "FR", "tag": "Photographers",
        "ftr_big": "Let’s create something <span class=\"jt-italic\">unforgettable</span>",
        "ftr_about": "Destination wedding photographers based between Annecy &amp; Geneva. France · Switzerland · Italy · Worldwide.",
        "ftr_explore": "Explore", "ftr_contact": "Contact", "ftr_lang": "Language",
        "ftr_extra": None,
        "rights": "© 2026 Julien Bonjour Photographie", "cookies": "Cookie policy",
        "view": "View", "drag": "Drag",
    },
    "fr": {
        "nav": [("portfolio", "Portfolio"), ("investment", "Investissement"), ("about", "À propos")],
        "menu": [("home", "Accueil"), ("portfolio", "Portfolio"), ("investment", "Investissement"), ("about", "À propos"), ("enquire", "Contact")],
        "cta": "Contact", "other": "EN", "tag": "Photographes",
        "ftr_big": "Créons quelque chose <span class=\"jt-italic\">d'inoubliable</span>",
        "ftr_about": "Photographes de mariage de destination, entre Annecy &amp; Genève. France · Suisse · Italie · Monde entier.",
        "ftr_explore": "Explorer", "ftr_contact": "Contact", "ftr_lang": "Langue",
        "ftr_extra": ("Séances famille &amp; couple →", FAMILY_SITE),
        "rights": "© 2026 Julien Bonjour Photographie", "cookies": "Politique de cookies",
        "view": "Voir", "drag": "Glisser",
    },
}


def story_url(lang, s):
    return URL[lang]["portfolio"] + s["slug"] + "/"


# --------------------------------------------------------------------------- partials
def header(lang):
    t = T[lang]
    u = URL[lang]
    other = "fr" if lang == "en" else "en"
    nav = "".join(f'<li class="jt-hdr__item"><a class="jt-hdr__link" href="{u[k]}">{label}</a></li>' for k, label in t["nav"])
    menu = "".join(f'<li class="jt-menu__item"><a class="jt-menu__link" href="{u[k]}">{label}</a></li>' for k, label in t["menu"])
    return f"""<header class="jt-hdr">
<ul class="jt-hdr__nav">{nav}</ul>
<a class="jt-hdr__logo" href="{u['home']}" aria-label="Julien Bonjour"><span class="jt-hdr__brand">Julien Bonjour</span><span class="jt-hdr__tag">{t['tag']}</span></a>
<div class="jt-hdr__right"><a class="jt-hdr__lang" href="{URL[other]['home']}" hreflang="{other}">{t['other']}</a><a class="jt-hdr__cta" href="{u['enquire']}">{t['cta']}</a><button class="jt-burger" type="button" aria-label="Menu" aria-expanded="false"><span class="jt-burger__bar jt-burger__bar--a"></span><span class="jt-burger__bar jt-burger__bar--b"></span></button></div>
</header>
<nav class="jt-menu" aria-label="Menu">
<ul class="jt-menu__list">{menu}</ul>
<div class="jt-menu__foot"><a class="jt-menu__foot-link" href="{INSTAGRAM}" target="_blank" rel="noopener">Instagram</a><a class="jt-menu__foot-link" href="{URL[other]['home']}" data-lang-switch="1">{'Français' if lang == 'en' else 'English'}</a></div>
</nav>"""


FOOT = {
    "en": dict(
        tag="Wedding photographers", where="Annecy &amp; Geneva, across Europe",
        h_site="Explore", h_follow="Follow", h_write="Write to us",
        legal=[("Legal notice", "/legal-notice/"), ("Privacy policy", "/privacy-policy/")],
        other=("Français", "fr"),
    ),
    "fr": dict(
        tag="Photographes de mariage", where="Annecy &amp; Genève, partout en Europe",
        h_site="Explorer", h_follow="Suivre", h_write="Nous écrire",
        legal=[("Mentions légales", "/fr/mentions-legales/"), ("Confidentialité", "/fr/confidentialite/")],
        other=("English", "en"),
    ),
}


def footer(lang):
    t = T[lang]
    f = FOOT[lang]
    u = URL[lang]
    nav = "".join(f'<a class="jt-foot__link" href="{u[k]}">{label}</a>' for k, label in t["menu"][1:])
    follow = (f'<a class="jt-foot__link" href="{INSTAGRAM}" target="_blank" rel="noopener">Instagram</a>'
              f'<a class="jt-foot__link" href="{FACEBOOK}" target="_blank" rel="noopener">Facebook</a>')
    if t["ftr_extra"]:
        follow += f'<a class="jt-foot__link" href="{t["ftr_extra"][1]}" target="_blank" rel="noopener">{t["ftr_extra"][0]}</a>'
    legal = "".join(f'<a class="jt-foot__small-link" href="{href}">{label}</a>' for label, href in f["legal"])
    return f"""<footer class="jt-foot">
<div class="jt-foot__grid">
<div class="jt-foot__brand"><span class="jt-foot__name">Julien Bonjour</span><span class="jt-foot__where">{f['tag']}<br>{f['where']}</span></div>
<div class="jt-foot__col"><span class="jt-foot__label">{f['h_site']}</span>{nav}</div>
<div class="jt-foot__col"><span class="jt-foot__label">{f['h_follow']}</span>{follow}</div>
<div class="jt-foot__col"><span class="jt-foot__label">{f['h_write']}</span><a class="jt-foot__link" href="mailto:{EMAIL}">{EMAIL}</a></div>
</div>
<div class="jt-foot__bottom"><span class="jt-foot__legal">{legal}</span><a class="jt-foot__small-link" href="{URL[f["other"][1]]["home"]}" hreflang="{f["other"][1]}" data-lang-switch="1">{f["other"][0]}</a></div>
</footer>"""

def page_open(lang, alt, hdr_light=True, extra_class=""):
    cls = "jt-page" + (" " + extra_class if extra_class else "")
    return f'<div class="{cls}" data-lang="{lang}" data-alt="{alt}" data-hdr="{"light" if hdr_light else "dark"}">'


def ph(i, cls="jt-ph", alt="", size="large", reveal=True, cap=None, lb=True):
    im = IMG[i]
    a = escape(alt)
    rev = ' data-reveal="1"' if reveal else ""
    capt = f'<span class="jt-ph__cap">{cap}</span>' if cap else ""
    return (f'<figure class="{cls}"{rev}><img class="jt-ph__img" src="{src(i, size)}" alt="{a}" '
            f'width="{im["w"]}" height="{im["h"]}" loading="lazy">{capt}</figure>')


def quotes_section(lang, cls="jt-section jt-section--aqua"):
    qs = QUOTES[lang]
    items = "".join(
        f'<blockquote class="jt-quote{" is-active" if k == 0 else ""}"><span class="jt-quote__mark">“</span><p class="jt-quote__text">{escape(q)}</p><cite class="jt-quote__who">{escape(w)}</cite></blockquote>'
        for k, (q, w) in enumerate(qs))
    dots = "".join(f'<span class="jt-quotes__dot" role="button" tabindex="0" aria-label="{k + 1}"></span>' for k in range(len(qs)))
    label = "Kind words" if lang == "en" else "Ils nous ont fait confiance"
    rating = ("5.0 / 5 — 63 reviews" if lang == "en" else "5,0 / 5 — 63 avis")
    return f"""<section class="{cls}">
<div class="jt-wrap jt-quotes">
<span class="jt-eyebrow">{label}</span>
<div class="jt-quotes__stage">{items}</div>
<div class="jt-quotes__nav">{dots}</div>
<a class="jt-link" href="{REVIEWS}" target="_blank" rel="noopener">{rating}</a>
</div>
</section>"""


def band_section(lang, img=563, copy=None):
    if copy:
        eb, h, btn = copy
    elif lang == "en":
        eb, h, btn = "Now booking 2027 &amp; 2028", "Let’s make it <span class=\"jt-italic\">unforgettable</span>", "Enquire"
    else:
        eb, h, btn = "Réservations 2027 &amp; 2028 ouvertes", "Rendons ce jour <span class=\"jt-italic\">inoubliable</span>", "Nous contacter"
    return f"""<section class="jt-band">
<img class="jt-band__img" src="{IMG[img]['full']}" alt="" loading="lazy" decoding="async" data-speed="0.2">
<div class="jt-band__shade"></div>
<div class="jt-band__inner"><span class="jt-eyebrow">{eb}</span><h2 class="jt-h2" data-split="1">{h}</h2><a class="jt-btn jt-btn--light" href="{URL[lang]['enquire']}">{btn}</a></div>
</section>"""


def planners_section(lang, copy=None):
    if copy:
        eb, h, txt, btn = copy
    elif lang == "en":
        eb, h = "Planners &amp; venues", "Made for <span class=\"jt-italic\">grand</span> celebrations"
        txt = ("We know the rhythm of a luxury, multi-day celebration — timelines, vendors, discretion. "
               "We work hand in hand with your team and share a curated gallery of your design work, ready for your portfolio and press.")
        btn = "Collaborate with us"
    else:
        eb, h = "Wedding planners &amp; lieux", "Pensés pour les <span class=\"jt-italic\">grandes</span> célébrations"
        txt = ("Nous connaissons le rythme d'une célébration de luxe sur plusieurs jours — rétroplanning, prestataires, discrétion. "
               "Nous travaillons main dans la main avec votre équipe et partageons une galerie dédiée à votre travail de décoration, prête pour votre portfolio et la presse.")
        btn = "Travailler ensemble"
    return f"""<section class="jt-section">
<div class="jt-wrap jt-planners">
{ph(442, "jt-ph jt-planners__img", "Floral design by the wedding team", "large")}
<div class="jt-planners__body"><span class="jt-eyebrow">{eb}</span><h2 class="jt-h2" data-split="1">{h}</h2><p class="jt-text">{txt}</p><a class="jt-link" href="{URL[lang]['enquire']}">{btn}</a></div>
{ph(452, "jt-ph jt-planners__img jt-planners__img--low", "Reception design in a château", "large")}
</div>
</section>"""


# --------------------------------------------------------------------------- home
# Hero: six frames hung around the title, each one a slow carousel
WALL = [[411, 245, 302], [292, 287, 387], [371, 370, 341], [491, 509, 477], [536, 414, 520], [577, 288, 260]]
MANIFEST_IMGS = (524, 552)
GROW_IMG = 563
# The exhibition: twenty frames, each with a museum label
EXPO = [356, 233, 380, 247, 393, 571, 484, 235, 378, 513, 244, 574, 508, 385, 353, 497, 550, 512, 390, 486]
EXPO_SLOTS = ["a", "b", "c", "d"]
# Museum labels: the moment and the place, never a date
CARTEL = {
    356: ("The grand entrance, Lake Annecy", "L’entrée, lac d’Annecy"),
    233: ("A bastide above the Riviera", "Une bastide au-dessus de la Riviera"),
    380: ("After midnight, Lake Annecy", "Après minuit, lac d’Annecy"),
    247: ("Colour study, Côte d’Azur", "Étude de couleurs, Côte d’Azur"),
    393: ("Guests on the water, Lake Annecy", "Les invités sur l’eau, lac d’Annecy"),
    571: ("First dance under sparklers, French Riviera", "Première danse sous les étincelles, Riviera"),
    484: ("Golden October, Burgundy", "Octobre doré, Bourgogne"),
    235: ("The garden marquee, from above", "Le chapiteau, vu du ciel"),
    378: ("The last dance, Lake Annecy", "La dernière danse, lac d’Annecy"),
    513: ("A song for the bride, Burgundy", "Une chanson pour la mariée, Bourgogne"),
    244: ("The arrival, Bastide du Roy", "L’arrivée, Bastide du Roy"),
    574: ("The dance floor, French Riviera", "La piste de danse, Riviera"),
    508: ("The champagne tower, Burgundy", "La pyramide de champagne, Bourgogne"),
    385: ("A palace on the lake", "Un palace au bord du lac"),
    353: ("Ceremony gardens, Lake Annecy", "Les jardins de la cérémonie, lac d’Annecy"),
    497: ("Through the flowers, Burgundy", "À travers les fleurs, Bourgogne"),
    550: ("Walking away together, French Riviera", "Partir ensemble, Riviera"),
    512: ("Champagne before dinner, Burgundy", "Champagne avant le dîner, Bourgogne"),
    390: ("Getting ready, Lake Annecy", "Les préparatifs, lac d’Annecy"),
    486: ("By the water, Burgundy", "Au bord de l’eau, Bourgogne"),
}
HOME_STORIES = ["B", "F", "D"]
GAL_HOVER = {"B": 293, "F": 529, "D": 455}
PRESS_NAMES = ["Martha Stewart Weddings", "Ruffled", "Wedding Chicks", "French Wedding Style", "Amber Muse"]
DUO_IMGS = (961, 442)
VOICE_IMGS = (415, 558, 488)
WATER_IMG = 292
IG = ["Detl5x_iJDD", "DePGR71iKuF", "DeEzIABCJTr", "DeClvJbCB6B", "DeMEO6miCnN", "DNyTC_u0CUZ"]

HOME_COPY = {
    "en": dict(
        eyebrow="Wedding photographers · France, Italy, Switzerland",
        title=("Some days", "are meant", "to be framed."),
        sub="Art-driven photography for celebrations that deserve more than a record.",
        statement="We don’t direct love. We read it.",
        manifest="A celebration of this calibre is made of a thousand details, and of moments that happen only once. We move through it quietly, with an editorial eye, so that every image feels like you on your finest day, never like a pose. Then we make it something you will want on your walls, not only on your phone.",
        phrase=("Nothing staged.", "Everything felt."), grow_cap="French Riviera, at twilight",
        words=["Candid", "Sun-drenched", "Unhurried", "Editorial", "Tender", "Golden", "Joyful", "Timeless"],
        expo_link="Enter the portfolio",
        gal_eb="Full galleries", gal_h="Told in full", gal_go="View the gallery",
        duo_eb="About us", duo_h="Shaped by the most demanding celebrations",
        duo_txt="Since 2012, we have photographed weddings across France, Italy and Switzerland: palaces on the lakes, châteaux, villas above the sea. One of us follows the moment as it unfolds, the other composes the details that make it yours. Two perspectives, one signature, and a discretion your guests will never notice.",
        duo_link="Meet us", press="Published in",
        voices="In their words",
        ask_eb="Your day", ask_h=("Tell us", "about your day."),
        ask_txt="Where, when, and what matters most to you. We will take it from there.",
        ig_eb="On Instagram", ig_link="The season, as it unfolds",
    ),
    "fr": dict(
        eyebrow="Photographes de mariage · France, Italie, Suisse",
        title=("Certains jours", "méritent", "un cadre."),
        sub="Une photographie d’auteur pour les célébrations qui méritent plus qu’un souvenir.",
        statement="Nous ne mettons pas l’amour en scène. Nous le lisons.",
        manifest="Une célébration de ce niveau est faite de mille détails, et d’instants qui n’arrivent qu’une fois. Nous la traversons discrètement, avec un regard éditorial, pour que chaque image vous ressemble au plus beau de vous-mêmes, jamais à une pose. Puis nous en faisons des images que vous voudrez sur vos murs, pas seulement dans votre téléphone.",
        phrase=("Rien de posé.", "Tout de ressenti."), grow_cap="La Riviera, au crépuscule",
        words=["Sincère", "Solaire", "Sans hâte", "Éditorial", "Tendre", "Doré", "Joyeux", "Intemporel"],
        expo_link="Entrer dans le portfolio",
        gal_eb="Galeries complètes", gal_h="Racontés en entier", gal_go="Voir la galerie",
        duo_eb="À propos", duo_h="Façonnés par les célébrations les plus exigeantes",
        duo_txt="Depuis 2012, nous photographions des mariages en France, en Italie et en Suisse : palaces au bord des lacs, châteaux, villas au-dessus de la mer. L’un suit l’instant tel qu’il se déroule, l’autre compose les détails qui le rendent unique. Deux regards, une seule signature, et une discrétion que vos invités ne remarqueront jamais.",
        duo_link="Nous rencontrer", press="Publiés dans",
        voices="Leurs mots",
        ask_eb="Votre journée", ask_h=("Racontez-nous", "votre journée."),
        ask_txt="Où, quand, et ce qui compte le plus pour vous. Nous nous occupons du reste.",
        ig_eb="Sur Instagram", ig_link="La saison, au fil des jours",
    ),
}


def fig(i, cls, alt, cap=None, speed=None, size="large"):
    im = IMG[i]
    sp = f' data-speed="{speed}"' if speed is not None else ""
    capt = f'<figcaption class="jt-cartel">{cap}</figcaption>' if cap else ""
    return (f'<figure class="{cls}"{sp}><img class="jt-fig__img" src="{src(i, size)}" alt="{alt}" '
            f'width="{im["w"]}" height="{im["h"]}"{LAZY} decoding="async">{capt}</figure>')


def home(lang):
    fr = lang == "fr"
    c = HOME_COPY[lang]
    alt = URL["fr" if not fr else "en"]["home"]
    img_alt = "Wedding photography by Julien Bonjour" if not fr else "Photographie de mariage par Julien Bonjour"

    wall = []
    for k, ids in enumerate(WALL):
        imgs = "".join(
            f'<img class="jt-fig__img jt-wall__slide" src="{src(i)}" alt="{img_alt}" width="{IMG[i]["w"]}" height="{IMG[i]["h"]}"{FPRI if n == 0 else ""} decoding="async">'
            for n, i in enumerate(ids))
        wall.append(f'<figure class="jt-wall__f jt-wall__f--{k + 1} jt-wall__f--carousel">{imgs}</figure>')
    title = "".join(f'<span class="jt-wall__line">{t}</span>' for t in c["title"])

    ma, mb = MANIFEST_IMGS
    gi = IMG[GROW_IMG]
    expo = "".join(
        fig(i, f"jt-expo__f jt-expo__f--{EXPO_SLOTS[k % 4]}{'l' if orient(i) in 'LW' else 'p'}", img_alt, CARTEL[i][1 if fr else 0])
        for k, i in enumerate(EXPO))
    words = "".join(f'<span class="jt-expo__word">{w}</span>' for w in c["words"])

    gal = []
    for key in HOME_STORIES:
        s = next(x for x in STORIES if x["key"] == key)
        ci = IMG[s["cover"]]
        hi = IMG[GAL_HOVER[key]]
        gal.append(f'<a class="jt-gal__card" href="{story_url(lang, s)}"><span class="jt-gal__media"><img class="jt-gal__img" src="{ci["full"]}" alt="{escape(s["names"] + " — " + s["place"][lang])}" width="{ci["w"]}" height="{ci["h"]}" loading="lazy" decoding="async"><img class="jt-gal__img jt-gal__img--alt" src="{hi["full"]}" alt="" width="{hi["w"]}" height="{hi["h"]}" loading="lazy" decoding="async"></span><span class="jt-gal__place">{s["place"][lang]}</span><span class="jt-gal__go">{c["gal_go"]}</span></a>')

    press = "".join(f'<li class="jt-press2__item">{p}</li>' for p in PRESS_NAMES)
    pa, pb = DUO_IMGS

    qs = QUOTES[lang]
    quotes = "".join(
        f'<blockquote class="jt-quote"><p class="jt-quote__text">{escape(q)}</p><cite class="jt-quote__who">{escape(w)}</cite></blockquote>'
        for q, w in qs)
    dots = "".join(f'<span class="jt-quotes__dot" role="button" tabindex="0" aria-label="{k + 1}"></span>' for k in range(len(qs)))
    vimgs = "".join(
        f'<img class="jt-fig__img jt-voices__img" src="{src(i)}" alt="{img_alt}" width="{IMG[i]["w"]}" height="{IMG[i]["h"]}" loading="lazy" decoding="async">'
        for i in VOICE_IMGS)

    wi = IMG[WATER_IMG]
    ask_h = "".join(f'<span class="jt-ask__line">{t}</span>' for t in c["ask_h"])
    ig = "".join(
        f'<a class="jt-ig__item" href="https://www.instagram.com/p/{code}/" target="_blank" rel="noopener"><img class="jt-ig__img" src="{SITE}/wp-content/uploads/2026/10/jb-ig-{code}.jpg" alt="Instagram — Julien Bonjour" width="480" height="640" loading="lazy" decoding="async"></a>'
        for code in IG)

    return f"""{page_open(lang, alt, hdr_light=False, extra_class="jt-home")}
<section class="jt-wall">
<div class="jt-wall__photos">{''.join(wall)}</div>
<div class="jt-wall__text"><span class="jt-eyebrow">{c['eyebrow']}</span><h1 class="jt-wall__title">{title}</h1><p class="jt-wall__sub">{c['sub']}</p></div>
</section>
<section class="jt-manifest">
<div class="jt-wrap jt-manifest__grid">
<h2 class="jt-manifest__statement" data-split="1">{c['statement']}</h2>
<p class="jt-manifest__text" data-words="1">{c['manifest']}</p>
{fig(ma, "jt-manifest__f jt-manifest__f--a", img_alt, speed=0.15)}
{fig(mb, "jt-manifest__f jt-manifest__f--b", img_alt, speed=-0.12)}
</div>
</section>
<section class="jt-grow">
<div class="jt-grow__frame"><img class="jt-grow__img" src="{gi['full']}" alt="{escape(c['grow_cap'])}" width="{gi['w']}" height="{gi['h']}" loading="lazy" decoding="async"><div class="jt-grow__shade"></div></div>
<h2 class="jt-grow__title"><span class="jt-grow__line"><span class="jt-grow__in">{c['phrase'][0]}</span></span><span class="jt-grow__line"><span class="jt-grow__in">{c['phrase'][1]}</span></span></h2>
<span class="jt-grow__cap">{c['grow_cap']}</span>
</section>
<section class="jt-expo">
<div class="jt-expo__grid">{expo}</div>
<div class="jt-expo__words" aria-hidden="true"><div class="jt-expo__sticky"><div class="jt-expo__stage">{words}</div></div></div>
</section>
<section class="jt-gal">
<div class="jt-gal__head"><span class="jt-eyebrow">{c['gal_eb']}</span><h2 class="jt-gal__title" data-split="1">{c['gal_h']}</h2></div>
<div class="jt-gal__grid">{''.join(gal)}</div>
</section>
<section class="jt-duo">
{fig(pa, "jt-duo__f jt-duo__f--main", "Julien &amp; Mélanie")}
{fig(pb, "jt-duo__f jt-duo__f--detail", img_alt, speed=-0.3)}
<div class="jt-duo__body"><span class="jt-eyebrow">{c['duo_eb']}</span><h2 class="jt-h2" data-split="1">{c['duo_h']}</h2><p class="jt-text">{c['duo_txt']}</p><p class="jt-duo__sign">Julien &amp; Mélanie</p><a class="jt-link" href="{URL[lang]['about']}">{c['duo_link']}</a></div>
<div class="jt-press2"><span class="jt-eyebrow jt-press2__label">{c['press']}</span><ul class="jt-press2__list">{press}</ul></div>
</section>
<section class="jt-voices">
<div class="jt-quotes jt-quotes--left jt-voices__body"><span class="jt-eyebrow">{c['voices']}</span><div class="jt-quotes__stage">{quotes}</div><div class="jt-quotes__nav">{dots}</div></div>
<figure class="jt-voices__f">{vimgs}</figure>
</section>
<section class="jt-ig">
<div class="jt-ig__head"><span class="jt-eyebrow">{c['ig_eb']}</span><a class="jt-ig__handle" href="{INSTAGRAM}" target="_blank" rel="noopener">{c['ig_link']} →</a></div>
<div class="jt-ig__grid">{ig}</div>
</section>
<section class="jt-ask">
<img class="jt-ask__water" src="{wi['full']}" alt="" width="{wi['w']}" height="{wi['h']}" loading="lazy" decoding="async">
<div class="jt-ask__tint"></div>
<div class="jt-ask__intro"><span class="jt-eyebrow">{c['ask_eb']}</span><h2 class="jt-ask__title">{ask_h}</h2><p class="jt-ask__text">{c['ask_txt']}</p></div>
<div class="jt-ask__form"></div>
</section>
</div>"""

# --------------------------------------------------------------------------- portfolio index
def portfolio(lang):
    fr = lang == "fr"
    alt = URL["en" if fr else "fr"]["portfolio"]
    cards = []
    n = len(STORIES)
    for k, s in enumerate(STORIES):
        cards.append(f"""<a class="jt-stack__card" href="{story_url(lang, s)}" data-cursor="{T[lang]['view']}">
<img class="jt-stack__img" src="{IMG[s['cover']]['full']}" alt="{escape(s['names'] + ' — ' + s['place'][lang])}" width="{IMG[s['cover']]['w']}" height="{IMG[s['cover']]['h']}" loading="{'eager' if k == 0 else 'lazy'}" decoding="async">
<span class="jt-stack__shade"></span>
<span class="jt-stack__body"><span class="jt-stack__info"><span class="jt-eyebrow">0{k + 1} / 0{n}</span><span class="jt-stack__title">{s['names']}</span><span class="jt-stack__meta"><span class="jt-stack__m">{s['place'][lang]}</span><span class="jt-stack__m">{s['what'][lang]}</span><span class="jt-stack__m">{s['when'][lang]}</span></span></span><span class="jt-stack__go">{'Voir l’histoire' if fr else 'View the story'}</span></span>
</a>""")
    head = ("Portfolio", "Nos histoires", "Cinq célébrations, racontées du premier toast à la dernière danse. Faites défiler, puis entrez dans celle qui vous ressemble.") if fr else \
           ("Portfolio", "The stories", "Five celebrations, told from the first toast to the last dance. Scroll through, then step into the one that feels like yours.")
    return f"""{page_open(lang, alt, hdr_light=False)}
<section class="jt-phead">
<div class="jt-wrap jt-phead__inner"><span class="jt-eyebrow">{head[0]}</span><h1 class="jt-h1" data-split="1">{head[1]}</h1><p class="jt-lead jt-phead__intro" data-fade="0.2">{head[2]}</p></div>
</section>
<section class="jt-stack">{''.join(cards)}</section>
{band_section(lang)}
</div>"""


# --------------------------------------------------------------------------- stories
def select_story_images(s):
    ids = sorted(i for i in IMG if IMG[i]["g"] == s["key"])
    if s["key"] == "C":
        return ids
    by = {}
    for i in ids:
        by.setdefault(SCENE[i], []).append(i)
    out = []
    for sc, lst in by.items():
        cap = s["caps"].get(sc, len(lst))
        if cap >= len(lst):
            out += lst
            continue
        if cap <= 0:
            continue
        must = [i for i in lst if i in s["must"]]
        rest = [i for i in lst if i not in s["must"]]
        need = max(0, cap - len(must))
        step = len(rest) / need if need else 0
        pick = [rest[int(k * step)] for k in range(need)] if need else []
        out += sorted(set(must + pick))
    return sorted(out, key=lambda i: ids.index(i))


ROW_CYCLE = ["bleed", "duo", "pair", "solo", "trio", "wide2", "pair-r", "solo-r", "quad", "duo"]
ROW_NEEDS = {"bleed": "L", "duo": "PP", "pair": "LP", "pair-r": "PL", "solo": "P", "solo-r": "P", "trio": "PPP", "wide2": "LL", "quad": "PPPP", "full": "L"}


def layout_rows(ids):
    """Greedy editorial layout: walk the cycle and fill each row with the next fitting images (small lookahead)."""
    queue = list(ids)
    rows = []
    c = 0
    guard = 0
    while queue and guard < 500:
        guard += 1
        kind = ROW_CYCLE[c % len(ROW_CYCLE)]
        c += 1
        need = ROW_NEEDS[kind]
        picked = []
        window = queue[:6]
        ok = True
        for o in need:
            cand = next((i for i in window if i not in picked and (orient(i) == o or (o == "L" and orient(i) == "W"))), None)
            if cand is None:
                ok = False
                break
            picked.append(cand)
        if not ok:
            continue
        for i in picked:
            queue.remove(i)
        rows.append((kind, picked))
        if guard > 400:
            break
    # anything left: single rows
    for i in queue:
        rows.append(("bleed" if orient(i) in "LW" else "solo", [i]))
    return rows


def story(lang, k):
    s = STORIES[k]
    fr = lang == "fr"
    alt = story_url("en" if fr else "fr", s)
    nxt = STORIES[(k + 1) % len(STORIES)]
    ids = select_story_images(s)
    alt_txt = f"{s['names']} — {s['place'][lang]} — {'photo Julien Bonjour' if fr else 'photo by Julien Bonjour'}"

    # group by chapter (merge scenes that share a chapter title)
    chapters = []
    for i in ids:
        title = CHAPTERS[SCENE[i]][1 if fr else 0]
        if not chapters or chapters[-1][0] != title:
            chapters.append((title, []))
        chapters[-1][1].append(i)

    body = []
    for n, (title, lst) in enumerate(chapters):
        body.append(f'<div class="jt-chapter"><span class="jt-chapter__num">{n + 1:02d}</span><h2 class="jt-chapter__title" data-split="1">{title}</h2></div>')
        for kind, pics in layout_rows(lst):
            size = "full" if kind in ("bleed", "full") else "large"
            figs = "".join(ph(i, "jt-ph", alt_txt, size) for i in pics)
            body.append(f'<div class="jt-row jt-row--{kind}">{figs}</div>')

    lbl = dict(place="Lieu" if fr else "Place", when="Quand" if fr else "When", what="Célébration" if fr else "Celebration",
               high="Moments forts" if fr else "Highlights")
    cnt = f"{len(ids)} {'photographies' if fr else 'photographs'}"
    next_lbl = "Histoire suivante" if fr else "Next story"
    return f"""{page_open(lang, alt, extra_class="jt-story")}
<section class="jt-imghero">
<img class="jt-imghero__img" src="{IMG[s['cover']]['full']}" alt="{escape(alt_txt)}" width="{IMG[s['cover']]['w']}" height="{IMG[s['cover']]['h']}" fetchpriority="high" data-speed="0.25">
<div class="jt-imghero__shade"></div>
<div class="jt-imghero__content"><span class="jt-eyebrow">{s['place'][lang]}</span><h1 class="jt-h1" data-split="1">{s['names']}</h1><span class="jt-eyebrow">{cnt}</span></div>
</section>
<section class="jt-section">
<div class="jt-wrap">
<div class="jt-facts" data-fade="0">
<div class="jt-fact"><span class="jt-fact__k">{lbl['place']}</span><span class="jt-fact__v">{s['place'][lang]}</span></div>
<div class="jt-fact"><span class="jt-fact__k">{lbl['when']}</span><span class="jt-fact__v">{s['when'][lang]}</span></div>
<div class="jt-fact"><span class="jt-fact__k">{lbl['what']}</span><span class="jt-fact__v">{s['what'][lang]}</span></div>
<div class="jt-fact"><span class="jt-fact__k">{lbl['high']}</span><span class="jt-fact__v">{s['high'][lang]}</span></div>
</div>
<div class="jt-sintro"><span class="jt-eyebrow">{'L’histoire' if fr else 'The story'}</span><p class="jt-lead" data-fade="0.1">{s['intro'][lang]}</p></div>
{''.join(body)}
</div>
</section>
<a class="jt-next" href="{story_url(lang, nxt)}" data-cursor="{T[lang]['view']}">
<img class="jt-next__img" src="{IMG[nxt['cover']]['full']}" alt="" loading="lazy" decoding="async">
<span class="jt-next__inner"><span class="jt-eyebrow">{next_lbl}</span><span class="jt-h1">{nxt['names']}</span><span class="jt-eyebrow">{nxt['place'][lang]}</span></span>
</a>
{band_section(lang, 536 if s['key'] == 'F' else 563)}
</div>"""


# --------------------------------------------------------------------------- investment
CALC = {
    "en": [
        ("01", "Your wedding day", "Choose the coverage of the big day — always the two of us.", "radio", "day", "", [
            ("Signature", "8 hours · from the ceremony to the first dance", 6900, False),
            ("Full day", "10 hours · from getting ready to the party", 7900, True),
            ("Until the last dance", "12 hours · nothing missed, all night long", 8900, False)]),
        ("02", "Around the wedding", "Multi-day celebrations are where the story truly unfolds.", "checkbox", "around", "1", [
            ("Welcome dinner or rehearsal", "Up to 4 hours, the evening before", 1900, False),
            ("Day-after brunch or pool party", "Up to 3 hours, the day after", 1400, False),
            ("Engagement session at your destination", "Two hours, on a day of your choosing", 1200, False)]),
        ("03", "Elevate", "The details that make the gallery unforgettable.", "checkbox", "plus", "1", [
            ("Aerial drone", "Venue, landscapes and ceremony from above", 650, False),
            ("Content creator", "Same-day reels and stories for you and your guests", 1600, False)]),
        ("04", "Heirloom", "Photographs are meant to be held.", "checkbox", "album", "1", [
            ("Fine-art album", "30 × 30 cm, linen or leather, 60 pages", 1900, False),
            ("Parents' albums", "Two 20 × 20 cm replicas", 900, False)]),
    ],
    "fr": [
        ("01", "Le jour J", "Choisissez la couverture du grand jour — toujours à deux photographes.", "radio", "day", "", [
            ("Signature", "8 heures · de la cérémonie à l'ouverture du bal", 6900, False),
            ("Journée complète", "10 heures · des préparatifs à la fête", 7900, True),
            ("Jusqu'à la dernière danse", "12 heures · rien n'est manqué, toute la nuit", 8900, False)]),
        ("02", "Autour du mariage", "Les célébrations sur plusieurs jours sont celles où l'histoire se déploie vraiment.", "checkbox", "around", "1", [
            ("Dîner de bienvenue ou de répétition", "Jusqu'à 4 heures, la veille", 1900, False),
            ("Brunch ou pool party du lendemain", "Jusqu'à 3 heures, le lendemain", 1400, False),
            ("Séance d'engagement sur place", "Deux heures, le jour de votre choix", 1200, False)]),
        ("03", "Sublimer", "Les détails qui rendent une galerie inoubliable.", "checkbox", "plus", "1", [
            ("Drone", "Le lieu, les paysages et la cérémonie vus du ciel", 650, False),
            ("Content creator", "Reels et stories le jour même, pour vous et vos invités", 1600, False)]),
        ("04", "Héritage", "Les photographies sont faites pour être tenues entre les mains.", "checkbox", "album", "1", [
            ("Album fine art", "30 × 30 cm, lin ou cuir, 60 pages", 1900, False),
            ("Albums parents", "Deux répliques 20 × 20 cm", 900, False)]),
    ],
}


def investment(lang):
    fr = lang == "fr"
    alt = URL["en" if fr else "fr"]["investment"]
    steps = []
    for num, title, hint, typ, name, plus, opts in CALC[lang]:
        cards = "".join(
            f'<label class="jt-opt{" is-on" if on else ""}" data-price="{price}"{DPLUS if plus else ""}><input class="jt-opt__in" type="{typ}" name="{name}" value="{price}"{" checked" if on else ""}><span class="jt-opt__tick"></span><span class="jt-opt__name">{oname}</span><span class="jt-opt__desc">{desc}</span><span class="jt-opt__price"></span></label>'
            for oname, desc, price, on in opts)
        cls = "jt-opts jt-opts--2" if len(opts) == 2 else "jt-opts"
        steps.append(f'<div class="jt-step"><div class="jt-step__head"><span class="jt-step__num">{num}</span><h2 class="jt-step__title">{title}</h2></div><p class="jt-step__hint">{hint}</p><div class="{cls}">{cards}</div></div>')

    if fr:
        c = dict(eb="Investissement", h="Votre collection",
                 intro="Chaque célébration est unique. Composez la couverture qui vous ressemble — l'estimation se met à jour au fil de vos choix, puis nous l'affinons ensemble.",
                 from_="Mariages à partir de 6 900 €", sum_lbl="Investissement estimé",
                 note="Estimation indicative. Déplacements inclus en France, Suisse et Italie ; partout ailleurs sur devis. Votre proposition finale est établie sur mesure.",
                 btn="Demander cette collection", incl_eb="Inclus dans chaque collection", incl_h="L'expérience, <span class=\"jt-italic\">en entier</span>",
                 faq_eb="Questions fréquentes", faq_h="Bon à savoir")
        incl = [("01", "Julien &amp; Mélanie, ensemble", "Deux photographes à chaque mariage, toujours."),
                ("02", "Appel de préparation", "Rétroplanning, lumière, logistique : nous préparons tout avec vous et votre wedding planner."),
                ("03", "Aperçu sous 48 h", "Une première sélection pour revivre la journée quand l'émotion est encore là."),
                ("04", "Galerie privée en ligne", "Téléchargez, partagez et commandez vos tirages depuis une galerie élégante."),
                ("05", "Déplacements en Europe", "France, Suisse et Italie inclus ; partout ailleurs sur devis."),
                ("06", "Discrétion", "Une présence calme et discrète, une direction éditoriale quand il le faut.")]
        faq = [("Voyagez-vous à l'étranger ?", "Oui. Basés entre Annecy et Genève, nous photographions des mariages partout en Europe et au-delà. Les déplacements en France, en Suisse et en Italie sont inclus ; pour les autres destinations, nous vous envoyons une proposition sur mesure."),
               ("Combien de temps à l'avance réserver ?", "La plupart de nos couples réservent 12 à 18 mois à l'avance, en particulier pour les week-ends d'été et les célébrations sur plusieurs jours."),
               ("Travaillez-vous avec notre wedding planner ?", "Toujours. Nous coordonnons le déroulé et la logistique avec votre planner et le lieu, et partageons une galerie dédiée à la décoration avec l'équipe créative."),
               ("Quand recevrons-nous nos photographies ?", "Un aperçu sous 48 heures, puis votre galerie complète sous six à huit semaines."),
               ("Comment réserver notre date ?", "Un contrat signé et un acompte de 30 % réservent votre date ; le solde est dû un mois avant le mariage.")]
    else:
        c = dict(eb="Investment", h="Your collection",
                 intro="Every celebration is unique. Compose the coverage that fits yours — the estimate updates as you go, and we refine it together.",
                 from_="Weddings from €6,900", sum_lbl="Estimated investment",
                 note="Indicative estimate. Travel within France, Switzerland and Italy included; worldwide on request. Your final proposal is tailored to your plans.",
                 btn="Enquire with this collection", incl_eb="Included in every collection", incl_h="The full <span class=\"jt-italic\">experience</span>",
                 faq_eb="Questions", faq_h="Good to know")
        incl = [("01", "Julien &amp; Mélanie, together", "Two photographers at every wedding, always."),
                ("02", "Planning call", "Timeline, light and logistics: we prepare everything with you and your planner."),
                ("03", "Sneak peek in 48 hours", "A first selection to relive the day while the emotion is still there."),
                ("04", "Private online gallery", "Download, share and order prints from one elegant gallery."),
                ("05", "Travel across Europe", "France, Switzerland and Italy included; worldwide on request."),
                ("06", "Discretion", "A calm, unobtrusive presence — with editorial direction when it matters.")]
        faq = [("Do you travel abroad?", "Yes. Based between Annecy and Geneva, we photograph weddings across Europe and beyond. Travel within France, Switzerland and Italy is included; for other destinations we send a tailored proposal."),
               ("How far in advance should we book?", "Most of our couples reserve 12 to 18 months ahead, especially for summer weekends and multi-day celebrations."),
               ("Do you work with our wedding planner?", "Always. We coordinate the timeline and logistics with your planner and venue, and share a curated gallery of the design with the creative team."),
               ("When will we receive our photographs?", "A sneak peek within 48 hours, then your complete gallery within six to eight weeks."),
               ("How do we secure our date?", "A signed agreement and a 30% retainer reserve your date; the balance is due one month before the wedding.")]

    incl_html = "".join(f'<div class="jt-incl__item"><span class="jt-incl__k">{k}</span><h3 class="jt-incl__t">{t}</h3><p class="jt-incl__d">{d}</p></div>' for k, t, d in incl)
    faq_html = "".join(f'<details class="jt-faq__item"><summary class="jt-faq__q">{q}</summary><p class="jt-faq__a">{a}</p></details>' for q, a in faq)
    return f"""{page_open(lang, alt, hdr_light=False)}
<section class="jt-phead">
<div class="jt-wrap jt-phead__inner"><span class="jt-eyebrow">{c['eb']}</span><h1 class="jt-h1" data-split="1">{c['h']}</h1><p class="jt-lead jt-phead__intro" data-fade="0.2">{c['intro']}</p><span class="jt-eyebrow">{c['from_']}</span></div>
</section>
<section class="jt-section" id="calculator">
<div class="jt-wrap jt-calc" data-rate="1.10">
<div class="jt-calc__steps">{''.join(steps)}</div>
<aside class="jt-sum">
<div class="jt-sum__top"><span class="jt-eyebrow">{c['sum_lbl']}</span><div class="jt-sum__cur"><a class="jt-sum__curbtn is-on" href="#" data-cur="EUR">EUR</a><a class="jt-sum__curbtn" href="#" data-cur="USD">USD</a></div></div>
<p class="jt-sum__total">—</p>
<ul class="jt-sum__lines"></ul>
<p class="jt-sum__note">{c['note']}</p>
<a class="jt-btn jt-btn--light jt-sum__btn" href="{URL[lang]['enquire']}">{c['btn']}</a>
</aside>
</div>
</section>
<section class="jt-section jt-section--aqua">
<div class="jt-wrap">
<div class="jt-stories__head"><div class="jt-stories__title"><span class="jt-eyebrow">{c['incl_eb']}</span><h2 class="jt-h2" data-split="1">{c['incl_h']}</h2></div></div>
<div class="jt-incl">{incl_html}</div>
</div>
</section>
<section class="jt-section">
<div class="jt-wrap">
<div class="jt-phead__inner jt-faqhead"><span class="jt-eyebrow">{c['faq_eb']}</span><h2 class="jt-h2" data-split="1">{c['faq_h']}</h2></div>
<div class="jt-faq">{faq_html}</div>
</div>
</section>
{band_section(lang, 536)}
</div>"""


# --------------------------------------------------------------------------- about
def about(lang):
    fr = lang == "fr"
    alt = URL["en" if fr else "fr"]["about"]
    if fr:
        c = dict(eb="À propos", h="Julien &amp; Mélanie",
                 intro="Deux photographes, un même regard. Entre Annecy et Genève, et partout où l'on nous appelle.",
                 s_eb="Notre histoire", s_h="Raconter la fête, <span class=\"jt-italic\">pas seulement</span> la photographier",
                 s_txt1="Depuis plusieurs années, nous photographions des célébrations en France, en Suisse, en Italie et au-delà — des week-ends de mariage au bord des lacs alpins, sur les falaises de la Riviera ou dans des châteaux d'automne.",
                 s_txt2="Deux sensibilités, une seule façon de voir : l'un compose la grande image — le lieu, la lumière, l'ampleur de la journée — pendant que l'autre reste au plus près des gens et de l'émotion. Vous recevez une histoire complète, élégante et vivante.",
                 v_eb="Ce qui nous guide", v=[("Éditorial", "Des images composées et lumineuses, avec l'élégance d'un magazine — jamais figées."),
                                               ("Discret", "Nous nous fondons dans votre célébration pour que vos invités restent eux-mêmes."),
                                               ("Joyeux", "Quand la musique commence, nous sommes sur la piste avec vous — la fête mérite sa propre histoire.")],
                 p_eb="L'expérience", p_h="Du premier message <span class=\"jt-italic\">à l'album</span>",
                 p=[("01", "Votre message", "Racontez-nous votre projet ; nous vous répondons sous 24 heures."),
                    ("02", "La rencontre", "Un appel vidéo pour faire connaissance et parler de votre vision."),
                    ("03", "La préparation", "Déroulé, lieux et lumière préparés avec vous et votre wedding planner."),
                    ("04", "La célébration", "Deux photographes, du premier toast à la dernière danse."),
                    ("05", "Votre héritage", "Un aperçu sous 48 h, votre galerie, puis un album fine art à transmettre.")],
                 pr_eb="Publications", pr_h="Vus dans la presse",
                 portrait="Portrait de Julien &amp; Mélanie — à venir")
    else:
        c = dict(eb="About", h="Julien &amp; Mélanie",
                 intro="Two photographers, one eye. Based between Annecy and Geneva — and wherever you need us.",
                 s_eb="Our story", s_h="Telling the celebration, <span class=\"jt-italic\">not just</span> photographing it",
                 s_txt1="For years we have photographed celebrations across France, Switzerland, Italy and beyond — wedding weekends on alpine lakes, on the cliffs of the Riviera and in autumn châteaux.",
                 s_txt2="Two sensibilities, one way of seeing: one of us composes the grand picture — the venue, the light, the scale of the day — while the other stays close to the people and the emotion. You receive the whole story: elegant, alive and entirely yours.",
                 v_eb="What guides us", v=[("Editorial", "Composed, luminous images with the elegance of a magazine — never stiff."),
                                          ("Discreet", "We blend into your celebration so your guests can simply be themselves."),
                                          ("Joyful", "When the music starts, we’re on the dance floor with you — the party deserves its own story.")],
                 p_eb="The experience", p_h="From first hello <span class=\"jt-italic\">to heirloom</span>",
                 p=[("01", "Your enquiry", "Tell us about your plans; we reply within 24 hours."),
                    ("02", "We meet", "A video call to get to know you and your vision."),
                    ("03", "Preparation", "Timeline, locations and light planned with you and your planner."),
                    ("04", "The celebration", "Two photographers, from the first toast to the last dance."),
                    ("05", "Your heirloom", "A sneak peek in 48 hours, your gallery, then a fine-art album to pass on.")],
                 pr_eb="Press", pr_h="As featured in",
                 portrait="Portrait of Julien &amp; Mélanie — coming soon")

    values = "".join(f'<div class="jt-value" data-fade="{k * 0.12:.2f}"><span class="jt-value__num">0{k + 1}</span><h3 class="jt-h3">{t}</h3><p class="jt-text">{d}</p></div>' for k, (t, d) in enumerate(c["v"]))
    process = "".join(f'<div class="jt-pstep"><span class="jt-pstep__k">{k}</span><h3 class="jt-pstep__t">{t}</h3><p class="jt-pstep__d">{d}</p></div>' for k, t, d in c["p"])
    press = "".join(f'<div class="jt-pressgrid__item">{p}</div>' for p in PRESS)
    return f"""{page_open(lang, alt, hdr_light=False)}
<section class="jt-phead">
<div class="jt-wrap jt-phead__inner"><span class="jt-eyebrow">{c['eb']}</span><h1 class="jt-h1" data-split="1">{c['h']}</h1><p class="jt-lead jt-phead__intro" data-fade="0.2">{c['intro']}</p></div>
</section>
<section class="jt-section jt-section--tight">
<div class="jt-wrap jt-split">
<div class="jt-split__media"><figure class="jt-ph jt-split__main jt-portrait" data-reveal="1"><span class="jt-portrait__label">{c['portrait']}</span></figure>{ph(356, "jt-ph jt-split__second", "Julien Bonjour wedding photography", "large")}</div>
<div class="jt-split__body"><span class="jt-eyebrow">{c['s_eb']}</span><h2 class="jt-h2" data-split="1">{c['s_h']}</h2><p class="jt-text">{c['s_txt1']}</p><p class="jt-text">{c['s_txt2']}</p><p class="jt-sign">Julien &amp; Mélanie</p></div>
</div>
</section>
<section class="jt-section">
<div class="jt-wrap"><div class="jt-stories__head"><div class="jt-stories__title"><span class="jt-eyebrow">{c['v_eb']}</span></div></div><div class="jt-values">{values}</div></div>
</section>
<section class="jt-section jt-section--dark">
<div class="jt-wrap"><div class="jt-stories__head"><div class="jt-stories__title"><span class="jt-eyebrow">{c['p_eb']}</span><h2 class="jt-h2" data-split="1">{c['p_h']}</h2></div></div><div class="jt-process">{process}</div></div>
</section>
<section class="jt-section">
<div class="jt-wrap"><div class="jt-stories__head"><div class="jt-stories__title"><span class="jt-eyebrow">{c['pr_eb']}</span><h2 class="jt-h2" data-split="1">{c['pr_h']}</h2></div></div><div class="jt-pressgrid">{press}</div></div>
</section>
{quotes_section(lang)}
{planners_section(lang)}
{band_section(lang, 299)}
</div>"""


# --------------------------------------------------------------------------- enquire
def enquire(lang):
    fr = lang == "fr"
    alt = URL["en" if fr else "fr"]["enquire"]
    if fr:
        c = dict(eb="Contact", h="Parlez-nous de votre célébration",
                 txt="Quelques détails suffisent : nous vous répondons sous 24 heures avec nos disponibilités et une proposition sur mesure.",
                 promise=["Réponse sous 24 h", "Basés à Annecy &amp; Genève", "Disponibles dans le monde entier"],
                 direct="Vous préférez écrire directement ?")
    else:
        c = dict(eb="Enquire", h="Tell us about your celebration",
                 txt="A few details are all we need: we reply within 24 hours with our availability and a tailored proposal.",
                 promise=["Reply within 24 hours", "Based in Annecy &amp; Geneva", "Available worldwide"],
                 direct="Prefer to write directly?")
    prom = "".join(f'<span class="jt-enq__p">{p}</span>' for p in c["promise"])
    return f"""{page_open(lang, alt, hdr_light=False)}
<section class="jt-enq">
<div class="jt-enq__media"><img class="jt-enq__img" src="{IMG[247]['full']}" alt="" width="{IMG[247]['w']}" height="{IMG[247]['h']}" data-speed="0.12"></div>
<div class="jt-enq__body">
<span class="jt-eyebrow">{c['eb']}</span>
<h1 class="jt-h2" data-split="1">{c['h']}</h1>
<p class="jt-text">{c['txt']}</p>
<div class="jt-form"></div>
<div class="jt-enq__promise">{prom}</div>
<p class="jt-text">{c['direct']} <a class="jt-inline" href="mailto:{EMAIL}">{EMAIL}</a></p>
</div>
</section>
</div>"""


# --------------------------------------------------------------------------- legal pages (EN; FR comes with the rollout)
TODO = '<span class="jt-legal__todo">[to complete: {}]</span>'
LEGAL = {
    "legal-notice": ("Legal notice", [
        ("Publisher", f"This website is published by Julien Bonjour Photographie, {TODO.format('legal form and share capital')}, "
                      f"registered under SIRET {TODO.format('SIRET number')}, whose registered office is at {TODO.format('postal address')}. "
                      f"Contact: <a class=\"jt-inline\" href=\"mailto:{EMAIL}\">{EMAIL}</a>."),
        ("Publication director", "Julien Bonjour."),
        ("Hosting", f"This website is hosted by {TODO.format('host name, address and phone number')}."),
        ("Intellectual property", "All photographs, texts and visual elements on this website are the exclusive property of Julien Bonjour Photographie, "
                                  "unless stated otherwise. Any reproduction, even partial, requires our prior written consent."),
        ("Personal data", "How we handle the information you share with us is described in our <a class=\"jt-inline\" href=\"/privacy-policy/\">privacy policy</a>."),
    ]),
    "privacy-policy": ("Privacy policy", [
        ("What we collect", "When you write to us through our forms, we receive the information you choose to share: your names, email address, "
                            "phone number and the details of your celebration (date, place, number of guests, budget, your story)."),
        ("Why", "We use it only to answer your enquiry, prepare a proposal and, if we work together, to organise your wedding photography. "
                "We never sell or share it for marketing purposes."),
        ("Who sees it", "Only the two of us. It is stored by our website and email providers, who process it on our behalf."),
        ("How long", "Enquiries that do not lead to a booking are deleted after three years. Client information is kept for as long as "
                     "our accounting and legal obligations require."),
        ("Your rights", f"You may access, correct or delete your information, or object to its use, at any time by writing to "
                        f"<a class=\"jt-inline\" href=\"mailto:{EMAIL}\">{EMAIL}</a>. You may also contact the CNIL, the French data protection authority."),
        ("Cookies", f"This website uses the cookies needed for it to work. {TODO.format('list any audience measurement or third-party cookies, and how consent is collected')}"),
    ]),
}


def legal(slug):
    title, blocks = LEGAL[slug]
    body = "".join(f'<h2 class="jt-legal__h">{h}</h2><p class="jt-text">{t}</p>' for h, t in blocks)
    return f"""{page_open("en", "/fr/", hdr_light=False)}
<section class="jt-legal">
<div class="jt-legal__inner"><h1 class="jt-legal__title">{title}</h1>{body}</div>
</section>
</div>"""


# --------------------------------------------------------------------------- output
def build():
    pages = {}
    for lang in ("en", "fr"):
        p = "" if lang == "en" else "fr-"
        pages[p + "home"] = (lang, home(lang))
        pages[p + "portfolio"] = (lang, portfolio(lang))
        pages[p + "investment"] = (lang, investment(lang))
        pages[p + "about"] = (lang, about(lang))
        pages[p + "enquire"] = (lang, enquire(lang))
        for k, s in enumerate(STORIES):
            pages[p + "story-" + s["slug"]] = (lang, story(lang, k))
        pages[p + "header"] = (lang, header(lang))
        pages[p + "footer"] = (lang, footer(lang))
    for slug in LEGAL:
        pages[slug] = ("en", legal(slug))

    for name, (lang, html) in pages.items():
        with open(os.path.join(ROOT, "push", name + ".html"), "w", encoding="utf-8") as f:
            f.write(html)
    counts = {s["slug"]: len(select_story_images(s)) for s in STORIES}
    print("pages:", len(pages), "story sizes:", counts)


if __name__ == "__main__":
    build()
