#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# injuries-index.html — the index for ONE service.
#
# Adi 2026-08-31: "maksud saya service index ini menjadi injuries index terlebih
# dahulu."  So the page is no longer five services with their pages under them —
# it is Injuries and its thirteen claims.
#
# THAT CHANGES WHICH OPTION SHOULD WIN, and it is worth saying rather than
# quietly switching. On the option sheet S1 won on two arguments, and this page
# retires both of them:
#   * "it survives an area with nothing under it" — there is only one area here,
#     and it has thirteen pages;
#   * "its head costs no height, which matters when five stack" — nothing
#     stacks, and the A1 hero already IS the head. A second block repeating the
#     plate, the name and the paragraph would say everything twice.
# So the body of this page is a LIST and nothing else. LAYOUT below picks which
# of the sheet's list treatments draws it; every one of them is the same option
# with its service head removed, because the hero is that head now.
#
# Nothing is transcribed. The shell comes out of gen_inner_replica_hero.py, the
# thirteen claims and their real URLs out of gen_service_index.py, the copy out
# of a read-only crawl of the live page.
# ---------------------------------------------------------------------------
import io, os, re, sys

OUT  = "/srv/apps/leaguelaw/wp-content/prototype/injuries-index.html"
HGEN = "/srv/apps/leaguelaw/prototyping/gen_inner_replica_hero.py"
SGEN = "/srv/apps/leaguelaw/prototyping/gen_service_index.py"
HERE = os.path.dirname(os.path.abspath(__file__))

# which treatment draws the thirteen. Adi picked C7 off claims-options.html
# on 2026-08-31: "gunakan c7 saja terlebih dahulu".
#   "c7"   = four across, 296 x 222, the orphan as it falls, grayscale off on hover
#   "grid" = five across, 230.4 x 173 (the team page's bench module)
#   "cols" = three ruled columns, text only
LAYOUT = "c7"
# what a tile does under the cursor.
#   "colour" = filter:grayscale(0) — C7's own hover, as picked
#   "none"   = the orange rule under the tile and nothing more
HOVER = "colour"

_h = io.open(HGEN, encoding="utf-8").read()
HNS = {"__name__": "gen_inner_replica_hero", "__file__": HGEN}
exec(compile(_h, HGEN, "exec"), HNS)
CSS = HNS["CSS"]

_s = io.open(SGEN, encoding="utf-8").read()
SNS = {"__name__": "gen_service_index", "__file__": SGEN}
exec(compile(_s, SGEN, "exec"), SNS)
INJ = next(a for a in SNS["AREAS"] if a["name"] == "Injuries")
GROUPS = INJ["groups"]
assert INJ["count"] == 13 and [len(i) for _, _, i in GROUPS] == [6, 4, 3]

# ---------------------------------------------------------------------------
# THE PHOTOGRAPHS — one per claim, and every one is the image the LIVE claim
# page already uses.
#
# The practice-areas round recorded that there is no imagery for the thirteen
# claims, which is why no option on the sheet was photo-led. That was true of
# the media library as a PRACTICE-AREA set: nothing is attached to a claim page
# as a featured image. But each of the thirteen live pages carries an image in
# its own body or its og:image, and crawling the thirteen turns up a picture
# for every one. So the grid is drawable after all — from the site's own
# choices, not from a pairing this prototype invented.
#
# Sizes are the derivative nearest 2x the 230.4px tile (461). Twelve of
# thirteen have a 768px derivative; DogBiteBW is 406px native and has none, so
# it is the one tile with no retina headroom. PedestrianInjury's ORIGINAL is
# 5472x3648 and 3.78 MB unresized — a page-weight defect on the live site, and
# the reason this map names derivatives rather than originals.
CLAIM_PHOTO = {
    "Car Accident Claims":        "blackandwhitemiata-1-768x576.png",
    "Motorcycle Accident Claims": "blackandwhiteMotorCycle-1-768x1024.png",
    "Cyclist Accident Claims":    "bike-1-768x512.jpg",
    "Passenger Injury Claims":    "passengerclaim-1-768x512.png",
    "Pedestrian Injury Claims":   "PedestrianInjury-1-768x512.jpeg",
    "Hit and Run Injury Claims":  "hitandrunimage-1-768x432.png",
    "Slip and Fall":              "slipandfall-1-768x508.jpg",
    "Premises Liability Claims":  "premiseliability-1-768x576.png",
    "School Accident Claims":     "SchoolAccident-1-768x512.png",
    "Product Liability Claims":   "shopping-1165437_960_720-768x442.jpg",
    "Dog Attack":                 "DogBiteBW-1.png",
    "Boating Accidents":          "boat_accident-768x384.jpg",
    "Assault &amp; Battery":      "girl-517555_1920-1-768x537.jpg",
}
UPLOADS = "/srv/apps/leaguelaw/wp-content/uploads"
# where a photograph's crop is anchored when the default centre would miss the
# subject. Only the two portrait sources need one; the CSS says what each
# photograph actually shows.
CROP = {"Motorcycle Accident Claims": "p-motorcycle", "Dog Attack": "p-dog"}
_names = [t for _, _, items in GROUPS for t, _ in items]
assert sorted(CLAIM_PHOTO) == sorted(_names), \
    "the photo map and the thirteen claims disagree: %s" % (
        sorted(set(CLAIM_PHOTO) ^ set(_names)),)
