#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# careers.html — the Careers page.
#
# Adi 2026-08-31: "saya ingin membuat prototype untuk halaman career ... untuk
# header, hero, footer dan ctatab mengikuti halaman halaman sebelumnya."
#
# So the shell is not designed here, it is EXECUTED — the same four calls the
# team page, the service index and the injuries index all make:
#   HNS["_header"]()  ·  HNS["_hero_css"]() + the option sheet's A1() builder
#   HNS["_footer"]()  ·  HNS["_consult"]()
# Five pages, one shell, no second copy to forget.
#
# The content is a read-only crawl of https://www.leaguelaw.com/career-opportunities/
# extracted to careers.json — three postings with their own paragraphs, their
# requirement lists and their apply addresses, plus the closing block about
# future vacancies. Nothing is retyped; the non-breaking spaces are asserted.
# ---------------------------------------------------------------------------
import io, json, os, re, sys

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

_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"]

DATA = json.load(io.open(os.path.join(HERE, "careers.json"), encoding="utf-8"))
POSTS, TAIL = DATA["posts"], DATA["tail"]
assert len(POSTS) == 3 and len(TAIL) == 3
assert all(p["email"] for p in POSTS), "a posting lost its apply address"
assert " " in "".join(b["html"] for p in POSTS for b in p["body"]) or \
       "&nbsp;" in "".join(TAIL), "the copy lost its non-breaking spaces"

# ---------------------------------------------------------------------------
# THE HERO
# ---------------------------------------------------------------------------
# The live page draws its own icon: `<i class="fa fa-suitcase">` sits in the
# hexagon at the top of its intro block. A1's hexagon takes the same glyph, so
# the page keeps its own mark rather than being given one.
HERO_ICON   = "fa-suitcase"
# Two people working at a desk. It is a stand-in like every other hero on this
# rebuild and carries the same tag: the firm has no photograph of its own
# people at work, and that is what the November shoot is for. 768x512 into the
# 420x380 panel is a downscale.
HERO_PHOTO  = "/wp-content/uploads/gabrielle-henderson-HJckKnwCXxQ-unsplash-1-768x512.jpg"
HERO_CRUMBS = ('<nav class="crumbs %s"><a href="/">Home</a><i></i>'
               '<span class="here">Careers</span></nav>')
# the live h1 is "Careers" — short, and the site's own word for the page, so
# unlike the team page and the injuries page there is nothing to decide here.
HERO_TITLE  = '<h1 class="htitle">Careers</h1>'
# A1's fifth line is the parent section; this page has none.
#
# It said "3 positions open" and Adi rejected it, correctly: a number in the
# hero is a number somebody has to remember to change, and the day a posting
# is filled the hero is wrong. Even derived from the data here, on the real
# site those postings are hand-edited paragraphs — the count would be
# maintained by hand, and the first person to forget makes the firm look
# careless on the one page where carefulness is the product.
#
# So the line says something that is true whatever the board holds. This is
# the page's own words: its h2 is "Career Opportunities at League and Williams
# – Apply to join our team!"
#
# Rejected, and why: "3 positions open" (rots) · "Victoria, British Columbia"
# (true and durable, but the postings already say it twice and it tells a
# visitor nothing they came for) · "We're hiring" (rots the same way the
# moment nothing is open) · "Careers" (repeats the h1 directly above it).
HERO_SUB    = "Join the team"


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", HERO_ICON)
    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 HERO_ICON 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, as on the other rebuilt pages ---
   The hexagon the live page puts here is in the hero now, so it is not
   drawn twice. 762px measure held rather than widened. */
