#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# inner-service.html — the 1:1 replica of the live inner service page (Car
# Accident Claims) with ONE part replaced: the hero.
#
# Adi picked A1 off the hero option sheet (hero-inner.html) on 2026-08-30 and
# asked for the hero ALONE to change: "section dibawahnya tidak perlu kamu
# ganti, tetap gunakan replika terlebih dahulu."  So everything below the hero
# is still the replica, byte for byte — the same measuring instrument, still
# matching the live page's line breaks.
#
# What changed against gen_inner_replica.py, and nothing else:
#   * `.phead` (the 252px photo banner with the flat rgba(0,0,0,.3) tint) and
#     its dead `.crumbs` rules are cut out;
#   * A1's CSS and markup are LIFTED at build time from the option sheet's own
#     generators and re-scoped `.a1` -> `.shero`, so this hero and the sheet's
#     hero cannot drift;
#   * Raleway 800 is added to the font request — the hero's phone number needs
#     it. The Open Sans half of the URL is untouched and asserted, because that
#     is what makes the replica wrap like the live page.
#
# Two hero class names had to move out of the replica's way. The build works
# out which ones by intersecting the two stylesheets and FAILS if the list ever
# changes, rather than trusting this comment.
# ---------------------------------------------------------------------------
import io, os, re, sys

OUT   = "/srv/apps/leaguelaw/wp-content/prototype/inner-service.html"
R1GEN = "/srv/apps/leaguelaw/prototyping/gen_heroinner.py"
R2GEN = "/srv/apps/leaguelaw/prototyping/gen_heroinner_r2.py"
SCOPE = ".shero"
# hero class -> what it is called on this page, to dodge the replica's own
# `lead` is kept here as insurance rather than necessity: the replica's own
# .lead rules went with the block Adi removed, so nothing collides today. If the
# h2 and lede are ever restored the rename is already correct, and if they are
# not, _assert_no_collision below will still catch anything new.
RENAME = {"lead": "shlead", "wrap": "container"}
# ...and one it deliberately BORROWS: the sheet calls its 1280px centring box
# `.wrap`, the replica calls the same box `.container`. Renaming into it means
# the hero sits on the replica's own grid by construction rather than by a
# second declaration that could disagree with it.
SHARED = {"container"}

TEL_HREF   = "tel:2508880002"
UP         = "/wp-content/uploads"

# --- the injury-claims sidebar menu, in live order -------------------------
INJURY = [
    ("Car Accident Claims",        "/our-services/bc-injury-claims/car-accident-claims/", True),
    ("Motorcycle Accident Claims", "/our-services/bc-injury-claims/motorcycle-accident-claims/", False),
    ("Cyclist Accident Claims",    "/our-services/bc-injury-claims/cyclist-accident-claims/", False),
    ("Passenger Injury Claims",    "/our-services/bc-injury-claims/passenger-injury-claims/", False),
    ("Pedestrian Injury Claims",   "/our-services/bc-injury-claims/pedestrian-injury-claims/", False),
    ("Hit and Run Injury Claims",  "/our-services/bc-injury-claims/hit-and-run-injury-claims/", False),
    ("Slip and Fall",              "/our-services/bc-injury-claims/slip-and-fall/", False),
    ("Premises Liability Claims",  "/our-services/bc-injury-claims/premises-liability-claims/", False),
    ("School Accident Claims",     "/our-services/bc-injury-claims/school-accident-claims/", False),
    ("Product Liability",          "/our-services/bc-injury-claims/product-liability/", False),
    ("Dog Attack",                 "/our-services/bc-injury-claims/dog-attack/", False),
    ("Boat Accidents",             "/our-services/bc-injury-claims/boat-accident-injuries/", False),
    ("Assault &amp; Battery",      "/our-services/bc-injury-claims/assault-battery/", False),
]

SOCIALS = [
    ("fa-facebook",     "https://www.facebook.com/leaguelaw/"),
    ("fa-twitter",      "https://twitter.com/law_in_victoria"),
    ("fa-linkedin",     "https://www.linkedin.com/company/league-and-williams-law-corporation"),
    ("fa-youtube-play", "https://www.youtube.com/channel/UCwrwHBHYoBeOoZtIgmn2PIQ"),
]

