#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# your-team.html — the Your Team page.
#
# team-replica.html stays untouched. It is the measuring instrument: the live
# page HAS a sidebar and a photo banner, and a replica that drops them is no
# longer a copy of anything. This is the build derived from it, and the diff
# between the two files is the complete record of what the rebuild changed.
#
# NOTHING IS TRANSCRIBED. Three files are EXECUTED and their own lifting
# functions called:
#   gen_team_replica.py   -> the crawled data (thirteen lawyers, the intro copy
#                            with its non-breaking spaces)
#   gen_inner_replica_hero.py
#                         -> the shell stylesheet, the A1 hero, home_v1's
#                            header, home_v1's footer, the consultation tab
#   home_v1.html          -> the `.teams` bench, swept out of its stylesheet
#
# So this page shares its shell with inner-service.html BY CONSTRUCTION, and
# its bench with the homepage. None of the four can drift from the others.
#
# Rounds so far, in Adi's order:
#   2026-08-31 a  "tanpa sidebar"          -> one 1280 column, grid re-flowed
#   2026-08-31 b  "hero seperti inner"     -> A1 in place of the 303px banner
#   2026-08-31 c  "hilangkan class=hex,     -> the intro's hexagon out, copy
#                  copy rata tengah,           centred; the lawyer grid becomes
#                  team seperti .teams,        home_v1's bench; header, footer
#                  footer + header seperti     and the consultation tab all
#                  home_v1, tambah ctatab"     lifted from home_v1
# ---------------------------------------------------------------------------
import io, os, re, sys

OUT     = "/srv/apps/leaguelaw/wp-content/prototype/your-team.html"
REPLICA = "/srv/apps/leaguelaw/prototyping/gen_team_replica.py"
HGEN    = "/srv/apps/leaguelaw/prototyping/gen_inner_replica_hero.py"
HOME_V1 = "/srv/apps/leaguelaw/wp-content/prototype/home_v1.html"
UPLOADS = "/srv/apps/leaguelaw/wp-content/uploads"

# --- the crawled page data --------------------------------------------------
_rsrc = io.open(REPLICA, encoding="utf-8").read()
RNS = {"__name__": "gen_team_replica", "__file__": REPLICA}
exec(compile(_rsrc, REPLICA, "exec"), RNS)
TEAM, INTRO = RNS["TEAM"], RNS["INTRO"]
assert len(TEAM) == 13 and " " in INTRO

# --- the shell, the hero, home_v1's header/footer, the consultation tab -----
_hsrc = io.open(HGEN, encoding="utf-8").read()
HNS = {"__name__": "gen_inner_replica_hero", "__file__": HGEN}
exec(compile(_hsrc, HGEN, "exec"), HNS)
CSS = HNS["CSS"]
assert HNS["SCOPE"] == ".shero"
# the shell stylesheet is already reduced to the two blocks this page keeps —
# content and article — with a named slot for each lifted component. Adopting
# it (rather than the replica's twelve-block sheet) is what makes this page and
# inner-service.html share a shell instead of merely resembling one.
for slot in ("__HEADER_CSS__", "__HERO_CSS__", "__CONSULT_CSS__",
             "__SIDENAV_CSS__", "__FOOTER_CSS__"):
    assert slot in CSS, "%s has no slot in the shell stylesheet" % slot


# ===========================================================================
# THE HERO — A1, the same one inner-service.html carries
# ===========================================================================
# The photograph is the page's OWN banner image. law-1.jpg is 1800x332
# (5.42:1) in a 420x380 (1.11:1) panel, so `cover` matches the HEIGHT and crops
# the width to a 367px slice — which makes the HORIZONTAL object-position
# load-bearing here, where on the inner service page (a 3:2 source) the
# vertical one was inert. At the 50% default the slice is the blur behind the
# statue; 70% is where the statue stands.
HERO_PHOTO  = "/wp-content/uploads/law-1.jpg"
HERO_CRUMBS = ('<nav class="crumbs %s"><a href="/">Home</a><i></i>'
               '<span class="here">Our Lawyers</span></nav>')