.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 ones this rebuild has already computed. On white: headings
# and body brown 10.87:1, meta #6B6B6B 5.33:1, the apply link #2F7186 5.50:1,
# hover #A94A29 5.68:1. Brand orange is 3.03:1 and carries no word — it is the
# rule down the open posting and the marker in the summary, nothing else.
JOBS_CSS = r"""
/* ---------- 5 · the three postings --------------------------------------
   NOTHING IS HIDDEN. These were a <details> accordion and Adi rejected it:
   an open position is what a visitor came for, and a layout whose first act
   is to close two of the three makes them work to find out whether the firm
   has anything for them. Search engines and a Ctrl-F both prefer it open
   too. So all three are on the page at rest.

   What the accordion was solving is real — three postings of ~350 words is
   2,000+ words — so the length is handled by NAVIGATION rather than by
   concealment: a jump list at the top of the section names the three roles
   and takes you to one. That list carries no count, for the same reason the
   hero no longer does.

   THE LIVE PAGE HAS NO HEADINGS FOR THESE. Its three job titles are
   `<p><strong>…</strong></p>` — bold paragraphs — so the whole page has one
   heading for three jobs and a screen reader's heading list says "Career
   Opportunities" and stops. Here each posting is a real <h3>, and the build
   counts them so this comment cannot drift from the markup.

   --line and --paper are home_v1's tokens and are NOT on this page's :root,
   so both are written as literals. The build asserts it: a var that resolves
   to nothing fails silently — it was a transparent panel AND a missing rule
   between every posting, and nothing errored. */
.jobs{padding:52px 0 64px}
.jobs .wrapn{max-width:900px;margin:0 auto}

/* the jump list — P2 off careers-options.html, picked 2026-08-31.
   It was a flex row of full job titles, and Adi rejected it: at three it fits
   on one line and looks tidy, at nine it is three ragged lines with nothing
   aligned — a paragraph of links rather than a list. A component that only
   works at the count it was drawn for is not a component.
   These are ruled rows with the columns aligned down the page: name, place,
   door. ALIGNMENT IS THE WHOLE POINT — it is what a wrapping row cannot give
   and what lets the eye run down one column instead of through a paragraph,
   and it costs the same per role whether there are three or thirty. No count
   anywhere, for the same reason the hero no longer carries one. */
.jobs .roles{border-top:1px solid #E4E1DC;margin:0 0 6px}
.jobs .roles a{display:flex;align-items:baseline;gap:24px;padding:16px 0;
               border-bottom:1px solid #E4E1DC}
.jobs .roles a:hover{background:#F9F9F9}
.jobs .roles .rn{flex:1;font-family:Raleway,sans-serif;font-weight:300;font-size:18px;
                 letter-spacing:.04em;text-transform:uppercase;color:var(--brown);line-height:1.25}
/* the meta column is EMPTY for the litigator posting, because the live page
   states no location for it. The column stays — an empty cell in an aligned
   list is information; filling it in would be invention. */
.jobs .roles .rl{flex:0 0 210px;font-family:Raleway,sans-serif;font-weight:700;font-size:11px;
                 letter-spacing:.16em;text-transform:uppercase;color:#6B6B6B}
.jobs .roles .go{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.14em;
                 text-transform:uppercase;color:#2F7186;white-space:nowrap}
.jobs .roles a:hover .rn,.jobs .roles a:hover .go{color:#A94A29}

.jobs .job{border-top:1px solid #E4E1DC;padding:34px 0 8px;scroll-margin-top:24px}
.jobs .job:last-of-type{border-bottom:1px solid #E4E1DC;padding-bottom:34px}
/* an h3, not a span: the comment above says each posting is a real heading
   and the markup has to be able to back that. The build counts them. */
.jobs .jt{font-family:Raleway,sans-serif;font-weight:300;font-size:24px;letter-spacing:.04em;
          text-transform:uppercase;color:var(--brown);line-height:1.25;display:block;margin:0}
.jobs .jm{display:block;margin-top:7px;font-family:Raleway,sans-serif;font-weight:700;
          font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:#6B6B6B}
.jobs .jb{padding:22px 0 0;font-size:17px;line-height:28px;color:var(--body)}
.jobs .jb p{margin:0 0 20px}
.jobs .jb ul{margin:0 0 20px 22px;padding:0}
.jobs .jb li{margin:0 0 8px}
.jobs .jb strong{color:var(--brown)}
.jobs .apply{border-left:3px solid var(--orange);padding:14px 0 14px 18px;margin-top:4px;
             font-size:15px;line-height:25px;color:#6B6B6B}
.jobs .apply a{color:#2F7186;font-weight:600}
.jobs .apply a:hover{color:#A94A29}
/* the closing block: no posting fits, send a resume anyway */
.jobs .spec{margin-top:44px;background:#F9F9F9;padding:34px 38px}
.jobs .spec h3{font-family:Raleway,sans-serif;font-weight:700;font-size:12px;letter-spacing:.16em;
               text-transform:uppercase;color:#6B6B6B;margin:0 0 14px}
.jobs .spec p{font-size:15px;line-height:25px;color:var(--body);margin:0 0 14px}
.jobs .spec p:last-child{margin:0}
.jobs .spec a{color:#2F7186;font-weight:600}
.jobs .spec a:hover{color:#A94A29}
"""


def _slug(t):
    return re.sub(r"[^a-z0-9]+", "-", t.lower().split("\u2013")[0].strip()).strip("-")