for _f in CLAIM_PHOTO.values():
    assert os.path.exists(os.path.join(UPLOADS, _f)), "missing image: " + _f


def _tone():
    """Which of the thirteen are already black and white — measured, not read
    off the filenames.

    Three of them SAY they are ("blackandwhite...", "DogBiteBW"), which is
    exactly the kind of evidence that is worth nothing: a name is not a
    measurement. Mean per-pixel chroma (max channel minus min) on an 80x80
    thumbnail, under 8 is monochrome."""
    from PIL import Image
    out = {}
    for name, f in CLAIM_PHOTO.items():
        im = Image.open(os.path.join(UPLOADS, f)).convert("RGB").resize((80, 80))
        px = list(im.getdata())
        out[name] = sum(max(p) - min(p) for p in px) / len(px)
    return out


TONE = _tone()
COLOUR = sorted(n for n, c in TONE.items() if c >= 8)
# ELEVEN of the thirteen are already monochrome. Only the cyclist and the
# pedestrian photographs are in colour, and in a grid of thirteen those two
# read as mistakes rather than as variety. The site's own tone is black and
# white — every one of the twelve team portraits, the inner service hero, and
# eleven of these thirteen — so the grid is filtered to match. One line, and
# it changes exactly two tiles; the assertion below fails if that ever stops
# being true, so the filter cannot quietly start doing more than it should.
assert COLOUR == ["Cyclist Accident Claims", "Pedestrian Injury Claims"], COLOUR

INTRO = io.open(os.path.join(HERE, "injuries_intro_body.html"), encoding="utf-8").read().strip()
assert INTRO.startswith("<h2>Injury Claims Lawyers</h2>")
# The live page's five body paragraphs are crawled and kept next to this file
# (injuries_body.html) but are NOT placed. This page is an index; where that
# copy goes — above the list, below it, or on a page of its own — is a decision
# nobody has made. It is saved so that placing it is a one-line change and so
# that nothing is lost by leaving it out today.
assert os.path.exists(os.path.join(HERE, "injuries_body.html"))

# ---------------------------------------------------------------------------
# THE HERO
# ---------------------------------------------------------------------------
# The live services page pairs Injuries with this photograph — it is the tile
# background on /our-services/. 1024x683 (1.5:1) in the 420x380 (1.105:1)
# panel, so `cover` matches the height and crops the width to 73% of the frame:
# a downscale, not an upscale, which the practice-area photographs mostly are
# not (the Injuries thumb is 411px native and would blow up 1.47x here).
HERO_PHOTO  = "/wp-content/uploads/car-crash-1024x683.jpg"
HERO_CRUMBS = ('<nav class="crumbs %s"><a href="/">Home</a><i></i>'
               '<a href="/our-services/">What We Do</a><i></i>'
               '<span class="here">Injuries</span></nav>')
# the live h1 is "Personal Injury Lawyers in Victoria". Same decision as the
# team page's 68-character title: the hero takes the site's own short name for
# the section — the word in the nav and in the breadcrumb — and the keyword
# phrasing is a copy question for the firm, not something a layout should
# silently keep or silently drop.
HERO_TITLE  = '<h1 class="htitle">Injuries</h1>'
HERO_SUB    = "What We Do"


def hero_css():
    return HNS["_hero_css"]()


