#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# careers-options.html — options for displaying the open positions.
#
# Adi 2026-08-31: "class=roles pada bagian ini saya kurang setuju, karena jika
# open position ada banyak akan berantakan."
#
# He is right, and the objection names the real constraint: THE LAYOUT HAS TO
# SURVIVE A LONG BOARD. `.roles` is one flex row of full job titles — at three
# it fits on one line, at six it wraps into a paragraph of links with no
# alignment, and at nine it is a wall. A component that only works at the count
# it was drawn for is not a component.
#
# So every option here is drawn TWICE: with the real three, and with the same
# three repeated to nine. The nine are a STRESS TEST, not invented postings —
# the sheet says so on every frame. What is being compared is not how the
# option looks with three roles; it is what it does when the firm is hiring.
#
# The postings themselves are executed out of gen_careers.py, so nothing here
# can drift from careers.html.
# ---------------------------------------------------------------------------
import io, os, re, sys

OUT  = "/srv/apps/leaguelaw/wp-content/prototype/careers-options.html"
CGEN = "/srv/apps/leaguelaw/prototyping/gen_careers.py"

_src = io.open(CGEN, encoding="utf-8").read()
NS = {"__name__": "gen_careers", "__file__": CGEN}
exec(compile(_src, CGEN, "exec"), NS)
POSTS, slug = NS["POSTS"], NS["_slug"]
assert len(POSTS) == 3

# A PROPOSED grouping, derived from the titles and nothing else. The firm has
# not published one. It is badged on the sheet rather than presented as data —
# the same rule the injury-claims grouping is under.
GROUP_OF = {"Intermediate to Senior Litigator": "Lawyers",
            "Intermediate Conveyancing Assistant – League and Williams Lawyers": "Staff",
            "Articling Student – League and Williams Lawyers": "Students"}
assert set(GROUP_OF) == {p["title"] for p in POSTS}

# Measured in headless Chrome at 1440. Re-measure if the CSS changes.
HEIGHTS = {"0a": 29,  "0b": 89,
           "1a": 173, "1b": 566,
           "2a": 168, "2b": 501,
           "3a": 303, "3b": 628,
           "4a": 395, "4b": 1183,
           "5a": 298, "5b": 587}


def short(t):
    return t.split("–")[0].strip()


def rows(n):
    """n entries: the real three, repeated. A stress test, never invented copy."""
    out = []
    for i in range(n):
        p = POSTS[i % 3]
        out.append({"title": short(p["title"]),
                    "loc": p["location"] or "",
                    "email": p["email"],
                    "group": GROUP_OF[p["title"]],
                    "id": "%s-%d" % (slug(p["title"]), i)})
    return out