# the live h1 is 68 characters. A1's type column is a constant 336px and
# .htitle is 38px, so it renders as SEVEN lines: the h1 box measures 303.2
# instead of 43.3, the band 572.4 instead of 380, and the page below it moves
# down 192px. Rendered both ways before choosing. "Your Team" is the site's own
# name for this page — in the nav and in the breadcrumb.
HERO_TITLE  = '<h1 class="htitle">Your Team</h1>'
# A1's fifth line is the parent section ("Injuries" on the inner page). This
# page has no parent section, so it takes the live page's own sub-line.
HERO_SUB    = "Your choice for legal advice"


def hero_css():
    return HNS["_hero_css"]() + (
        "/* law-1.jpg is 5.42:1 in a 1.11:1 panel: `cover` crops the WIDTH, so\n"
        "   this is the only control over what the panel shows. 70% is the\n"
        "   statue; the 50% default is the blur behind it. */\n"
        ".shero .ph img{object-position:70% 50%}\n")


def hero_markup():
    """A1's markup, out of the option sheet's own builder.

    The builder reads PHOTO, CRUMBS, TITLE and hexicon() from its module
    globals, so this page's content is put INTO those globals and A1() is run —
    rather than run-then-edited. The one thing A1 writes inline instead of
    reading from a global is the sub-line, and that one is swapped with an
    assertion."""
    r2 = io.open(HNS["R2GEN"], encoding="utf-8").read()
    ns = {}
    exec(compile(r2[r2.index("PHOTO = "):r2.index('HEAD = """')], HNS["R2GEN"], "exec"), ns)
    _hex = ns["hexicon"]
    ns["PHOTO"], ns["CRUMBS"], ns["TITLE"] = HERO_PHOTO, HERO_CRUMBS, HERO_TITLE
    ns["hexicon"] = lambda size="m", tone="w": _hex(size, tone).replace("fa-car", "fa-users")
    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"
    old = '<p class="hsub">Injuries</p>'
    assert html.count(old) == 1, "A1's sub-line is not where it was"
    html = html.replace(old, '<p class="hsub">%s</p>' % HERO_SUB)
    assert "fa-users" in html and "fa-car" not in html, "the hexagon icon did not change"
    assert html.count("<h1") == 1 and "law-1.jpg" in html and "Our Lawyers" in html
    return "\n<!-- 3 · hero — A1, the inner service page's hero -->\n" + \
           HNS["_rename_markup"](html) + "\n"


# ===========================================================================
# THE INTRO — hexagon out, copy centred
# ===========================================================================
# Adi: "hilangkan class=hex, kemudian buat copy content menjadi rata tengah."
#
# The hexagon was the reason the block was indented 70px and set left. With it
# gone there is nothing holding the left edge, and the page now runs a centred
# hero title over a centred bench, so the copy between them centres too.
#
# The measure does NOT widen with the column. It stays at 762px — the width the
# copy was written for, and what it measured on the live page — because a
# centred paragraph 1280px wide would be ~185 characters a line, twice a
# readable measure. Centring changes the axis, not the measure.
INTRO_CSS = r"""
/* ---------- intro — centred, no hexagon (Adi 2026-08-31) ----------------
   .tc keeps its name and loses its indent: the 70px was the hexagon's
   column and there is no hexagon. 762px is the same measure the copy had
   at 832 minus that indent — held, not widened, because centred text gets
   HARDER to read as the measure grows, not easier. */
/* the gap down to the first face is 80px and ONE element owns it: .teams
   below. Before this it was 110 and no element declared it — 46 here plus
   home_v1's 64 there, two paddings doing the same job and stacking into a
   number neither of them said. A gap you cannot find by reading either rule
   is a gap nobody can tune. */
.icontext{padding:14px 0 0}
.icontext .tc{position:static;padding-left:0;max-width:762px;margin:0 auto;text-align:center}
.icontext h2{font-family:Raleway,"Helvetica Neue",Arial,sans-serif;font-size:26px;line-height:39px;
             font-weight:700;color:#000;margin-bottom:14px}
.icontext p{margin-bottom:20px}
.icontext p:last-child{margin-bottom:0}
.icontext .fa-phone{color:var(--body);margin:0 4px}
"""