def hero_markup():
    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-ambulance")
    html = ns["A1"]()
    html = '<section class="shero">' + html[html.index("</header>") + len("</header>"):]
    old = '<p class="hsub">Injuries</p>'
    assert html.count(old) == 1
    html = html.replace(old, '<p class="hsub">%s</p>' % HERO_SUB)
    assert "fa-ambulance" in html and "fa-car" not in html and html.count("<h1") == 1
    return "\n<!-- 3 · hero — A1 -->\n" + HNS["_rename_markup"](html) + "\n"


# ---------------------------------------------------------------------------
INTRO_CSS = r"""
/* ---------- intro — centred, no hexagon, same as the other rebuilt pages -- */
.icontext{padding:14px 0 0}
.icontext .tc{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:0}
.cwrap.solo{display:block}
.cwrap.solo .primary{width:100%;padding-bottom:0}
"""

# Colours are the sheet's, and they were computed: names brown 10.87:1, labels
# #6B6B6B 5.33:1, hover #A94A29 5.68:1. Brand orange is 3.03:1 and carries no
# word — it is the rule under a group label and nothing else.
LIST_CSS = r"""
/* ---------- 5 · the thirteen claims -------------------------------------
   The list and nothing else: the hero above it already carries the hexagon,
   the name and the section, so a second head would say all three twice.
   Three ruled columns on the 1280 container (S2's treatment off the option
   sheet, with its head removed for that reason). At 405px every claim sits
   on ONE line — the longest, "Motorcycle Accident Claims", measures ~205px —
   where the index page's 256px columns wrapped four of the thirteen. */
.claims{padding:56px 0 64px}
.claims .cols{display:flex;gap:32px}
.claims .col{flex:1 1 0}
.claims .glab{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;line-height:18px;
              letter-spacing:.16em;text-transform:uppercase;color:#6B6B6B;
              padding-bottom:8px;border-bottom:2px solid var(--orange);margin-bottom:4px}
.claims .glab .n{color:#B9B2A8;margin-left:6px}
.claims .col > a{display:block;font-size:15px;line-height:21px;color:var(--brown);
                 padding:10px 0;border-bottom:1px solid var(--line)}
.claims .col > a:hover{color:#A94A29}
"""

GRID_CSS = r"""
/* ---------- 5 · the thirteen claims, as a grid ---------------------------
   Five across, 5/5/3. The module is the team page's bench exactly —
   230.4px cell, 32px gutter, 1280 = 5 x 230.4 + 4 x 32 — so the site has ONE
   card, used for people on one page and for claims on this one, rather than
   two that nearly match. Thirteen is awkward in every column count (4 leaves
   a lone tile on row four, 6 leaves one on row three); five leaves a row of
   three, which reads as a row rather than an orphan.

   THE GROUPING IS NOT DRAWN HERE. Road Accidents / On Premises / Other Claims
   is home_v1's proposal and the live menu lists the thirteen flat, so a grid
   that ignores it loses nothing the site has decided — and the layout does not
   have to survive the taxonomy changing. */
/* 56px above, the same beat the team page's bench sits on */
.claims{padding:56px 0 64px}
.claims .grid{display:grid;grid-template-columns:repeat(5,1fr);column-gap:32px;row-gap:34px}
.claims .cx{display:block;text-align:center}
/* 230.4 x 3:4 = 172.8. object-fit:cover in a fixed box is what makes thirteen
   pictures of thirteen shapes — 0.71:1 to 2.00:1 — the same size; the same
   mechanism that retired the thirteen-widths defect on the team page. */
/* position:relative is kept even with no overlay on it today: any absolutely
   positioned child added here (a hover layer, a scrim, a badge) needs it, and
   without it such a child resolves against the initial containing block. That
   is not hypothetical — it painted orange over the whole section once. */
.claims .cx .ph{display:block;position:relative;width:100%;height:173px;overflow:hidden;
                transition:box-shadow .18s ease}
/* eleven of the thirteen are already monochrome on the live site — measured,
   not taken from their filenames. This makes the other two match rather than
   read as mistakes, and it is the site's own tone: every team portrait and
   the inner service hero are black and white too. Remove this one line to
   see the two colour photographs. */
.claims .cx .ph img{width:100%;height:100%;object-fit:cover;display:block;
                    filter:grayscale(1)}
/* Two sources are PORTRAIT (Dog 0.71:1, Motorcycle 0.75:1) and lose most of
   their height to a 4:3 crop, so where the crop sits is not a detail — it
   decides whether the tile shows the subject at all. Both were looked at
   rather than guessed at: the motorcycle photograph is a rider's-eye view
   with the machine in the LOWER half, so a top anchor showed an empty road
   and no motorcycle; the dog's open jaw fills the frame and centres slightly
   high. Anchored per photograph, not by one rule for "tall". */
.claims .cx.p-motorcycle .ph img{object-position:50% 62%}
.claims .cx.p-dog .ph img{object-position:50% 40%}
.claims .cx:hover .ph{box-shadow:0 4px 0 var(--orange)}
/* No colour hover on the page: Adi 2026-08-31, "hanya update pada C7" —
   the hover belongs to the option, not to this build, until C7 is picked.
   The three treatments and the measurement behind them are in
   claims-options.html (H·a / H·b / H·c). */
.claims .cx .nm{display:block;font-family:Raleway,sans-serif;font-weight:700;font-size:12px;
                line-height:17px;letter-spacing:.04em;text-transform:uppercase;
                color:var(--brown);margin-top:11px}
/* the bench turns its name brand orange on hover, which is 3.03:1 — under the
   floor for 12px bold. Deepened here; the bench should follow. */
.claims .cx:hover .nm{color:#A94A29}
"""