# ---------------------------------------------------------------------------
CSS = r"""
/* ===========================================================================
   HTML replica of the live inner-service page (Car Accident Claims).
   Measured off www.leaguelaw.com at a 1440px viewport, 2026-08-27.
   Container 1280 · content 832 (65%) · gutter 64 (5%) · sidebar 384 (30%).
   Static: the two forms do not submit.
   ======================================================================== */
:root{
  --orange:#EF6D3E;      /* brand orange (icons, form chrome, page-header tint) */
  --orange-cta:#EF6136;  /* the contact strip's own, slightly redder orange     */
  --teal:#46ADC8;        /* accent: links, hexagons, buttons, current nav item  */
  --brown:#423C37;       /* contact strip right half                            */
  --grey-bar:#f3f3f3;    /* top bar + header band                               */
  --body:#3f3f3f;        /* article text                                        */
  --muted:#666;          /* theme default text                                  */
  --footer:#222;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:"Open Sans","Helvetica Neue",Arial,sans-serif;font-size:14px;line-height:23.1px;
     color:var(--muted);background:#fff;min-width:1380px;-webkit-font-smoothing:antialiased}
img{display:block}
a{text-decoration:none;color:inherit}
.container{width:1280px;margin:0 auto}

__HEADER_CSS__

__HERO_CSS__

__CONSULT_CSS__

/* ---------- 4 · content + sidebar --------------------------------------- */
.cwrap{display:flex}
.primary{width:832px;padding-bottom:40px}
.sidebar{width:384px;margin-left:64px}

/* ---------- 5 · article ------------------------------------------------- */
.entry{font-size:17px;line-height:28px;color:var(--body)}
.entry a{color:var(--teal)}
.entry p{margin-bottom:20px}
/* The .lead block is gone (Adi, 2026-08-30). It carried four things: the live
   site's squashed teal hexagon, the page's h2, its lede sentence, and a third
   printing of the phone number about 200px under a hero that had just set it
   at 54px. The last of those is why it went; the h2 and the lede went with it
   and are recoverable in one line if they are wanted back. */

/* The article's one photograph. It was floated right at 256 x 192 out of a
   1024 x 768 original — 6% of the column, and small enough that the damage in
   it could not be read. It is now the full 832 of the column at 16:9, which is
   832 x 468: 3.25x wider and 7.9x the AREA. The crop is by height, so the
   people at the top and the road at the bottom go and the car keeps the frame.
   Its place in the reading order is unchanged — it still sits between the Tort
   claim and Part 7, which is the hinge of the article. */
figure.wide{margin:6px 0 30px;clear:both}
figure.wide img{width:100%;height:468px;object-fit:cover;object-position:50% 45%;display:block}
/* The caption runs the full width of the image (Adi 2026-08-30). It was capped
   at 64ch, which is the comfortable measure for reading — but a caption that
   stops two thirds of the way across the picture it belongs to reads as a
   fragment, not a caption. Full width it is, and the text falls to two clean
   lines instead of three ragged ones. */
figure.wide figcaption{font-size:13px;line-height:21px;color:#8D867E;margin-top:11px}

.entry ul{margin:0 0 20px 30px}
.entry ul li{margin:0}

__SIDENAV_CSS__

__FOOTER_CSS__
"""

# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# THE HERO, LIFTED FROM THE OPTION SHEET
# ---------------------------------------------------------------------------
def _scope(css, sel, strip=None):
    """Re-scope a flat, hand-written CSS block: drop `strip` off the front of
    every selector, then put `sel` on the front instead.

    The block must be flat — no @-rules, no nesting. Comments are lifted out
    first so their prose can never be parsed as a selector, then put back.
    The brace count is asserted on the way through, because a rescoper that
    silently drops a rule produces a page that merely looks wrong.
    """
    holes = []

    def stash(m):
        holes.append(m.group(0))
        return "\x00%d\x00" % (len(holes) - 1)

    body = re.sub(r"/\*.*?\*/", stash, css, flags=re.S)
    assert "@" not in body, "block contains an @-rule; _scope cannot handle it"
    braces = body.count("{")
    assert braces == body.count("}"), "unbalanced braces before scoping"

    out = []
    for chunk in body.split("}"):
        if "{" not in chunk:
            out.append(chunk)
            continue
        selpart, decls = chunk.split("{", 1)
        # indentation and any stashed comment belong OUTSIDE the prefix, or the
        # scope lands in front of the comment and the rule escapes unscoped
        lead = re.match(r"(?:\s|\x00\d+\x00)*", selpart).group(0)
        sels = [x.strip() for x in selpart[len(lead):].split(",")]
        if strip:
            sels = [x[len(strip):].lstrip() if x.startswith(strip + " ") else x
                    for x in sels]
        sels = [sel + " " + x for x in sels]
        out.append(lead + ", ".join(sels) + " {" + decls)
    body = "}".join(out)

    assert body.count("{") == braces, "a rule was lost while scoping"
    return re.sub(r"\x00(\d+)\x00", lambda m: holes[int(m.group(1))], body)


def _rename_css(css):
    for old, new in RENAME.items():
        css = re.sub(r"\.%s\b" % re.escape(old), "." + new, css)
    return css


def _rename_markup(html):
    def one(m):
        return 'class="%s"' % " ".join(RENAME.get(c, c) for c in m.group(1).split())
    return re.sub(r'class="([^"]*)"', one, html)


def _selector_classes(css):
    """Every selector in a stylesheet, paired with the class names it uses."""
    css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    out = []
    for chunk in css.split("}"):
        if "{" not in chunk:
            continue
        for sel in chunk.split("{", 1)[0].split(","):
            sel = sel.strip()
            if sel:
                out.append((sel, set(re.findall(r"\.([A-Za-z0-9_-]+)", sel))))
    return out


def _hero_css():
    r2 = io.open(R2GEN, encoding="utf-8").read()
    r1 = io.open(R1GEN, encoding="utf-8").read()

    # (a) the pieces the hero shares with every option on the sheet: the
    #     hexagon (declared as a ratio), the breadcrumb, the title, the
    #     placeholder tag on the photograph.
    i = r1.index("THE HEXAGON, FIXED.")
    j = r1.rindex("\n", 0, r1.rindex("/*", 0, i)) + 1
    shared = r1[j:r1.index("  /* ---------- A1", j)]
    a = shared.find("  /* contact block")      # round 1's bespoke contact block,
    b = shared.find("  .phtag {")              # replaced by home_v1's .call
    assert 0 < a < b, "could not find round 1's .cc block to drop"
    shared = shared[:a] + shared[b:]

    # (b) A1's own rules, as re-cut on 2026-08-30.
    i = r2.index('A1CSS = r"""')
    css = shared + r2[i + len('A1CSS = r"""'):r2.index('"""', i + 12)]

    assert "aspect-ratio:.8660254" in css, "the hexagon ratio did not survive the lift"
    assert "height:10.392px" in css, "the swarm hexagon ratio did not survive the lift"
    assert "min-height:380px" in css, "the band lost its min-height"
    assert "padding:40px 0 40px 60px" in css, "the contact panel lost its left gap"

    css = _rename_css(_scope(css, SCOPE, strip=".a1"))

    rules = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    assert ".a1" not in rules, "an .a1 selector survived the re-scope"
    assert rules.count(SCOPE) >= 30, "suspiciously few scoped rules"

    # custom properties the hero wants that the replica's :root does not define
    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    want = set(re.findall(r"var\((--[a-z-]+)\)", css))
    fill = {"--line": "#E4E1DC", "--paper": "#F9F9F9", "--muted": "#6B6B6B",
            "--orange": "#EF6D3E", "--teal": "#46ADC8", "--brown": "#423C37"}
    missing = sorted(want - have)
    assert not (set(missing) - set(fill)), "hero wants %s and nothing defines them" % (
        sorted(set(missing) - set(fill)),)

    head = ["/* ---------- 3 · hero — A1, picked 2026-08-30 --------------------------",
            "   Lifted whole from the option sheet's generators and re-scoped .a1 ->",
            "   .shero. The 40px below is the margin the old .phead carried, kept so",
            "   the content starts exactly where it did. ---------- */",
            ".shero{margin-bottom:40px%s}" % "".join(
                ";%s:%s" % (v, fill[v]) for v in missing)]
    return "\n".join(head) + "\n" + css