# ---------- the single column ---------------------------------------------
SOLO_CSS = r"""
/* ---------- no sidebar (Adi 2026-08-31) --------------------------------
   832 + 64 + 384 collapses to one 1280. The bench below is a full-bleed
   band of its own, so the column carries the intro and nothing else and
   the 40px tail is dropped — .teams brings its own 64px of top padding. */
.cwrap.solo{display:block}
.cwrap.solo .primary{width:100%;padding-bottom:0}
"""


def _intro_copy():
    """The live page's intro copy, minus its closing contact line.

    Adi 2026-08-31: "hilangkan Contact us today for a consultation (250)
    888-0002 di copy content." The line is a third printing of the phone
    number on a page that already carries it in the hero's call panel, in the
    CTA band and twice in the footer — and the hero's is 54px with a form
    beside it. A paragraph repeating it in body type asks for a call in the
    weakest place on the page.

    Cut by its own paragraph boundaries and asserted both ways, so a change to
    the crawled copy fails the build rather than silently leaving it in."""
    i = INTRO.index("<p><strong>Contact us today for a consultation")
    j = INTRO.index("</p>", i) + len("</p>")
    assert "(250) 888-0002" in INTRO[i:j] and "tel:2508880002" in INTRO[i:j], \
        "that is not the contact paragraph"
    out = (INTRO[:i] + INTRO[j:]).rstrip()
    assert "(250) 888-0002" not in out and "Contact us today" not in out
    assert out.count("<p>") == 2 and out.startswith("<h2>"), \
        "the intro should be a heading and two paragraphs, got %r" % out[-60:]
    assert "\xa0" in out, "the copy lost its non-breaking spaces"
    return out


def entry():
    """The intro block. The copy is the live page's, with its non-breaking
    spaces intact, and the hexagon span is gone."""
    assert '<span class="hex">' not in INTRO, "the hexagon is not part of the copy"
    return ("""
<!-- 4 · intro -->
<div class="container cwrap solo">
  <main class="primary">
    <div class="entry">
      <div class="icontext"><div class="tc">
""" + _intro_copy() + """
      </div></div>
    </div>
  </main>
</div>
""")


# ===========================================================================
# THE BENCH — home_v1's `.teams`, swept out of its stylesheet
# ===========================================================================
# Adi: "untuk tampilan anggota team tampilkan seperti class=teams di
# home_v1.html."
#
# Swept, not block-cut, for the reason the footer taught: a component's rules
# are not all filed under the component. `.hexpt` (the kicker's marker) sits
# ~250 lines from the bench block. So the whole stylesheet is scanned for every
# rule rooted at one of the bench's four roots and the build fails if the
# count changes.
#
# This also retires the defect the replica reproduced and the sidebar removal
# amplified: thirteen portraits at thirteen widths (126.3 - 143.6), each
# floating in white. `.bx .bph` is a fixed 210 x 250 box with object-fit:cover
# and object-position:top, so every face is the same size and cropped from the
# top of the frame.
BROOTS = (".teams", ".bench", ".bx", ".hexpt")