CSS = r"""
:root{--orange:#EF6D3E;--teal:#46ADC8;--brown:#423C37;--line:#E4E1DC;--paper:#F9F9F9;
      --muted:#6B6B6B;--deepteal:#2F7186;--deepor:#A94A29;--body:#3f3f3f}
*{box-sizing:border-box}
body{margin:0;background:#EDEAE4;font-family:"Open Sans",Arial,sans-serif;color:var(--brown)}
a{text-decoration:none}
h1,h2,h3{margin:0}
.page-head{max-width:1360px;margin:0 auto;padding:44px 40px 10px}
.page-head h1{font-family:Raleway,sans-serif;font-weight:300;font-size:34px;letter-spacing:.06em;
              text-transform:uppercase;margin-bottom:14px}
.page-head p,.box{max-width:1010px}
.page-head p{font-size:15px;line-height:26px;color:#4A443E;margin:0 0 16px}
.box{background:#fff;border-left:4px solid var(--teal);padding:16px 20px;font-size:14px;
     line-height:24px;color:#4A443E;margin:0 0 16px}
.box.warn{border-left-color:var(--orange)}
.box code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;background:#F3F0EB;padding:1px 5px}
table{border-collapse:collapse;width:100%;margin:12px 0 4px;font-size:13.5px}
th,td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--line)}
th{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.14em;
   text-transform:uppercase;color:var(--muted)}
.frame{max-width:1360px;margin:0 auto 10px;padding:0 40px}
.oh{display:flex;align-items:baseline;gap:14px;padding:30px 0 6px}
.oh .tag{font-family:Raleway,sans-serif;font-weight:800;font-size:22px;letter-spacing:.06em}
.oh .nm{font-family:Raleway,sans-serif;font-weight:600;font-size:14px;letter-spacing:.1em;
        text-transform:uppercase;color:var(--deepteal)}
.oh .h{margin-left:auto;font-size:12.5px;color:var(--muted)}
.on{font-size:13.5px;line-height:23px;color:#4A443E;margin:0 0 12px;max-width:1010px}
.on b{color:var(--brown)}
.stage{background:#fff}
.stage > .s{width:1280px;margin:0 auto;padding:38px 0}
.stage .cap{width:1280px;margin:0 auto;font-family:Raleway,sans-serif;font-weight:700;font-size:11px;
            letter-spacing:.16em;text-transform:uppercase;color:var(--muted);padding-top:26px}
.stage .cap.stress{color:var(--deepor)}
.wrapn{max-width:900px;margin:0 auto}

/* ---------- tokens shared by every option --------------------------------
   Colour is settled and identical in all five, so the comparison is about
   what happens when the list grows. On white: role names brown 10.87:1,
   meta and labels #6B6B6B 5.33:1, links #2F7186 5.50:1, hover #A94A29
   5.68:1. Brand orange carries no word — it is a rule and a marker only. */
.rn{font-family:Raleway,sans-serif;font-weight:300;font-size:20px;letter-spacing:.04em;
    text-transform:uppercase;color:var(--brown);line-height:1.25;display:block}
.rm{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.16em;
    text-transform:uppercase;color:var(--muted);display:block}
.go{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.14em;
    text-transform:uppercase;color:var(--deepteal);white-space:nowrap}
a:hover .rn,a:hover .go{color:var(--deepor)}

/* ---------- what is on the page today, for comparison ------------------- */
.p0 .roles{display:flex;flex-wrap:wrap;gap:10px 28px;padding:0 0 8px;
           border-bottom:1px solid var(--line)}
.p0 .roles .lbl{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;line-height:20px;
                letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.p0 .roles a{font-size:15px;line-height:20px;color:var(--deepteal);font-weight:600}

/* ================= P1 · cards ============================================
   A card per role: three across, wrapping. The count stops mattering —
   nine is three rows, twelve is four — and every card is the same size
   whatever its title's length, which is the thing the flex row cannot do. */
.p1 .g{display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.p1 .c{display:block;background:var(--paper);border:1px solid var(--line);padding:22px 22px 20px}
.p1 .c .rn{font-size:18px;margin-bottom:10px;min-height:45px}
.p1 .c .rm{margin-bottom:16px}
.p1 .c:hover{border-color:var(--orange)}

/* ================= P2 · ruled rows =======================================
   One row per role, the columns aligned down the page: name, place, door.
   Alignment is what a wrapping flex row cannot give you, and it is what
   makes a long list scannable — the eye runs down one column, not through
   a paragraph of links. */
.p2 .rowlist{border-top:1px solid var(--line)}
.p2 .r{display:flex;align-items:baseline;gap:24px;padding:16px 0;
       border-bottom:1px solid var(--line)}
.p2 .r .rn{flex:1;font-size:18px}
.p2 .r .rm{flex:0 0 210px}
.p2 .r:hover{background:var(--paper)}

/* ================= P3 · sticky rail ======================================
   The roles stay on screen while the postings scroll past them. The
   sidebar round already measured what this costs: a sticky block taller
   than the viewport has a bottom the reader can never reach, so the rail
   is capped and scrolls inside itself — which is precisely the case that
   turns up when the board is long. */
.p3 .cols{display:flex;gap:64px;align-items:flex-start}
.p3 .rail{flex:0 0 280px;position:sticky;top:24px;
          max-height:calc(100vh - 48px);overflow-y:auto;overscroll-behavior:contain;
          border-top:2px solid var(--brown)}
.p3 .rail a{display:block;padding:13px 0;border-bottom:1px solid var(--line)}
.p3 .rail .rn{font-size:15px}
.p3 .rail .rm{font-size:10px;margin-top:4px}
.p3 .body{flex:1;min-width:0}
.p3 .body .stub{border-top:1px solid var(--line);padding:20px 0}
.p3 .body .stub p{margin:10px 0 0;font-size:15px;line-height:25px;color:var(--muted)}

/* ================= P4 · no index at all ==================================
   The postings, and nothing above them. Every other option is an index
   plus the postings; this one asks whether the index is needed. At three
   it plainly is not. The frames below are the posting HEADERS, so what the
   option costs at nine is visible: nine headers is nine screenfuls with
   nothing to jump with. */
.p4 .h{border-top:1px solid var(--line);padding:26px 0 22px}
.p4 .h:last-child{border-bottom:1px solid var(--line)}
.p4 .h .rn{font-size:24px}
.p4 .h .rm{margin-top:7px}
.p4 .h .lede{margin:14px 0 0;font-size:15px;line-height:25px;color:var(--muted)}

/* ================= P5 · grouped ==========================================
   Roles under headings. It is the only option whose readability IMPROVES
   as the board grows: three roles do not need grouping, nine do, and at
   twelve it is the difference between a list and an inventory.
   The grouping is a PROPOSAL derived from the titles — the firm has not
   published one — which is the same standing on which the injury claims
   are grouped. */
.p5 .grp{margin-bottom:26px}
.p5 .gl{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.16em;
        text-transform:uppercase;color:var(--muted);padding-bottom:8px;
        border-bottom:2px solid var(--orange);margin-bottom:2px}
.p5 .r{display:flex;align-items:baseline;gap:24px;padding:13px 0;
       border-bottom:1px solid var(--line)}
.p5 .r .rn{flex:1;font-size:17px}
.p5 .r .rm{flex:0 0 210px}
.p5 .r:hover .rn{color:var(--deepor)}
"""