def _hero_markup():
    """A1's markup, executed out of the option sheet's own builder — never
    transcribed. The sheet's A1() also emits the sheet's header stand-in and
    wraps itself in `.a1`; both are cut, and the cut is asserted."""
    r2 = io.open(R2GEN, encoding="utf-8").read()
    ns = {}
    exec(compile(r2[r2.index("PHOTO = "):r2.index('HEAD = """')], R2GEN, "exec"), ns)
    html = ns["A1"]()
    html = '<section class="shero">' + html[html.index("</header>") + len("</header>"):]
    assert "<header" not in html, "the sheet's header stand-in survived the cut"
    assert html.count("<h1") == 1 and "passengerclaim" in html and "call" in html
    return _rename_markup(html)


def _assert_no_collision(hero_css, hero_html):
    """No replica rule may reach inside the hero, and no hero rule outside it.

    A replica selector is dangerous when EVERY class it names is a class the
    hero uses — then it has no ancestor to stop it at the hero's edge. Tag-only
    rules (a, img, *) are the page's base styles and are meant to be inherited,
    and SHARED names the boxes the hero borrows on purpose.
    """
    hero_classes = set()
    for c in re.findall(r'class="([^"]*)"', hero_html):
        hero_classes.update(c.split())
    bad = [sel for sel, cls in _selector_classes(CSS)
           if cls and cls <= hero_classes and not cls <= SHARED]
    assert not bad, ("these replica rules would reach inside the hero: %s — "
                     "add them to RENAME, or cut them if they are dead" % bad)
    for sel, cls in _selector_classes(hero_css):
        assert sel.startswith(SCOPE), "hero rule %r escaped %s" % (sel, SCOPE)


# ---------------------------------------------------------------------------
# THE CONSULTATION TAB, LIFTED FROM home_v1
#
# Adi: "seperti di home_v1.html sudah ada class=ctatab, tambahkan class=ctatab
# di inner service."  The tab is only worth having if it opens something, so
# the modal it opens comes with it — CSS, markup and the script that drives it,
# all lifted from home_v1.html at build time and re-scoped to `.sconsult`.
#
# It is the reason the sidebar form is now an open question: a contact form
# that is ALWAYS one click away, at any scroll position, does a job no
# in-page form can do.
# ---------------------------------------------------------------------------
HOME_V1 = "/srv/apps/leaguelaw/wp-content/prototype/home_v1.html"
CSCOPE  = ".sconsult"
# the only three replica rules that reach inside the modal
CRENAME = {"safe": "csafe", "topbar": "ctopbar"}


def _split_at_rules(css):
    """Yield ("css", text) and ("at", prelude, inner) so @media blocks can have
    their CONTENTS scoped while the @-rule itself stays at the top level."""
    i = 0
    while True:
        j = css.find("@media", i)
        if j < 0:
            yield ("css", css[i:])
            return
        yield ("css", css[i:j])
        k = css.index("{", j)
        depth, end = 0, None
        for m in range(k, len(css)):
            if css[m] == "{":
                depth += 1
            elif css[m] == "}":
                depth -= 1
                if depth == 0:
                    end = m
                    break
        assert end is not None, "unterminated @media block"
        yield ("at", css[j:k + 1], css[k + 1:end])
        i = end + 1


def _scope_sel(sel, scope):
    """Put `scope` on the front of one selector — except a selector that starts
    at `body` or `html`, where the scope goes AFTER it. `body.consult-open
    .cmodal` must stay a body-state rule; prefixing it would make it
    `.sconsult body...`, which matches nothing."""
    m = re.match(r"^((?:body|html)[^\s>]*)\s+(.+)$", sel)
    if m:
        return "%s %s %s" % (m.group(1), scope, m.group(2))
    assert not re.match(r"^(body|html)[^\s>]*$", sel), \
        "bare %r cannot be scoped; decide what it should mean" % sel
    return scope + " " + sel


def _scope2(css, scope, rename=None):
    """The hero's rescoper, plus @media support and body-state selectors."""
    holes = []

    def stash(m):
        holes.append(m.group(0))
        return "\x00%d\x00" % (len(holes) - 1)

    out = []
    for part in _split_at_rules(css):
        if part[0] == "at":
            out.append(part[1] + _scope2(part[2], scope, rename) + "}")
            continue
        body = re.sub(r"/\*.*?\*/", stash, part[1], flags=re.S)
        braces = body.count("{")
        assert braces == body.count("}"), "unbalanced braces before scoping"
        chunks = []
        for chunk in body.split("}"):
            if "{" not in chunk:
                chunks.append(chunk)
                continue
            selpart, decls = chunk.split("{", 1)
            lead = re.match(r"(?:\s|\x00\d+\x00)*", selpart).group(0)
            sels = [_scope_sel(x.strip(), scope)
                    for x in selpart[len(lead):].split(",")]
            chunks.append(lead + ", ".join(sels) + " {" + decls)
        body = "}".join(chunks)
        assert body.count("{") == braces, "a rule was lost while scoping"
        out.append(body)
    css = "".join(out)
    css = re.sub(r"\x00(\d+)\x00", lambda m: holes[int(m.group(1))], css)
    for old, new in (rename or {}).items():
        css = re.sub(r"\.%s\b" % re.escape(old), "." + new, css)
    return css