def _bench():
    """(css, html) for home_v1's bench."""
    h = io.open(HOME_V1, encoding="utf-8").read()
    style = h[h.index("<style>") + 7:h.index("</style>")]
    flat = re.sub(r"@media[^{]*\{(?:[^{}]|\{[^{}]*\})*\}", "", style)
    assert not any(r in re.findall(r"@media[^{]*\{(?:[^{}]|\{[^{}]*\})*\}", style)
                   and r in style for r in ()), ""
    # nothing bench-related lives inside an @media block in home_v1; asserted
    # rather than assumed, because a media rule dropped by this sweep would
    # vanish silently.
    for m in re.finditer(r"@media[^{]*\{((?:[^{}]|\{[^{}]*\})*)\}", style):
        assert not any(r in m.group(1) for r in BROOTS), \
            "a bench rule now lives inside %s and this sweep would drop it" % m.group(0)[:40]

    noc = re.sub(r"/\*.*?\*/", "", flat, flags=re.S)
    kept = []
    for chunk in noc.split("}"):
        if "{" not in chunk:
            continue
        sel, decls = chunk.split("{", 1)
        sels = [x.strip() for x in sel.split(",") if x.strip()]
        if any(any(re.match(r"^%s\b" % re.escape(r), x) for r in BROOTS) for x in sels):
            kept.append((", ".join(sels), " ".join(decls.split())))
    assert len(kept) == 19, "the bench swept %d rules, not the 19 measured" % len(kept)

    # ---- just the bench (Adi 2026-08-31) ---------------------------------
    # "untuk display team bisakah untuk halaman your team ini langsung diambil
    # dari section class=bench saja?" — home_v1's `.teams` is a bench PLUS a
    # section head (kicker, "The whole bench.", the Google credibility line).
    # On the homepage that head is doing the introducing; on this page the
    # intro paragraph two inches above it has already done it, and a second
    # heading between the two reads as the page starting over. So the head
    # goes, and with it every rule that exists only to style it — including
    # `.hexpt`, which is on the kicker and nowhere else in this component.
    #
    # Listed by name and asserted rather than matched by pattern: a rule that
    # quietly stopped being dropped, or one that vanished from home_v1, would
    # otherwise go unnoticed.
    HEAD_ONLY = {".hexpt", ".teams .head", ".teams .kick", ".teams .kick i",
                 ".teams h2", ".teams h2 b", ".teams .cred", ".teams .cred .stars",
                 ".teams .cred b", ".teams .cred .dot"}
    dropped = {sel for sel, _ in kept if sel in HEAD_ONLY}
    assert dropped == HEAD_ONLY, "the section head's rules changed: %s" % (
        sorted(HEAD_ONLY ^ dropped),)
    kept = [(sel, d) for sel, d in kept if sel not in HEAD_ONLY]
    assert len(kept) == 9, "the bench itself is %d rules, not 9" % len(kept)

    out = []
    for sel, decls in kept:
        # .hexpt, .bench and .bx are loose in home_v1 (it has one bench, so
        # nothing collides); here they exist only to serve this section, so
        # they are scoped to it rather than left in the page's global space.
        sel = ", ".join(s if s.startswith(".teams") else ".teams " + s
                        for s in sel.split(", "))
        # the blur is home_v1's low-fi device — "static-blurred 10px on purpose:
        # low-fi until the November shoot, so the client discusses layout, not
        # imagery." It is exactly wrong on THIS page: these thirteen portraits
        # are the finished studio photographs and they are the page's content.
        # The scale(1.06) goes with it — it existed only to hide the halo the
        # blur left inside the crop.
        # ---- the band stays white (Adi 2026-08-31: "bg color tetap putih") --
        # home_v1 sets the bench on --paper so it separates from the white
        # sections above and below it. Here there is nothing to separate from:
        # with the section head gone the bench is simply the rest of the page,
        # and a tinted band would draw a box around it for no reason. The
        # padding stays — it is the only thing holding the bench off the copy.
        if sel == ".teams":
            before = decls
            decls = decls.replace("background: var(--paper);", "background: #fff;")
            assert decls != before, "the bench band no longer sets a background"
            # ---- the gap above the bench (Adi 2026-08-31: "kurangi sedikit
            # jarak 110px itu"). home_v1's 64px held its section head off the
            # band edge; with the head gone the same padding is holding the
            # bench off the copy, which is a different job. This rule is now
            # the SOLE owner of that gap — .icontext's bottom padding went to
            # zero — so the number here is the number on the page.
            #
            # 110 -> 80 -> 56. The last step lands on the band's OWN bottom
            # padding, so the bench now sits in equal air above and below
            # rather than on a top figure picked by eye. There is nothing left
            # to trim to without the copy and the first row of faces starting
            # to read as one block.
            before = decls
            decls = decls.replace("padding: 64px 0 56px;", "padding: 56px 0 56px;")
            assert decls != before, "the band's padding is not what we expected"
        if ".bph img" in sel:
            before = decls
            decls = re.sub(r"\s*(filter: blur\(10px\)|transform: scale\(1\.06\));", "", decls)
            assert decls != before, "the blur is no longer where it was"
            assert "blur" not in decls and "scale" not in decls
        out.append("  %s { %s }" % (sel, decls))
    css = "\n".join(out)
    assert ".teams .bench { display: grid" in css and ".teams .bx .bph {" in css

    # the 1280px box: home_v1 calls it .wrap, this page calls it .container
    css = re.sub(r"\.wrap\b", ".container", css)
    box = HNS["_box_delta"](h, ".teams")

    i = h.index('<section class="teams">')
    j = h.index("</section>", i) + len("</section>")
    html = h[i:j]

    a = html.index('    <div class="head">')
    b = html.index('    <div class="bench">')
    assert 0 < a < b and "whole bench" in html[a:b] and "on Google" in html[a:b], \
        "the section head is not where it was"
    html = html[:a] + html[b:]
    assert 'class="head"' not in html and 'class="kick"' not in html and \
        'class="cred"' not in html and "<h2" not in html, "part of the head survived"
    html = re.sub(r'class="([^"]*)"',
                  lambda m: 'class="%s"' % " ".join(
                      "container" if c == "wrap" else c for c in m.group(1).split()), html)
    # home_v1 serves its stand-ins out of prototype/img/; this page uses the
    # real media library. Every rewritten path is checked on disk.
    html = html.replace('src="img/', 'src="/wp-content/uploads/')
    for src in re.findall(r'src="/wp-content/uploads/([^"]+)"', html):
        assert os.path.exists(os.path.join(UPLOADS, src)), "missing image: " + src

    # the bench must be THIS page's people. home_v1 shows twelve lawyers; the
    # live team page shows those twelve plus a recruitment tile.
    names = re.findall(r'<span class="bnm">([^<]+)</span>', html)
    assert len(names) == 12, "the bench is not twelve"
    live = {c["name"] for c in TEAM}
    assert set(names) < live, "the bench names someone the live page does not: %s" % (
        sorted(set(names) - live),)
    assert live - set(names) == {"IS IT YOU?"}, \
        "the bench is missing a lawyer the live page lists: %s" % (sorted(live - set(names)),)

    # ...and that recruitment tile is not a lawyer, so it does not go in the
    # bench: thirteen cells in a six-column grid would leave it alone on a
    # third row. It keeps its own line, with the live page's own words.
    careers = [c for c in TEAM if c["name"] == "IS IT YOU?"][0]
    html = html.replace("    </div>\n  </div>\n</section>",
        '    </div>\n'
        '    <p class="joinus"><a href="/lawyers/%s/"><b>Is it you?</b> %s &rarr;</a></p>\n'
        '  </div>\n</section>' % (careers["slug"], careers["role"]), 1)
    assert 'class="joinus"' in html, "the careers line did not attach"

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

    head = [box.rstrip(),
            "/* ---------- 5 · the bench — home_v1's `.teams`, bench only ----------",
            "   home_v1's section head is dropped (the intro above already introduces",
            "   these people) and the band is white, not --paper. The grid it replaces",
            "   thirteen portraits at thirteen widths (126.3 - 143.6) in fixed-height",
            "   boxes; .bph is one 250px box with object-fit:cover, so every face is",
            "   the same size. home_v1's 10px placeholder blur is NOT carried over —",
            "   see the note in the generator. */",
            ".teams{line-height:normal}"]
    if missing:
        head.append(".teams{%s}" % ";".join("%s:%s" % (v, fill[v]) for v in missing))
    head.append(
        "/* the recruitment tile is not a lawyer and does not belong in the bench */\n"
        ".teams .joinus{text-align:center;margin:34px 0 0;font-size:13px}\n"
        ".teams .joinus a{font-family:Raleway,sans-serif;letter-spacing:.04em;"
        "text-transform:uppercase;color:var(--brown)}\n"
        ".teams .joinus a:hover{color:var(--orange)}")
    return "\n".join(x for x in head if x) + "\n" + css + "\n", "\n" + html + "\n"