def P0(n):
    r = rows(n)
    return ('<div class="s p0"><div class="wrapn"><nav class="roles">'
            '<span class="lbl">Open roles</span>%s</nav></div></div>'
            % "".join('<a href="#%s">%s</a>' % (x["id"], x["title"]) for x in r))


def P1(n):
    return ('<div class="s p1"><div class="wrapn"><div class="g">%s</div></div></div>'
            % "".join('<a class="c" href="#%s"><span class="rn">%s</span>'
                      '<span class="rm">%s</span><span class="go">Read the posting &rarr;</span></a>'
                      % (x["id"], x["title"], x["loc"] or "&nbsp;") for x in rows(n)))


def P2(n):
    return ('<div class="s p2"><div class="wrapn"><div class="rowlist">%s</div></div></div>'
            % "".join('<a class="r" href="#%s"><span class="rn">%s</span>'
                      '<span class="rm">%s</span><span class="go">Read &rarr;</span></a>'
                      % (x["id"], x["title"], x["loc"]) for x in rows(n)))


def P3(n):
    r = rows(n)
    rail = "".join('<a href="#%s"><span class="rn">%s</span><span class="rm">%s</span></a>'
                   % (x["id"], x["title"], x["loc"]) for x in r)
    stubs = "".join('<div class="stub"><span class="rn">%s</span>'
                    '<p>&hellip; the full posting runs here.</p></div>' % x["title"]
                    for x in r[:3])
    return ('<div class="s p3"><div class="cols"><div class="rail">%s</div>'
            '<div class="body">%s</div></div></div>' % (rail, stubs))