def _consult():
    """(css, html, script) for the tab + the modal it opens."""
    h = io.open(HOME_V1, encoding="utf-8").read()

    def cut(a, b, label):
        i = h.find(a)
        assert i >= 0, "start marker missing: " + label
        j = h.find(b, i + len(a))
        assert j >= 0, "end marker missing: " + label
        return h[i:j]

    css = (cut("  .swarmbtn {", "\n  .cband .officenote", "swarm button")
           + "\n"
           + cut("  /* ---------- consultation tab + modal",
                 "  /* ---------- fold ruler", "tab + modal css"))
    html = cut("<!-- consultation tab + modal", "<!-- fold ruler", "tab + modal markup")
    script = cut("// consultation modal — CONTEXTUAL",
                 '\n// "Did you know?"', "modal script")

    assert ".ctatab {" in css and "body.consult-open .ctatab" in css
    assert 'class="ctatab"' in html and 'id="cmodal"' in html
    assert "data-open-consult" in script and "consult-open" in script

    css = _scope2(css, CSCOPE, CRENAME)
    rules = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    for sel, cls in _selector_classes(rules):
        if sel.startswith("@"):        # the @media prelude is not a selector
            continue
        assert CSCOPE in sel, "consult rule %r escaped %s" % (sel, CSCOPE)

    def one(m):
        return 'class="%s"' % " ".join(CRENAME.get(c, c) for c in m.group(1).split())
    html = '<div class="sconsult">\n' + re.sub(r'class="([^"]*)"', one, html) + "</div>\n"

    # the script reaches the modal through ids and [data-open-consult], not
    # through the classes that were renamed — assert that, don't hope it
    assert ".safe" not in script and ".topbar" not in script, \
        "the modal script names a class that was renamed"

    # custom properties the modal wants that the replica's :root does not define
    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    want = set(re.findall(r"var\((--[a-z-]+)\)", css))
    fill = {"--line": "#E4E1DC", "--paper": "#F9F9F9", "--muted": "#6B6B6B"}
    missing = sorted(want - have)
    assert not (set(missing) - set(fill)), "modal wants %s" % (sorted(set(missing) - set(fill)),)
    if missing:
        css = "%s{%s}\n" % (CSCOPE, ";".join("%s:%s" % (v, fill[v]) for v in missing)) + css
    return css, html, "<script>\n" + script + "\n</script>\n"


# ---------------------------------------------------------------------------
# THE SIDEBAR, LIFTED FROM THE OPTION SHEET (N5·a, picked 2026-08-30)
#
# The replica's sidebar was a navigation widget AND a four-field form. Both go:
# the form because the page already asks for contact in eight other places and
# the consultation tab does its job better (sidebar-inner.html), the widget
# because N5·a replaces it.
#
# What is lifted: N5·a's own CSS and markup, out of gen_sidebarnav.py at build
# time. Its class names (.n5x/.n5a and their descendants) do not collide with
# anything the replica has left, so nothing needs re-scoping here — but the
# build asserts that rather than assuming it.
# ---------------------------------------------------------------------------
SBGEN = "/srv/apps/leaguelaw/prototyping/gen_sidebarnav.py"


def _sidenav():
    """(css, html) for the picked sidebar."""
    g = io.open(SBGEN, encoding="utf-8").read()

    i = g.index('ROUND3_CSS = r"""')
    css = g[i + len('ROUND3_CSS = r"""'):g.index('"""', i + 18)]
    # the sheet's round 3 carries three variants; only N5·a was picked
    keep = []
    for chunk in re.split(r"(?=\n  /\* -+ N5)", css):
        if "N5b ·" in chunk or "N5c ·" in chunk or ".n5b " in chunk or ".n5c " in chunk:
            continue
        keep.append(chunk)
    css = "".join(keep)
    assert ".n5b" not in css and ".n5c" not in css, "an unpicked variant survived"
    assert ".n5a .rlist a { padding:" in css, "N5a's own rule did not survive"
    assert ".n5x .parent {" in css and ".n5x.shut .rlist" in css, "the parent row did not survive"
    assert "clip-path" not in css, "N5a is the no-hexagon variant; a hexagon came with it"

    # the sticky rule lives in the sheet's shared block
    j = g.index("  .stick { position:sticky;")
    css = g[j:g.index("\n", j) + 1] + css
    assert "position:sticky; top:24px" in css, "the sticky rule did not survive"

    # THE ONE NUMBER THE OPTION SHEET SAID WOULD HAVE TO MOVE.
    # The sheet set top:24px and said so explicitly: "The page's header does not
    # stick today, so 24 is the whole number. If a sticky header is ever added,
    # this becomes that header's height plus 24, and every option here loses
    # exactly that much room." home_v1's header IS sticky, so that day is today.
    #   .nheader = 22px padding + 48px logo + 22px padding + 1px border = 93px
    # and 93 + 24 = 117. Asserted against the header's own rule so the number
    # cannot rot if the header is ever re-padded.
    hdr = io.open(HOME_V1, encoding="utf-8").read()
    assert ".nheader { padding: 22px 0;" in hdr, "the header's padding changed; re-derive 117"
    assert ".nheader .logo img { height: 48px;" in hdr, "the logo height changed; re-derive 117"
    css = css.replace("top:24px", "top:117px", 1)
    assert "position:sticky; top:117px" in css, "the sticky offset was not moved"

    # ...and the sheet's fallback, which the same 117px now makes necessary.
    # The block is 582px. With top:117 it needs a 699px viewport to fit, and a
    # 1366x768 laptop gives about 648 after browser chrome — so the last two
    # claims would sit permanently below the screen edge with no way to reach
    # them. max-height + overflow-y is the sheet's own answer: the list scrolls
    # INSIDE itself, and only when it has to. At 900px the max-height is 759
    # against a 582px block, so nothing appears and nothing changes.
    css = css.replace(
        "  .stick { position:sticky; top:117px; }",
        "  .stick { position:sticky; top:117px;\n"
        "           max-height:calc(100vh - 141px); overflow-y:auto;\n"
        "           overscroll-behavior:contain; }")
    assert "max-height:calc(100vh - 141px)" in css, "the short-viewport fallback was not applied"

    # custom properties this needs that the replica's :root does not define
    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    want = set(re.findall(r"var\((--[a-z-]+)\)", css))
    fill = {"--line": "#E4E1DC", "--paper": "#F9F9F9", "--muted": "#6B6B6B"}
    missing = sorted(want - have)
    assert not (set(missing) - set(fill)), "sidebar wants %s" % (sorted(set(missing) - set(fill)),)

    head = ["/* ---------- 6 · sidebar — N5·a, picked 2026-08-30 -------------------",
            "   Navigation only, sticky, no form. Lifted whole from the option sheet",
            "   (sidebar-nav.html) so the two cannot drift. The old navigation widget",
            "   and the four-field form it sat above are gone; their rules with them.",
            "   ---------- */"]
    if missing:
        head.append(".sidebar{%s}" % ";".join("%s:%s" % (v, fill[v]) for v in missing))
    return "\n".join(head) + "\n" + css.lstrip("\n"), _sidenav_markup(g)


