"""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": "Château wedding, France", "fr": "Mariage au château, 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(
        where="Annecy · Geneva · and wherever you celebrate",
        other=("Français", "fr"),
    ),
    "fr": dict(
        where="Annecy · Genève · et partout où vous célébrerez",
        other=("English", "en"),
    ),
}


def footer(lang):
    t = T[lang]
    f = FOOT[lang]
    u = URL[lang]
    nav = "".join(f'<a class="jt-foot__nav-link" href="{u[k]}">{label}</a>' for k, label in t["menu"][1:])
    social = (f'<a class="jt-foot__small-link" href="{INSTAGRAM}" target="_blank" rel="noopener">Instagram</a>'
              f'<a class="jt-foot__small-link" href="{FACEBOOK}" target="_blank" rel="noopener">Facebook</a>'
              f'<a class="jt-foot__small-link" href="{URL[f["other"][1]]["home"]}" hreflang="{f["other"][1]}" data-lang-switch="1">{f["other"][0]}</a>')
    if t["ftr_extra"]:
        social = f'<a class="jt-foot__small-link" href="{t["ftr_extra"][1]}" target="_blank" rel="noopener">{t["ftr_extra"][0]}</a>' + social
    return f"""<footer class="jt-foot">
<div class="jt-foot__lead"><span class="jt-foot__where">{f['where']}</span><a class="jt-foot__mail" href="mailto:{EMAIL}">{EMAIL}</a></div>
<nav class="jt-foot__nav" aria-label="Footer">{nav}</nav>
<p class="jt-foot__mark" aria-hidden="true">Julien Bonjour</p>
<div class="jt-foot__bottom"><span class="jt-foot__small">{t['rights']} · <a class="jt-foot__small-link" href="/politique-de-cookies-ue/">{t['cookies']}</a></span><span class="jt-foot__social">{social}</span></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: photos hung around the title; frames 2, 4 and 5 are slow carousels
WALL = [[411], [292, 287], [371], [491, 509, 477], [536, 414], [577]]
WALL_SPEEDS = [0.3, -0.2, 0.45, -0.3, 0.25, -0.15]
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"]
CARTEL = {
    "en": {"A": "Bastide du Roy, Côte d’Azur, 2026", "B": "Lake Annecy, 2026", "C": "Lake Annecy, 2025",
           "D": "A château in France, 2024", "F": "French Riviera, 2025"},
    "fr": {"A": "Bastide du Roy, Côte d’Azur, 2026", "B": "Lac d’Annecy, 2026", "C": "Lac d’Annecy, 2025",
           "D": "Un château en France, 2024", "F": "Riviera, 2025"},
}
HOME_STORIES = ["B", "F", "D"]
GAL_HOVER = {"B": 342, "F": 525, "D": 494}
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", prev="Previous", next="Next",
        ask_eb="Now booking 2027 &amp; 2028", ask_h=("Tell us", "about your day."),
        ask_txt="A few words are enough to begin. We reply personally, within 24 hours.",
        ask_full="Prefer the full form?",
        ig_eb="On Instagram",
    ),
    "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", prev="Précédent", next="Suivant",
        ask_eb="Réservations 2027 &amp; 2028 ouvertes", ask_h=("Racontez-nous", "votre journée."),
        ask_txt="Quelques mots suffisent pour commencer. Nous répondons personnellement, sous 24 heures.",
        ask_full="Vous préférez le formulaire complet ?",
        ig_eb="Sur Instagram",
    ),
}


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):
        car = " jt-wall__f--carousel" if len(ids) > 1 else ""
        imgs = "".join(
            f'<img class="jt-fig__img{" jt-wall__slide" if car else ""}" src="{src(i)}" alt="{img_alt}" width="{IMG[i]["w"]}" height="{IMG[i]["h"]}"{FPRI if k < 2 and n == 0 else ""} decoding="async">'
            for n, i in enumerate(ids))
        wall.append(f'<figure class="jt-wall__f jt-wall__f--{k + 1}{car}" data-speed="{WALL_SPEEDS[k]}">{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[lang][IMG[i]["g"]])
        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="{src(GAL_HOVER[key])}" 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__meta">{s["names"]}</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, 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>
<div class="jt-expo__foot"><a class="jt-link" href="{URL[lang]['portfolio']}">{c['expo_link']}</a></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-voices__ctrl"><span class="jt-voices__arrow jt-voices__arrow--prev" role="button" tabindex="0" aria-label="{c['prev']}">←</span><div class="jt-quotes__nav">{dots}</div><span class="jt-voices__arrow jt-voices__arrow--next" role="button" tabindex="0" aria-label="{c['next']}">→</span></div></div>
<figure class="jt-voices__f">{vimgs}</figure>
</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><a class="jt-link" href="{URL[lang]['enquire']}">{c['ask_full']}</a></div>
<div class="jt-ask__form"></div>
</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">@julien_bonjour_photographe</a></div>
<div class="jt-ig__grid">{ig}</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>"""


# --------------------------------------------------------------------------- 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 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()