def P4(n):
    return ('<div class="s p4"><div class="wrapn">%s</div></div>'
            % "".join('<div class="h"><span class="rn">%s</span><span class="rm">%s</span>'
                      '<p class="lede">&hellip; the full posting runs here.</p></div>'
                      % (x["title"], x["loc"]) for x in rows(n)))


def P5(n):
    r = rows(n)
    out = []
    for g in ("Lawyers", "Staff", "Students"):
        items = [x for x in r if x["group"] == g]
        if not items:
            continue
        out.append('<div class="grp"><div class="gl">%s</div>%s</div>'
                   % (g, "".join('<a class="r" href="#%s"><span class="rn">%s</span>'
                                 '<span class="rm">%s</span><span class="go">Read &rarr;</span></a>'
                                 % (x["id"], x["title"], x["loc"]) for x in items)))
    return '<div class="s p5"><div class="wrapn">%s</div></div>' % "".join(out)


OPTIONS = [
    ("P0", "what is on the page today", P0,
     "<code>.roles</code>: one flex row of full job titles. At three it sits on one line and looks "
     "tidy, which is the trap &mdash; <b>look at the nine</b>. There is no alignment to run the eye "
     "down, the titles are different lengths so nothing lines up, and a wrapped row of links reads "
     "as a paragraph rather than a list. Adi's objection, drawn."),
    ("P1", "cards", P1,
     "A card per role, three across, wrapping. <b>The count stops mattering</b> &mdash; nine is "
     "three rows, twelve is four &mdash; and every card is the same size whatever the length of its "
     "title, which is the one thing a flex row cannot do. Cost: a card is a big object for a line "
     "of text, so three cards take more room than three rows do, and the panel makes each role look "
     "like a product."),
    ("P2", "ruled rows", P2,
     "One row per role with the columns aligned down the page: name, place, door. <b>Alignment is "
     "exactly what the wrapping row cannot give you</b>, and it is what makes a long list scannable "
     "&mdash; the eye runs down one column instead of through a paragraph of links. The cheapest of "
     "the five in vertical space per role. Cost: it is plain, and at three roles it can read as a "
     "list of files rather than an invitation."),
    ("P3", "sticky rail", P3,
     "The roles stay on screen while the postings scroll past them, so a reader deep in the third "
     "posting can still see the first two exist. <b>The sidebar round already measured what this "
     "costs</b>: a sticky block taller than the viewport has a bottom the reader can never reach, "
     "so the rail is capped at <code>calc(100vh - 48px)</code> and scrolls inside itself &mdash; "
     "which is precisely the case a long board produces. Cost: it spends 280px of width on "
     "navigation for a page most visitors read once."),
    ("P4", "no index at all", P4,
     "The postings and nothing above them. Every other option here is an index plus the postings; "
     "this one asks whether the index earns its place. <b>At three it plainly does not</b> &mdash; "
     "the three headers are visible in one and a half screens. The frames below are the posting "
     "HEADERS, so the cost at nine is visible too: nine of them is nine screenfuls with nothing to "
     "jump with, and the page has no way to tell a visitor what it holds."),
    ("P5", "grouped", P5,
     "Roles under headings. <b>The only option whose readability improves as the board grows</b>: "
     "three roles do not need grouping, nine do, and at twelve it is the difference between a list "
     "and an inventory. It also answers a question none of the others do &mdash; a conveyancing "
     "assistant and an articling student are not looking for the same page. <b>The grouping is a "
     "proposal derived from the titles; the firm has not published one</b>, which is the same "
     "standing the injury claims are grouped on. If it is wrong, this option is wrong in a way "
     "P1&ndash;P4 are not."),
]