def _sidenav_markup(g):
    """N5·a's markup, executed out of the sheet's own builder, not transcribed."""
    ns = {}
    src = g[g.index("CLAIMS = ["):g.index("def lift_chrome():")]
    src += g[g.index("def n5x(variant):"):g.index("def N5A():")]
    exec(compile(src, SBGEN, "exec"), ns)
    html = ns["n5x"]("n5a")
    assert html.count('<a href="#"') == 13, "not thirteen claims"
    assert '<button class="parent"' in html, "the parent row is not a control"
    assert 'class="cur"' in html, "nothing marks the current claim"
    # the sheet wraps it in .rail; on the page the column is .sidebar
    html = html.replace('<div class="rail n5x n5a">', '<aside class="sidebar n5x n5a">', 1)
    assert html.endswith("</div></div>"), "unexpected tail"
    html = html[:-len("</div>")] + "</aside>"
    assert '<div class="rail' not in html, "the sheet's rail wrapper survived"
    return "\n    " + html + "\n"


SIDENAV_SCRIPT = """
<script>
// the sidebar's parent row collapses the thirteen claims. Lifted behaviour, not
// re-invented: same handler the option sheet uses.
document.querySelectorAll('.sidebar .parent').forEach(function(b){
  b.addEventListener('click', function(){
    var shut = b.closest('.sidebar').classList.toggle('shut');
    b.setAttribute('aria-expanded', shut ? 'false' : 'true');
  });
});
</script>
"""

# ---------------------------------------------------------------------------
# THE FOOTER, LIFTED FROM home_v1
#
# Adi, 2026-08-30: "bagian class='cstrip' sampai footer ganti menjadi seperti
# footer home_v1.html". Everything from the contact strip down — the strip, Get
# in touch, the socials row, the footer form + map, and the footer bottom bar —
# is replaced by home_v1's `.nfoot`: an orange CTA band, four columns, and a bar.
#
# Cutting those five sections also kills the last user of the shared form
# chrome (.fcard/.gf/.legend/.safe/.sendbtn) — the sidebar form went first, the
# footer form goes here — so that block goes too. The build asserts nothing on
# the page still wants it.
#
# `.wrap` is renamed to `.container` in both the lifted CSS and the lifted
# markup: the sheet and the replica call the same 1280px box by different
# names, and renaming into the replica's means the footer sits on the page's
# own grid by construction.
# ---------------------------------------------------------------------------
FRENAME = {"wrap": "container"}


def _box_delta(h, scope):
    """What home_v1's `.wrap` declares that the replica's `.container` does not.

    Renaming `.wrap` into `.container` borrows the host's box. The two are the
    same SIZE, which is what made the rename look safe — but home_v1's `.wrap`
    is also `position:relative`, and the replica's `.container` is not. The mega
    menu is `position:absolute; left:0; right:0`, so it sizes to its nearest
    positioned ancestor: 1280px on the homepage, and the full 1425px viewport
    here. Nothing errored; the menu was just 145px too wide.

    So the delta is computed rather than assumed, and emitted scoped to the
    component that borrowed the box.
    """
    def decls(text, sel):
        i = text.index(sel)
        d = text[text.index("{", i) + 1:text.index("}", i)]
        return {k.strip(): v.strip() for k, v in
                (p.split(":", 1) for p in d.split(";") if ":" in p)}

    theirs = decls(h, "  .wrap {")
    ours = decls(CSS, ".container{")
    for k in ("width", "margin"):
        assert theirs.get(k) == ours.get(k), \
            "the two 1280px boxes no longer agree on %s (%r vs %r)" % (k, theirs.get(k), ours.get(k))
    delta = {k: v for k, v in theirs.items() if ours.get(k) != v}
    if not delta:
        return ""
    return "%s .container{%s}\n" % (scope, ";".join("%s:%s" % kv for kv in sorted(delta.items())))