GRID4_CSS = r"""
/* ---------- 5 · the thirteen claims, C7 ---------------------------------
   Picked off claims-options.html, 2026-08-31. Four across:
   1280 = 4 x 296 + 3 x 32, tile 296 x 222 at 4:3 — 28% wider than the
   five-across version, so these are the biggest photographs any even grid
   on the sheet gives them.

   THE LAST ROW IS ONE TILE BESIDE THREE EMPTY CELLS, and that is not an
   oversight: thirteen is prime, and four is the column count it likes
   least. The sheet draws the two honest cures beside this one — C7·a puts
   the first claim across the whole top row so 1 + 4 + 4 + 4 = 13, and C7·b
   centres the short row — and either is a one-line change here. It is left
   as it falls because that is the option that was picked. */
.claims{padding:56px 0 64px}
.claims .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:32px}
.claims .cx{display:block;text-align:center}
/* position:relative is kept even with nothing positioned inside it today:
   an absolutely positioned child added later resolves against the initial
   containing block without it, which once painted a whole section orange. */
.claims .cx .ph{display:block;position:relative;width:100%;height:222px;overflow:hidden;
                transition:box-shadow .18s ease}
/* eleven of the thirteen are already monochrome on the live site — measured,
   not taken from their filenames — so the filter makes the other two match
   rather than read as mistakes. */
.claims .cx .ph img{width:100%;height:100%;object-fit:cover;display:block;
                    filter:grayscale(1);transition:filter .3s ease}
/* C7's hover: the grayscale comes off. Worth remembering what it does and
   does not do — ELEVEN of the thirteen sources are black and white in the
   file, so on eleven tiles this returns nothing and only the cyclist and the
   pedestrian change. That is C7 as drawn and as picked; H·b and H·c on the
   sheet are what to reach for if it needs to work on all thirteen. */
.claims .cx:hover .ph img{filter:grayscale(0)}
/* the two portrait sources are anchored where their subject is: the
   motorcycle is a rider's-eye view with the machine in the LOWER half, and
   the dog's open jaw centres slightly high. */
.claims .cx.p-motorcycle .ph img{object-position:50% 62%}
.claims .cx.p-dog .ph img{object-position:50% 40%}
.claims .cx:hover .ph{box-shadow:0 4px 0 var(--orange)}
.claims .cx .nm{display:block;font-family:Raleway,sans-serif;font-weight:700;font-size:12px;
                line-height:17px;letter-spacing:.04em;text-transform:uppercase;
                color:var(--brown);margin-top:11px}
/* brand orange is 3.03:1 — under the floor for 12px bold — so hover is the
   deepened #A94A29 at 5.68:1. */
.claims .cx:hover .nm{color:#A94A29}
"""


def glab(gh, gr, n):
    inner = '<a href="%s">%s</a>' % (gr, gh) if gr else gh
    return '<div class="glab">%s<span class="n">%d</span></div>' % (inner, n)