def jobs():
    out = []
    for p in POSTS:
        body = "".join(
            ("<ul>%s</ul>" if b["tag"] == "ul" else "<p>%s</p>") % b["html"] for b in p["body"])
        # ONE POSTING OF THREE STATES NO LOCATION. The litigator posting says
        # "downtown Victoria" inside its body and nothing on its title line,
        # where the other two carry "Victoria, British Columbia" as a second
        # line. The layout does not invent one — an empty meta line is the
        # truth, and the inconsistency is the firm's to close.
        meta = ('<span class="jm">%s</span>' % p["location"]) if p["location"] else ""
        out.append(
            '<article class="job" id="%s"><h3 class="jt">%s</h3>%s'
            '<div class="jb">%s<div class="apply">%s</div></div></article>'
            % (_slug(p["title"]), p["title"], meta, body,
               re.sub(r"([\w.]+@leaguelaw\.com)", r'<a href="mailto:\1">\1</a>',
                      re.sub(r"</?strong>", "", p["apply"]))))
    # the jump list names the roles, aligns them, and counts nothing
    roles = ('<nav class="roles">%s</nav>' % "".join(
        '<a href="#%s"><span class="rn">%s</span><span class="rl">%s</span>'
        '<span class="go">Read &rarr;</span></a>'
        % (_slug(p["title"]), p["title"].split("\u2013")[0].strip(), p["location"] or "")
        for p in POSTS))
    spec = ('<div class="spec"><h3>Nothing here that fits?</h3>%s</div>'
            % "".join("<p>%s</p>" % t for t in TAIL))
    return ('\n<!-- 5 · the three postings -->\n<section class="jobs">\n'
            '  <div class="container"><div class="wrapn">%s%s%s</div></div>\n</section>\n'
            % (roles, "".join(out), spec))


def entry():
    return ("""
<!-- 4 · intro -->
<div class="container cwrap solo">
  <main class="primary">
    <div class="entry">
      <div class="icontext"><div class="tc">
<h2>""" + DATA["h2"] + """</h2>
<p>""" + DATA["lead"] + """</p>
      </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():
    hero_html = hero_markup()
    ccss, chtml, cscript = HNS["_consult"]()
    fcss, fhtml = HNS["_footer"]()
    hcss, hhtml = HNS["_header"]()
    # careers is not a practice area, so nothing in the nav is current. The
    # header lift marks Injuries because the page it was written for is one.
    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 and 'class="mi cur"' not 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 + JOBS_CSS, hero_html)
    # every custom property this page's own CSS uses must be defined on its
    # :root. A var that resolves to nothing fails silently — a transparent
    # panel, an unstyled colour — and nothing errors.
    have = set(re.findall(r"(--[a-z-]+)\s*:", CSS[:CSS.index("}")]))
    # comments stripped first: a var named in a comment explaining why it is
    # NOT used would otherwise fail this check — the same trap that made the
    # header sweep drop every rule following a comment.
    _own = re.sub(r"/\*.*?\*/", "", INTRO_CSS + JOBS_CSS, flags=re.S)
    want = set(re.findall(r"var\((--[a-z-]+)\)", _own))
    assert not (want - have), "undefined custom properties: %s" % sorted(want - have)

    css = css.replace("__HERO_CSS__", hero_css()) + INTRO_CSS + JOBS_CSS
    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; Careers</title>")
    assert "<title>League Law &mdash; Careers</title>" in html

    out = "".join([html, hhtml, hero_html, entry(), jobs(), fhtml,
                   "\n<!-- consultation tab + modal, lifted from home_v1 -->\n",
                   chtml, cscript, "\n</body>\n</html>\n"])
    assert out.count("<h1") == 1
    seg = out[out.index('<section class="jobs">'):out.index("</section>", out.index('<section class="jobs">'))]
    assert seg.count('<article class="job"') == 3 and "<details" not in seg, \
        "the postings must not be hidden behind a disclosure"
    assert out.count('<nav class="roles">') == 1
    assert out.count('<span class="rn">') == 3 and out.count('<span class="go">') == 3, \
        "the jump list is not the aligned three-column row it was picked as"
    assert not re.search(r">\s*\d+\s+(positions|roles|openings)", out), \
        "a count crept back onto the page" 
    assert out.count('class="apply"') == 3 and out.count("mailto:") >= 4
    # the CSS comment claims each posting is a real heading; this is what
    # makes that claim true rather than aspirational.
    assert out.count('<h3 class="jt">') == 3, "the postings are not headings"
    assert '<footer class="nfoot">' in out and '<header class="nheader">' in out
    assert 'class="ctatab"' in out and "<aside" not in out
    assert 'class="sidebar"' not in out and 'class="phead' not in out
    assert 'class="hex"' not in out, "the intro hexagon should be in the hero, not 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)))