def _footer():
    """(css, html) for home_v1's footer."""
    h = io.open(HOME_V1, encoding="utf-8").read()

    def cut(a, b, label):
        i = h.find(a)
        assert i >= 0, "start marker missing: " + label
        j = h.find(b, i + len(a))
        assert j >= 0, "end marker missing: " + label
        return h[i:j]

    css = (cut("  .hexpt {", "\n", "hexpt")
           + "\n" + cut("  .swarmbtn {", "\n  .cband .officenote", "swarm button")
           + "\n" + cut("  /* ---------- 7 footer", "  /* ---------- consultation tab", "footer css"))
    html = cut("<!-- 7 · footer", "<!-- consultation tab", "footer markup")

    assert ".nfoot {" in css and ".nfoot .ctaband" in css, "the footer rules did not survive"
    assert '<footer class="nfoot">' in html, "the footer markup did not survive"

    # NOT EVERY FOOTER RULE LIVES IN THE FOOTER BLOCK.
    # `.nfoot .fc { display: none }` sits ~320 lines earlier, under a
    # cross-cutting decision ("HEXAGON-LIGHT, Adi 2026-08-27: home_v1 drops the
    # big hexagon styling — header watermark hive, mega-menu corner hives,
    # FOOTER HIVE, hex social buttons"). Lifting the block alone brought the
    # hive back and made this page's footer differ from the homepage's by
    # 72,767 pixels. So the whole stylesheet is swept for stray .nfoot rules,
    # and the build fails if one is found that was not brought across.
    def pairs(text):
        out = set()
        for chunk in re.sub(r"/\*.*?\*/", "", text, flags=re.S).split("}"):
            if "{" not in chunk:
                continue
            sel, decls = chunk.split("{", 1)
            for one in sel.split(","):
                one = one.strip()
                if one:
                    out.add((one, " ".join(decls.split())))
        return out

    # compared as (selector, declarations) PAIRS, not as selector substrings:
    # `.nfoot .fc` appears in the footer block too, with different declarations,
    # so a substring test would have silently found nothing wrong
    got = pairs(css)
    style = h[h.index("<style>"):h.index("</style>")]
    stray = [(sel, decls) for sel, decls in sorted(pairs(style))
             if sel.startswith(".nfoot") and (sel, decls) not in got]
    for sel, decls in stray:
        css += "\n  %s { %s }" % (sel, decls)
    assert ".nfoot .fc { display: none" in css, \
        "the hexagon-light rule for the footer was not picked up"

    # .hexpt and .swarmbtn are bare utilities in home_v1; here they exist only
    # to serve the footer, so they are scoped to it rather than left loose
    css = css.replace("  .hexpt {", "  .nfoot .hexpt {", 1)
    css = re.sub(r"(^|\n)  \.swarmbtn\b", r"\1  .nfoot .swarmbtn", css)

    for old, new in FRENAME.items():
        css = re.sub(r"\.%s\b" % re.escape(old), "." + new, css)

    def one(m):
        return 'class="%s"' % " ".join(FRENAME.get(c, c) for c in m.group(1).split())
    html = re.sub(r'class="([^"]*)"', one, html)

    rules = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    for sel, cls in _selector_classes(rules):
        if sel.startswith("@"):
            continue
        assert sel.startswith(".nfoot"), "footer rule %r is not scoped to .nfoot" % sel

    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    want = set(re.findall(r"var\((--[a-z-]+)\)", css))
    fill = {"--line": "#E4E1DC", "--paper": "#F9F9F9", "--brown": "#423C37"}
    missing = sorted(want - have)
    assert not (set(missing) - set(fill)), "footer wants %s" % (sorted(set(missing) - set(fill)),)

    head = [_box_delta(h, ".nfoot").rstrip(),
            "/* ---------- 7 · footer — home_v1's, lifted whole (Adi 2026-08-30) -----",
            "   Replaces five sections of the replica: the contact strip, Get in touch,",
            "   the socials row, the footer form + map, and the footer bottom bar.",
            "   .wrap renamed to .container so it sits on the replica's own grid.",
            "",
            "   line-height:normal is the WHOLE POINT of the reset below. home_v1's",
            "   body declares no line-height, so its footer inherits `normal`; this",
            "   page's body declares 23.1px, the live site's own. Without the reset the",
            "   same footer rendered 44px TALLER here than on the homepage — .fmenu 17",
            "   -> 23, .fcontact 250 -> 288, .fbar 50 -> 56 — and looked looser at every",
            "   line. The four rules inside the footer that set their own line-height",
            "   still win; this only restores the ground they were designed on. */",
            ".nfoot{line-height:normal}"]
    if missing:
        head.append(".nfoot{%s}" % ";".join("%s:%s" % (v, fill[v]) for v in missing))
    return "\n".join(head) + "\n" + css.lstrip("\n"), "\n" + html

# ---------------------------------------------------------------------------
# THE HEADER, LIFTED FROM home_v1
#
# Adi, 2026-08-30: "samakan header seperti di home_v1.html". Out goes the
# replica's two-band header — the 52px top bar and the 43px nav band with the
# logo hanging out of it. In comes home_v1's `.nheader`, mega menus and all.
#
# The footer taught the lesson this reuses: a component's rules are not all
# filed under the component. `.nheader .combclip { display:none }` and
# `.mega .mhex { display:none }` live ~320 lines from the header block, under
# the HEXAGON-LIGHT decision. So this does not cut a block — it SWEEPS the
# whole stylesheet for every rule rooted at one of the header's four roots
# (.nheader, .mega, .mi, .hexpt), keeps them in source order, and keeps the
# @media wrapper around the ones that live inside one.
# ---------------------------------------------------------------------------
HROOTS = (".nheader", ".mega", ".mi", ".hexpt")
HSCOPE = ".nheader"