def claims_grid():
    from PIL import Image
    cells = []
    for _gh, _gr, items in GROUPS:
        for t, h in items:
            f = CLAIM_PHOTO[t]
            w, hh = Image.open(os.path.join(UPLOADS, f)).size
            cls = " " + CROP[t] if t in CROP else ""
            # a portrait source with no crop anchor of its own is a bug waiting
            # to happen, so the build refuses one rather than shipping a tile
            # cropped to whatever the middle of the frame happens to hold.
            assert w >= hh or t in CROP, "%s is portrait and has no crop anchor" % t
            cells.append('<a class="cx%s" href="%s"><span class="ph">'
                         '<img src="/wp-content/uploads/%s" alt=""></span>'
                         '<span class="nm">%s</span></a>' % (cls, h, f, t))
    assert len(cells) == 13
    return ('\n<!-- 5 \u00b7 the thirteen claims -->\n<section class="claims">\n'
            '  <div class="container"><div class="grid">%s</div></div>\n</section>\n'
            % "".join(cells))


def claims_cols():
    cols = "".join(
        '<div class="col">%s%s</div>'
        % (glab(gh, gr, len(items)),
           "".join('<a href="%s">%s</a>' % (h, t) for t, h in items))
        for gh, gr, items in GROUPS)
    return ('\n<!-- 5 · the thirteen claims -->\n<section class="claims">\n'
            '  <div class="container"><div class="cols">%s</div></div>\n</section>\n' % cols)


def claims():
    return {"c7": claims_grid, "grid": claims_grid, "cols": claims_cols}[LAYOUT]()


def entry():
    return ("""
<!-- 4 · intro -->
<div class="container cwrap solo">
  <main class="primary">
    <div class="entry">
      <div class="icontext"><div class="tc">
""" + INTRO + """
      </div></div>
    </div>
  </main>
</div>
""")


def _assert_no_collision(css, hero_html):
    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


def build():
    assert LAYOUT in ("c7", "grid", "cols"), LAYOUT
    assert (HOVER == "colour") == (LAYOUT == "c7"), \
        "the grayscale-off hover is C7's; it is not drawn for the other treatments"
    hero_html = hero_markup()
    ccss, chtml, cscript = HNS["_consult"]()
    fcss, fhtml = HNS["_footer"]()
    hcss, hhtml = HNS["_header"]()
    # this page IS an injuries page, so the header's own current marker is
    # right for once and is left where the lift puts it.
    assert hhtml.count('class="mi cur"') == 1 and "Injuries</a>" in hhtml

    css = (CSS.replace("__HEADER_CSS__", hcss).replace("__FOOTER_CSS__", fcss)
              .replace("__CONSULT_CSS__", ccss).replace("__SIDENAV_CSS__", ""))
    _assert_no_collision(css.replace("__HERO_CSS__", "") + INTRO_CSS + GRID4_CSS
                         + GRID_CSS + LIST_CSS, hero_html)
    css = css.replace("__HERO_CSS__", hero_css()) + INTRO_CSS + \
          {"c7": GRID4_CSS, "grid": GRID_CSS, "cols": LIST_CSS}[LAYOUT]
    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>")
    html = html[:i] + css + html[j:]
    html = html.replace("<title>League Law &mdash; Car Accident Claims</title>",
                        "<title>League Law &mdash; Injuries</title>")
    assert "<title>League Law &mdash; Injuries</title>" in html

    out = "".join([html, hhtml, hero_html, entry(), claims(), fhtml,
                   "\n<!-- consultation tab + modal, lifted from home_v1 -->\n",
                   chtml, cscript, "\n</body>\n</html>\n"])
    assert out.count("<h1") == 1
    assert out.count('<section class="shero">') == 1 and out.count('<section class="claims">') == 1
    assert (out.count('class="cx') == 13 if LAYOUT in ("c7", "grid")
            else out.count('class="col"') == 3)
    assert '<footer class="nfoot">' in out and '<header class="nheader">' in out
    assert 'class="ctatab"' in out and "<aside" not in out and 'class="sidebar"' not in out
    assert 'class="hex"' not in out, "a bare .hex survived"
    seg = out[out.index('<section class="claims">'):out.index("</section>", out.index('<section class="claims">'))]
    links = re.findall(r'<a [^>]*?href="([^"]*)"', seg)
    assert len(links) == 13, "expected the thirteen claims, got %d" % len(links)
    assert all(l.startswith("/our-services/bc-injury-claims/") for l in links), links
    assert len(set(links)) == 13, "a claim is linked twice"
    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)))