HEAD = """<!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; open positions, layout options</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;500;600;700;800&amp;family=Open+Sans:wght@400;600;700&amp;display=swap" rel="stylesheet">
<style>__CSS__</style>
</head>
<body>

<div class="page-head">
  <h1>Open positions &mdash; layout options</h1>
  <p>Five ways to list the open positions on <code>careers.html</code>, plus the row that is on the
  page today so the comparison has a baseline.</p>

  <div class="box warn"><b>The objection names the constraint, so the sheet is built around it.</b>
  Adi: <i>"jika open position ada banyak akan berantakan."</i> <code>.roles</code> is one flex row of
  full job titles &mdash; at three it fits on one line and looks tidy, which is exactly the trap.
  <b>A component that only works at the count it was drawn for is not a component.</b>
  <br><br>So <b>every option below is drawn twice</b>: with the real three, and with the same three
  repeated to nine. <b>The nine are a stress test, not invented postings</b> &mdash; every stressed
  frame says so &mdash; because what is being compared is not how an option looks with three roles.
  It is what it does when the firm is hiring.</div>

  <table>
    <tr><th>option</th><th>at three</th><th>at nine</th><th>what it costs</th></tr>
    <tr><td>P0 &mdash; today's flex row</td><td>__H0a__</td><td>__H0b__</td><td>no alignment; wraps into a paragraph</td></tr>
    <tr><td>P1 &mdash; cards</td><td>__H1a__</td><td>__H1b__</td><td>a big object for a line of text</td></tr>
    <tr><td>P2 &mdash; ruled rows</td><td>__H2a__</td><td>__H2b__</td><td>plain</td></tr>
    <tr><td>P3 &mdash; sticky rail</td><td>__H3a__</td><td>__H3b__</td><td>280px of width, always</td></tr>
    <tr><td>P4 &mdash; no index</td><td>__H4a__</td><td>__H4b__</td><td>nothing to jump with</td></tr>
    <tr><td>P5 &mdash; grouped</td><td>__H5a__</td><td>__H5b__</td><td>needs a taxonomy the firm has not given</td></tr>
  </table>
  Measured in headless Chrome at 1440. P4's figures are its posting HEADERS, not whole postings.

  <div class="box"><b>Colour is not one of the variables</b> &mdash; identical in all five, so the
  comparison is about what happens when the list grows. Role names brown <b>10.87:1</b>, meta and
  labels <code>#6B6B6B</code> <b>5.33:1</b>, doors <code>#2F7186</code> <b>5.50:1</b>, hover
  <code>#A94A29</code> <b>5.68:1</b>. Brand orange is 3.03:1 and carries no word: it is a rule and a
  border, nothing else.</div>

  <div class="box"><b>One posting of three states no location</b>, so the meta column is empty for
  it in every option. That is the live page's inconsistency, not the layout's, and no option fills
  it in.</div>
</div>
"""


def build():
    body = []
    for tag, name, fn, note in OPTIONS:
        k = tag[1]
        body.append(
            '<div class="frame">\n'
            '  <div class="oh"><span class="tag">%s</span><span class="nm">%s</span>'
            '<span class="h">three __H%sa__ &middot; nine __H%sb__</span></div>\n'
            '  <p class="on">%s</p>\n'
            '  <div class="stage">\n'
            '    <div class="cap">the real three</div>%s\n'
            '    <div class="cap stress">stressed to nine &mdash; the same three roles repeated, '
            'a stand-in for a longer board, not real postings</div>%s\n'
            '  </div>\n</div>\n' % (tag, name, k, k, note, fn(3), fn(9)))
    html = HEAD.replace("__CSS__", CSS) + "\n".join(body) + "\n</body>\n</html>\n"
    assert html.count('class="stage"') == 6
    for tag, _, _, _ in OPTIONS:
        k = tag[1]
        for suf in "ab":
            key = k + suf
            assert "__H%s__" % key in html, key
            html = html.replace("__H%s__" % key,
                                "%dpx" % HEIGHTS.get(key, 0) if HEIGHTS.get(key) else "&mdash;")
    assert not re.search(r"__H\w+__", html)
    assert html.count("stand-in for a longer board") == 6, "a stressed frame lost its badge"
    return html


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