def _rooted(sel):
    return any(re.match(r"^%s\b" % re.escape(r), sel) for r in HROOTS)


def _pick(css):
    """Keep only the rules rooted at the header, preserving source order.

    Comments come out FIRST. Chunking on `}` makes everything since the last
    brace part of the selector, so a rule that follows a comment reads as
    `/* ... */ .nheader` and fails the root test — which silently dropped the
    base `.nheader` rule and every other rule that opened a section. Since this
    cherry-picks rules rather than cutting a block, home_v1's comments have
    nowhere to travel to anyway.
    """
    css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    out = []
    for chunk in css.split("}"):
        if "{" not in chunk:
            continue
        sel, decls = chunk.split("{", 1)
        keep = [x.strip() for x in sel.split(",") if _rooted(x.strip())]
        if keep:
            out.append("  %s {%s}" % (", ".join(keep), decls))
    return "\n".join(out)


def _header():
    """(css, html) for home_v1's header."""
    h = io.open(HOME_V1, encoding="utf-8").read()
    style = h[h.index("<style>") + 7:h.index("</style>")]

    parts = []
    for part in _split_at_rules(style):
        if part[0] == "at":
            inner = _pick(part[2])
            if inner.strip():
                parts.append(part[1] + "\n" + inner + "\n}")
        else:
            got = _pick(part[1])
            if got.strip():
                parts.append(got)
    css = "\n".join(parts)

    assert ".nheader {" in css.replace("  .nheader {", ".nheader {"), "the header rules did not survive"
    assert ".mega {" in css.replace("  .mega {", ".mega {"), "the mega-menu rules did not survive"
    assert "@media" in css, "the reduced-motion block did not survive"
    # the two cross-cutting HEXAGON-LIGHT rules the footer taught us to look for
    assert "combclip" in css and ".mega .mhex" in css, \
        "the hexagon-light rules for the header were not swept up"

    # .mega, .mi and .hexpt all live INSIDE .nheader in the markup, so they are
    # scoped to it rather than left loose on the page
    def scope(m):
        sels = [x.strip() for x in m.group(1).split(",")]
        sels = [x if x.startswith(HSCOPE) else HSCOPE + " " + x for x in sels]
        return "  " + ", ".join(sels) + " {"
    css = re.sub(r"^  ([^{@\n][^{\n]*) \{", scope, css, flags=re.M)

    for old, new in FRENAME.items():                      # .wrap -> .container
        css = re.sub(r"\.%s\b" % re.escape(old), "." + new, css)

    rules = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
    for sel, cls in _selector_classes(rules):
        if sel.startswith("@"):
            continue
        assert sel.startswith(HSCOPE), "header rule %r is not scoped to %s" % (sel, HSCOPE)

    html = h[h.index("<!-- 1 · header"):h.index("<!-- 2 · triage")]

    def one(m):
        return 'class="%s"' % " ".join(FRENAME.get(c, c) for c in m.group(1).split())
    html = re.sub(r'class="([^"]*)"', one, html)

    # this page is an Injuries page, so the nav says so
    before = html
    html = html.replace('<div class="mi"><a href="#">Injuries</a>',
                        '<div class="mi cur"><a href="/our-services/bc-injury-claims/">Injuries</a>', 1)
    assert html != before and 'class="mi cur"' in html, "could not mark the current practice area"

    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    want = set(re.findall(r"var\((--[a-z-]+)\)", css))
    fill = {"--line": "#E4E1DC", "--paper": "#F9F9F9", "--body-text": "#6B6B6B"}
    missing = sorted(want - have)
    assert not (set(missing) - set(fill)), "header wants %s" % (sorted(set(missing) - set(fill)),)

    head = [_box_delta(h, HSCOPE).rstrip(),
            "/* ---------- 1 · header — home_v1's, swept whole (Adi 2026-08-30) ------",
            "   Replaces the replica's top bar and nav band. Swept, not cut: two of",
            "   its rules (.nheader .combclip, .mega .mhex) live under the",
            "   HEXAGON-LIGHT decision ~320 lines away, exactly as the footer's did.",
            "   line-height:normal for the same reason as the footer — home_v1's body",
            "   declares none, this page's declares the live site's 23.1px.",
            "   The current practice area is marked with .mi.cur. ---------- */",
            ".nheader{line-height:normal}",
            ".nheader .services .mi.cur > a { color:var(--orange);"
            " box-shadow:inset 0 -2px 0 var(--orange); }"]
    if missing:
        head.append(".nheader{%s}" % ";".join("%s:%s" % (v, fill[v]) for v in missing))
    return "\n".join(head) + "\n" + css, "\n" + html

def head():
    return """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>League Law &mdash; Car Accident Claims</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- the live site loads the v1 CSS API; css2 serves the 2021 variable Open Sans,
     which is ~5% wider and re-wraps every paragraph. Do not "modernise" this.
     Raleway 800 is the ONE addition — the hero's phone number needs it. No text
     in the replica uses Raleway 800, so nothing below the hero re-measures; the
     Open Sans half of this URL is byte-identical and is asserted in build(). -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700|Raleway:300,400,500,600,700,800&subset=latin" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>__CSS__</style>
</head>
<body>
<span id="top"></span>
""".replace("__CSS__", CSS.replace("__HERO_CSS__", _hero_css())
                          .replace("__CONSULT_CSS__", _consult()[0])
                          .replace("__SIDENAV_CSS__", _sidenav()[0])
                          .replace("__FOOTER_CSS__", _footer()[0])
                          .replace("__HEADER_CSS__", _header()[0]))