# ===========================================================================
def _assert_no_collision(css, hero_html):
    """No page rule may reach inside the hero, and no hero rule outside it."""
    hero_classes = set()
    for c in re.findall(r'class="([^"]*)"', hero_html):
        hero_classes.update(c.split())
    bad = [sel for sel, cls in HNS["_selector_classes"](css)
           if cls and cls <= hero_classes and not cls <= HNS["SHARED"]]
    assert not bad, "these page rules would reach inside the hero: %s" % bad
    for sel, cls in HNS["_selector_classes"](hero_css()):
        assert sel.startswith(".shero"), "hero rule %r escaped .shero" % sel


def build():
    hero_html = hero_markup()
    bcss, bhtml = _bench()
    ccss, chtml, cscript = HNS["_consult"]()
    fcss, fhtml = HNS["_footer"]()
    hcss, hhtml = HNS["_header"]()

    # the header lift marks Injuries as the current practice area, because the
    # page it was written for is one. This page is not: the nav's current item
    # is Your Team, and the mega menu behind it is the one that lists the people
    # this page is about. Both swaps are asserted, so a change to home_v1's nav
    # breaks the build instead of quietly leaving the wrong item underlined.
    was = hhtml
    hhtml = hhtml.replace(
        '<div class="mi cur"><a href="/our-services/bc-injury-claims/">Injuries</a>',
        '<div class="mi"><a href="/our-services/bc-injury-claims/">Injuries</a>', 1)
    assert hhtml != was, "the header no longer marks Injuries as current"
    was = hhtml
    hhtml = hhtml.replace('<div class="mi"><a href="#">Your Team</a>',
                          '<div class="mi cur"><a href="/lawyers/">Your Team</a>', 1)
    assert hhtml != was and hhtml.count('class="mi cur"') == 1, \
        "could not move the current marker to Your Team"

    # the collision test is run on everything EXCEPT the hero — its own rules
    # are all scoped .shero and would otherwise flag themselves.
    css = (CSS.replace("__HEADER_CSS__", hcss)
              .replace("__FOOTER_CSS__", fcss)
              .replace("__CONSULT_CSS__", ccss)
              # the shell's sidebar slot: this page has no sidebar
              .replace("__SIDENAV_CSS__", ""))
    assert css.count("__HERO_CSS__") == 1
    _assert_no_collision(css.replace("__HERO_CSS__", "") + INTRO_CSS + SOLO_CSS + bcss,
                         hero_html)
    css = css.replace("__HERO_CSS__", hero_css()) + INTRO_CSS + SOLO_CSS + bcss
    assert not re.search(r"__[A-Z_]+__", css), "a stylesheet slot went unfilled"

    html = HNS["head"]()
    i, j = html.index("<style>") + 7, html.index("</style>")
    assert "__CSS__" not in html and j > i
    html = html[:i] + css + html[j:]
    html = html.replace("<title>League Law &mdash; Car Accident Claims</title>",
                        "<title>League Law &mdash; Your Team</title>")
    assert "<title>League Law &mdash; Your Team</title>" in html, "the title did not change"

    out = "".join([html, hhtml, hero_html, entry(), bhtml, fhtml,
                   "\n<!-- consultation tab + modal, lifted from home_v1 -->\n",
                   chtml, cscript, "\n</body>\n</html>\n"])

    assert out.count("<h1") == 1, "expected exactly one h1"
    assert out.count('<section class="shero">') == 1
    assert out.count('<section class="teams">') == 1
    assert out.count('class="bx"') == 12, "expected twelve bench cells"
    assert '<footer class="nfoot">' in out and '<header class="nheader">' in out
    assert 'class="ctatab"' in out, "the consultation tab is missing"
    assert "<aside" not in out and 'class="sidebar"' not in out, "a sidebar survived"
    assert 'class="phead' not in out and 'class="cstrip"' not in out, \
        "a replaced section survived"
    assert 'class="hex"' not in out, "a bare .hex survived — Adi asked for it to go"
    assert "blur(10px)" not in out, "the placeholder blur survived"
    return out


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