def page_header():
    """The picked hero (A1), in place of the live page's 252px photo banner.

    The banner it replaces was a background photograph under a flat
    `rgba(0,0,0,.3)` wash with the h1 at left and the breadcrumb at right. The
    replica reproduced that wash faithfully, defects and all; A1 carries the
    same two jobs — title and breadcrumb — and adds the contact block.

    NOTHING BELOW THIS SECTION CHANGES. The article, the sidebar, both forms
    and the footer are still the replica, so the page still measures against
    the live one everywhere except here.
    """
    return "\n<!-- 3 · hero — A1 (hero-inner.html), picked 2026-08-30 -->\n" + _hero_markup() + "\n"


ARTICLE = u"""<p>Being involved in a car accident can be scary and often those injured in motor vehicle accidents are unaware of how to ensure the best recovery possible.&nbsp; Those injured in car accidents in British Columbia, whether as a driver, passenger, pedestrian or cyclist are entitled to pursue compensation for their injuries.&nbsp; Car accident victims who are injured may claim for pain and suffering, out-of-pocket expenses, loss of housekeeping capacity, loss of income and diminished earning capacity, and for cost of future care.&nbsp; The kind of compensation available and the amounts will depend on the extent of the injuries, the impact they have had on the victim’s life, and whether or not the victim was partly at fault for their injuries.</p>


<p>Typically those involved in car&nbsp;accidents in British Columbia may make two types of claims – a Tort claim and a Part 7 claim.</p>


<p>A Tort claim is available for those who are injured and who can prove that another person was, at least partly, at fault or responsible for causing the accident.&nbsp; The person must also establish that they suffered damages, and that those damages were the result of the accident.&nbsp; It is important to note that in order to make a tort claim, that claim must also be filed within 2 years of the date of the accident (this is the limitation date).&nbsp; If a person cannot prove that their injuries were caused by the accident, then the injured person is limited in their tort claim to the damages that they can prove were the result of the accident. &nbsp;If a person cannot prove that another person is at least partly at fault, then their claims for their injuries will be limited to a Part 7 claim, also known as a no-fault benefits claim.</p>





<figure class="wide"><img src="/wp-content/uploads/blackandwhitemiata-1-1024x768.png" alt=""><figcaption>An experienced injury lawyer can help you recover after a car accident by handling your ICBC claim, ensuring access to treatments needed to recover, and advocating for fair compensation.</figcaption></figure>

<p>A Part 7 claim refers to the “no-fault” benefits that are payable to accident victims in BC, even if they are wholly at fault for their injuries.&nbsp; To receive these benefits, the person must have an insurance policy with ICBC; or, live in a household with someone who has an insurance policy with ICBC; or, was the occupant of a vehicle licensed in BC; or was a cyclist or pedestrian who has collided with a vehicle insured in BC.&nbsp; It is important to note that notice and limitation periods apply to Part 7 claims.&nbsp; Although the claimant does not need to prove that someone else was at fault for the accident, they must still prove that the injuries that they sustained were the result of the accident.</p>


<p>Under Part 7, a person is entitled to the following benefits:</p>


<ul><li>Up to $150,000 in medical and rehabilitation expenses</li><li>Up to $300 per week in wage loss benefits for those who were totally disabled from work within 20 days of the accident. Note these benefits are payable for a maximum of 104 weeks.</li><li>Homemaker benefits (limits apply).</li><li>Death benefits – limited amounts paid to surviving spouses and children.</li></ul>


<p>If you, or someone you know was injured in a car accident, speaking with an experienced injury lawyer and making informed decisions about their claim is critical.&nbsp; The lawyers at League and Williams provide free consultations to those injured in car accidents and have the experience needed to help injured victims recover to their fullest potential. &nbsp;We <a href="/contact-our-lawyers/">may be contacted</a> by phone 250-888-0002 or via email at&nbsp;<a href="mailto:info@leaguelaw.com">info@leaguelaw.com.</a></p>"""


def article():
    """The live page's own article markup, lifted verbatim.

    Transcribing this by hand loses the non-breaking spaces the copy carries
    (e.g. "via email at\u00a0info@leaguelaw.com."), and a lost NBSP re-wraps the
    line. It is extracted, never retyped.
    """
    return '\n      <div class="entry">\n' + ARTICLE + '\n      </div>\n'


def sidebar():
    """The picked sidebar (N5·a), in place of the replica's navigation widget
    and four-field form.

    Nothing here is written: the markup comes out of the option sheet's own
    builder and the CSS out of its stylesheet, both at build time.
    """
    return _sidenav()[1]


def build():
    hero_css, hero_html = _hero_css(), page_header()
    _assert_no_collision(hero_css, hero_html)
    ccss, chtml, cscript = _consult()
    scss, shtml = _sidenav()
    fcss, fhtml = _footer()
    hcss, hhtml = _header()
    for slot, css in (("__HERO_CSS__", hero_css), ("__CONSULT_CSS__", ccss),
                      ("__SIDENAV_CSS__", scss), ("__FOOTER_CSS__", fcss),
                      ("__HEADER_CSS__", hcss)):
        assert slot in CSS, "%s has no slot in the stylesheet" % slot
        assert slot not in css, "%s would inject itself" % slot

    parts = [head(), hhtml, hero_html,
             '\n<!-- 4 · content + sidebar -->\n<div class="container cwrap">\n  <main class="primary">\n',
             article(),
             '  </main>\n', shtml, '</div>\n',
             fhtml,
             "\n<!-- consultation tab + modal, lifted from home_v1 -->\n", chtml, cscript,
             SIDENAV_SCRIPT,
             "\n</body>\n</html>\n"]
    return "".join(parts)


if __name__ == "__main__":
    html = build()
    dest = sys.argv[1] if len(sys.argv) > 1 else OUT
    with io.open(dest, "w", encoding="utf-8") as fh:
        fh.write(html)
    print("wrote %s (%d bytes)" % (dest, len(html)))
