#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# service-index-options.html — options for the service index, drawn for ONE
# service.
#
# Adi 2026-08-31: "bisakah kamu buatkan beberapa opsi untuk menampilkan service
# index. mungkin service index ini ditampilkan per service, misal untuk page
# sekarang kita ambil contoh untuk service index injuries saja terlebih
# dahulu."
#
# Injuries is the right service to draw them on, and not only because Adi
# named it: it is the ONLY one that stresses a layout. Thirteen pages in three
# groups against Estates' seven, Real Estate's two and Business and Marine's
# nothing. An option that reads well on Marine tells you nothing; one that
# reads well on Injuries has been tested against the worst case the site has.
# Each option's caption says what it does to the four thin ones.
#
# The data is EXECUTED out of gen_service_index.py — the same live tree, the
# same home_v1 grouping, the same real URLs — so no option can drift from the
# page it is proposing to replace.
# ---------------------------------------------------------------------------
import io, os, re, sys

OUT  = "/srv/apps/leaguelaw/wp-content/prototype/service-index-options.html"
SGEN = "/srv/apps/leaguelaw/prototyping/gen_service_index.py"

_src = io.open(SGEN, encoding="utf-8").read()
NS = {"__name__": "gen_service_index", "__file__": SGEN}
exec(compile(_src, SGEN, "exec"), NS)
INJ = next(a for a in NS["AREAS"] if a["name"] == "Injuries")
assert INJ["count"] == 13 and len(INJ["groups"]) == 3, INJ["count"]
GROUPS = INJ["groups"]                       # [(label, label_href|None, [(name, href)])]
COUNTS = [len(i) for _, _, i in GROUPS]
assert COUNTS == [6, 4, 3], COUNTS
ICON, COPY, HREF = INJ["icon"], INJ["copy"], INJ["href"]

# Measured in headless Chrome at a 1440px viewport, not estimated. Re-measure
# if the CSS below changes: the sheet quotes these numbers in a table and a
# stale one would be worse than none.
HEIGHTS = {"1": 335, "2": 620, "3": 497, "4": 705, "5": 419, "6": 962}

# ---------------------------------------------------------------------------
CSS = r"""
:root{
  --orange:#EF6D3E; --teal:#46ADC8; --brown:#423C37; --line:#E4E1DC;
  --paper:#F9F9F9; --muted:#6B6B6B; --deepteal:#2F7186; --deepor:#A94A29;
}
*{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}

/* ---------- sheet chrome ---------- */
/* the stages are 1280 wide because that is the real container; the sheet has
   to be wider than its widest stage or the last column is clipped and the
   option is judged on a crop. 1360 = 1280 + 2x40. */
.page-head{max-width:1360px;margin:0 auto;padding:44px 40px 10px}
.page-head p,.box{max-width:1010px}
.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{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;
         color:var(--brown)}
.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:980px}
.on b{color:var(--brown)}
.stage{background:#fff;overflow-x:auto}
.stage > .s{width:1280px;margin:0 auto}

/* ---------- tokens every option shares -----------------------------------
   Colour is not an option here — it is decided, and it is the same in all six
   so the comparison is about structure. Measured on white:
     brown  #423C37 10.87:1   page names, service name
     muted  #6B6B6B  5.33:1   group labels, counts, body copy
     teal-d #2F7186  5.50:1   the door and any linked label
     hover  #A94A29  5.68:1
   Brand orange is 3.03:1 and carries no word anywhere on this sheet: it is a
   rule, a plate edge or a marker. Orange marks, brown reads. */
.s{padding:44px 0;font-size:14px}
.plate{width:64px;height:73.9px;background:var(--brown);
       clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
       display:flex;align-items:center;justify-content:center}
/* 64 / 0.8660254 = 73.9. A hexagon off ratio is a bug, not a style.
   46px icon: the files are 93x107 8-bit PNGs with the brand colour baked in,
   so 96px is already an upscale and they cannot be recoloured for the dark
   plate. Orange on brown measures 3.58:1 and teal 4.18:1 — both clear the 3:1
   non-text floor with the colour they already have. */
.plate img{width:46px;height:auto;display:block}
.snm{font-family:Raleway,sans-serif;font-weight:300;font-size:26px;letter-spacing:.06em;
     text-transform:uppercase;color:var(--brown);line-height:1.2}
.snm a{color:inherit}.snm a:hover{color:var(--deepor)}
.scopy{font-size:15px;line-height:25px;color:var(--muted)}
.door{display:inline-block;font-family:Raleway,sans-serif;font-weight:700;font-size:12px;
      letter-spacing:.14em;text-transform:uppercase;color:var(--deepteal)}
.door:hover{color:var(--deepor)}
.glab{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.16em;
      text-transform:uppercase;color:var(--muted);line-height:18px}
.glab .n{color:#B9B2A8;margin-left:6px}

/* ================= S1 · rail and columns ================= */
.s1 .area{display:flex;gap:64px}
.s1 .ahead{width:384px;flex:none}
.s1 .plate{margin-bottom:18px}
.s1 .snm{margin-bottom:12px}
.s1 .scopy{margin:0 0 18px}
.s1 .alist{flex:1;display:flex;gap:32px}
.s1 .agrp{flex:0 0 256px}
.s1 .glab{margin-bottom:6px}
.s1 .agrp > a{display:block;font-size:14px;line-height:20px;color:var(--brown);
              padding:8px 0;border-bottom:1px solid var(--line)}
.s1 .agrp > a:hover{color:var(--deepor);border-bottom-color:var(--orange)}

/* ================= S2 · head across, three columns ================= */
.s2 .top{text-align:center;max-width:762px;margin:0 auto 34px}
.s2 .top .plate{margin:0 auto 16px}
.s2 .snm{margin-bottom:10px}
.s2 .scopy{margin:0 0 14px}
.s2 .cols{display:flex;gap:32px}
.s2 .col{flex:1 1 0}
.s2 .glab{padding-bottom:8px;border-bottom:2px solid var(--orange);margin-bottom:4px}
.s2 .col a{display:block;font-size:15px;line-height:21px;color:var(--brown);
           padding:10px 0;border-bottom:1px solid var(--line)}
.s2 .col a:hover{color:var(--deepor)}

/* ================= S3 · group panels ================= */
.s3 .top{display:flex;align-items:flex-end;gap:24px;margin-bottom:28px}
.s3 .top .tx{flex:1}
.s3 .snm{margin-bottom:8px}
.s3 .scopy{margin:0;max-width:640px}
.s3 .cards{display:flex;gap:32px}
.s3 .card{flex:1 1 0;background:var(--paper);border:1px solid var(--line)}
.s3 .ch{background:var(--brown);color:#fff;padding:12px 18px;
        font-family:Raleway,sans-serif;font-weight:700;font-size:11px;letter-spacing:.16em;
        text-transform:uppercase;display:flex;justify-content:space-between}
.s3 .ch .n{color:#C9C4BC}
.s3 .cb{padding:6px 18px 14px}
.s3 .cb a{display:block;font-size:14px;line-height:20px;color:var(--brown);padding:9px 0;
          border-bottom:1px solid var(--line)}
.s3 .cb a:last-child{border-bottom:0}
.s3 .cb a:hover{color:var(--deepor)}

/* ================= S4 · numbered index, typography only ==================
   No icon and no photograph, so this is the one option that neither the
   93px icon ceiling nor the missing per-claim imagery can touch. */
.s4 .t4{border-bottom:2px solid var(--brown);padding-bottom:18px;margin-bottom:8px}
.s4 .snm{font-size:34px;margin-bottom:8px}
.s4 .scopy{margin:0;max-width:700px}
.s4 .g4{padding:22px 0 6px;border-bottom:1px solid var(--line)}
.s4 .g4:last-child{border-bottom:0}
.s4 .glab{margin-bottom:10px}
.s4 .ol{column-count:2;column-gap:64px}
.s4 .ol a{display:flex;gap:14px;align-items:baseline;font-size:16px;line-height:24px;
          color:var(--brown);padding:7px 0;break-inside:avoid}
.s4 .ol a:hover{color:var(--deepor)}
.s4 .ol .num{font-family:Raleway,sans-serif;font-weight:700;font-size:12px;color:var(--muted);
             letter-spacing:.06em;min-width:22px}

/* ================= S5 · dense strip ================= */
.s5 .h5{display:flex;align-items:center;gap:22px;margin-bottom:26px}
.s5 .h5 .tx{flex:1}
.s5 .snm{font-size:22px;margin-bottom:4px}
.s5 .scopy{margin:0;font-size:14px;line-height:22px}
.s5 .band{display:flex;gap:24px;align-items:baseline;padding:14px 0;
          border-top:1px solid var(--line)}
.s5 .band .glab{flex:0 0 150px}
.s5 .chips{display:flex;flex-wrap:wrap;gap:8px}
.s5 .chips a{display:inline-block;font-size:13.5px;line-height:18px;color:var(--brown);
             background:var(--paper);border:1px solid var(--line);padding:7px 14px}
.s5 .chips a:hover{color:var(--deepor);border-color:var(--orange)}

/* ================= S6 · full-width ruled rows ================= */
.s6 .h6{display:flex;align-items:flex-end;gap:24px;padding-bottom:22px;margin-bottom:6px;
        border-bottom:1px solid var(--line)}
.s6 .h6 .tx{flex:1}
.s6 .snm{margin-bottom:8px}
.s6 .scopy{margin:0;max-width:640px}
.s6 .grow{display:flex;gap:64px;padding:20px 0;border-bottom:1px solid var(--line)}
.s6 .grow:last-child{border-bottom:0}
.s6 .grow .glab{flex:0 0 256px;padding-top:12px}
.s6 .rl{flex:1}
.s6 .rl a{display:flex;align-items:center;justify-content:space-between;
          font-size:17px;line-height:26px;color:var(--brown);padding:11px 0;
          border-bottom:1px solid var(--line)}
.s6 .rl a:last-child{border-bottom:0}
.s6 .rl a i{color:var(--orange);font-size:13px;opacity:0;transition:opacity .14s,transform .14s;
            transform:translateX(-6px)}
.s6 .rl a:hover{color:var(--deepor)}
.s6 .rl a:hover i{opacity:1;transform:translateX(0)}
"""


# ---------------------------------------------------------------------------
def head_bits():
    return ('<span class="plate"><img src="%s" alt=""></span>' % ICON,
            '<h2 class="snm"><a href="%s">Injuries</a></h2>' % HREF,
            '<p class="scopy">%s</p>' % COPY,
            '<a class="door" href="%s">All injuries &rarr;</a>' % HREF)


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


def S1():
    p, n, c, d = head_bits()
    grps = "".join(
        '<div class="agrp">%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 ('<div class="s s1"><div class="area">'
            '<div class="ahead">%s%s%s%s</div>'
            '<div class="alist">%s</div></div></div>' % (p, n, c, d, grps))


def S2():
    p, n, c, d = head_bits()
    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 ('<div class="s s2"><div class="top">%s%s%s%s</div>'
            '<div class="cols">%s</div></div>' % (p, n, c, d, cols))


def S3():
    p, n, c, d = head_bits()
    cards = "".join(
        '<div class="card"><div class="ch"><span>%s</span><span class="n">%d</span></div>'
        '<div class="cb">%s</div></div>'
        % (gh, len(items), "".join('<a href="%s">%s</a>' % (h, t) for t, h in items))
        for gh, gr, items in GROUPS)
    return ('<div class="s s3"><div class="top">%s<div class="tx">%s%s</div>%s</div>'
            '<div class="cards">%s</div></div>' % (p, n, c, d, cards))


def S4():
    _, n, c, d = head_bits()
    k = [0]

    def row(t, h):
        k[0] += 1
        return '<a href="%s"><span class="num">%02d</span><span>%s</span></a>' % (h, k[0], t)
    gs = "".join(
        '<div class="g4">%s<div class="ol">%s</div></div>'
        % (glab(gh, gr, len(items)), "".join(row(t, h) for t, h in items))
        for gh, gr, items in GROUPS)
    return ('<div class="s s4"><div class="t4">%s%s</div>%s'
            '<div style="padding-top:20px">%s</div></div>' % (n, c, gs, d))


def S5():
    p, n, c, d = head_bits()
    bands = "".join(
        '<div class="band">%s<div class="chips">%s</div></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 ('<div class="s s5"><div class="h5">%s<div class="tx">%s%s</div>%s</div>%s</div>'
            % (p, n, c, d, bands))


def S6():
    p, n, c, d = head_bits()
    rows = "".join(
        '<div class="grow">%s<div class="rl">%s</div></div>'
        % (glab(gh, gr, len(items)),
           "".join('<a href="%s"><span>%s</span><i class="fa fa-arrow-right"></i></a>' % (h, t)
                   for t, h in items))
        for gh, gr, items in GROUPS)
    return ('<div class="s s6"><div class="h6">%s<div class="tx">%s%s</div>%s</div>%s</div>'
            % (p, n, c, d, rows))


OPTIONS = [
    ("S1", "rail and columns", S1,
     "What the index page does today, cut down to one service. The service sits in the 384px "
     "rail and its pages in the 832 beside it &mdash; the inner pages' own lattice, so a "
     "reader who has seen a service page recognises the shape. <b>It is also the only one of "
     "the six that survives an area with nothing under it</b>: the rail carries the name, the "
     "copy and the door on its own, so Business and Marine still look like entries rather than "
     "gaps. Cost: the list can never be wider than 832, so a fourth group has nowhere to go."),
    ("S2", "head across, three columns", S2,
     "The service introduces itself across the full width, then gets out of the way; the "
     "thirteen pages take the whole 1280 in three 405px columns. <b>Longest line length of the "
     "six</b>, so every claim sits on one line where S1 wraps four of them. Cost: the centred "
     "head is a section opening, which reads oddly if five of these are stacked on one page "
     "&mdash; it is a layout for a service that has a page to itself."),
    ("S3", "group panels", S3,
     "Each group becomes a panel with a solid brown header bar. <b>The grouping is the loudest "
     "thing on the page</b>, which is the point if the groups are real &mdash; and the risk if "
     "they are not (see the box above). White text on brown measures 10.87:1. Cost: three "
     "panels of 6, 4 and 3 rows are visibly ragged at the bottom, and there is no honest way "
     "to even them up."),
    ("S4", "numbered index, typography only", S4,
     "No icon, no photograph, no panel &mdash; a numbered list under ruled group headings. "
     "<b>The only option here that neither of the rebuild's two image constraints can touch</b>: "
     "the icons are 93px rasters with the colour baked in, and there is no photograph for any "
     "of the thirteen claims. It is also the only one that says <b>how many</b> pages there are "
     "by counting them in front of the reader. Cost: it is the least branded thing on the site."),
    ("S5", "dense strip", S5,
     "The thirteen as chips, three bands, one per group. <b>The most compact LIST</b> &mdash; the "
     "thirteen claims occupy 3 bands where S6 needs 13 rows &mdash; though S1 still ends up shorter "
     "overall, because S5 pays for a head above the list and S1 does not. Cost: "
     "a chip is sized by its own text, so the column edges disappear and the list stops being "
     "scannable top-to-bottom; and the longest names (Motorcycle Accident Claims) make chips "
     "wide enough to look like buttons that do something bigger than navigate."),
    ("S6", "full-width ruled rows", S6,
     "One page per row across the full width, the group label in a 256px column beside its "
     "rows. <b>The most scannable and the most emphatic</b>: 17px rows with an arrow that "
     "arrives on hover. Cost: it is the tallest, and the row is mostly empty space &mdash; "
     "a 17px name in a 1024px row. That space is where a one-line description would go, and "
     "there is no such copy for any of the thirteen today."),
]

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; service index options (Injuries)</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">
<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>

<div class="page-head">
  <h1>Service index &mdash; options</h1>
  <p>Six ways to draw <b>one service and its pages</b>, all six carrying the real thirteen injury
  claims with their real URLs, lifted at build time from the same two sources the index page uses:
  the live site's menu decides what exists, home_v1's mega menu decides how it groups. Nothing here
  can drift from <code>service-index.html</code>.</p>

  <div class="box warn"><b>Injuries is not an example, it is the worst case.</b> Thirteen pages in
  three groups, against Estates' seven, Real Estate's two, and Business and Marine's <b>nothing at
  all</b>. An option that reads well on Marine has been tested against nothing. Every caption below
  says what its option does to the four thin services &mdash; because whatever wins here has to
  survive an area with an empty list, and only one of the six does that without help.</p>
  <table>
    <tr><th>option</th><th>height at 1280</th><th>tallest element</th><th>an area with no pages</th></tr>
    <tr><td>S1 &mdash; rail and columns</td><td>__H1__</td><td>the rail, not the list</td><td><b>holds its shape</b></td></tr>
    <tr><td>S5 &mdash; dense strip</td><td>__H5__</td><td>the head</td><td>collapses to the head alone</td></tr>
    <tr><td>S3 &mdash; group panels</td><td>__H3__</td><td>the 6-row panel</td><td>nothing left to draw</td></tr>
    <tr><td>S2 &mdash; head across, columns</td><td>__H2__</td><td>the 6-row column</td><td>head with a hole under it</td></tr>
    <tr><td>S4 &mdash; numbered index</td><td>__H4__</td><td>the 6-row group</td><td>a heading and no list</td></tr>
    <tr><td>S6 &mdash; ruled rows</td><td>__H6__</td><td>the 6-row group</td><td>a label column beside nothing</td></tr>
  </table>
  Heights measured in headless Chrome at a 1440px viewport, not estimated.
  <b>S1 is the shortest and it is not close</b> &mdash; 335 against S5's 419 &mdash; and the reason is
  structural rather than stylistic: <b>S1 is the only option that puts the service's head BESIDE the
  list instead of above it</b>, so the plate, the name, the paragraph and the door cost nothing
  vertically. Every other option pays 150&ndash;220px before its first claim. Stack five services on
  one page and that is the whole difference between a page you scroll once and a page you scroll
  five times.</div>

  <div class="box"><b>The grouping is a proposal, not the firm's.</b> <i>Road Accidents</i>,
  <i>On Premises</i> and <i>Other Claims</i> come from home_v1's mega menu; the live site's own menu
  lists the thirteen flat, with no groups at all. Three of the six options (S3, S6 and to a lesser
  extent S4) make the grouping structural &mdash; if it is wrong, they are wrong in a way S1 and S5
  are not. Someone at the firm has to confirm it before those three can be picked.</div>

  <div class="box"><b>Two constraints bound all six, and only S4 escapes both.</b> The five practice
  icons are <b>93&times;107 8-bit PNGs with the brand colour baked into the file</b> &mdash; 96px is
  already an upscale and they cannot be recoloured, so nothing here draws one above 46px, and the
  dark plate was checked rather than assumed (orange <b>3.58:1</b>, teal <b>4.18:1</b> on
  <code>#423C37</code>, both over the 3:1 non-text floor). And <b>there is no photograph for any of
  the thirteen claims</b>, which is why no option on this sheet is photo-led: a tile-per-claim
  layout cannot be drawn from what exists.</div>

  <div class="box"><b>Colour is not one of the variables.</b> It is settled and identical in all six,
  so the comparison is about structure. On white: page names brown <b>10.87:1</b>, group labels and
  copy <code>#6B6B6B</code> <b>5.33:1</b>, the door <code>#2F7186</code> <b>5.50:1</b>, hover
  <code>#A94A29</code> <b>5.68:1</b>. Brand orange is <b>3.03:1</b> and carries no word anywhere on
  this sheet &mdash; it is a rule, a plate edge or a hover marker. Orange marks, brown reads.</div>
</div>
"""


def build():
    body = []
    for tag, name, fn, note in OPTIONS:
        body.append(
            '<div class="frame">\n'
            '  <div class="oh"><span class="tag">%s</span><span class="nm">%s</span>'
            '<span class="h">height __H%s__</span></div>\n'
            '  <p class="on">%s</p>\n'
            '  <div class="stage">%s</div>\n</div>\n' % (tag, name, tag[1], note, fn()))
    html = HEAD.replace("__CSS__", CSS) + "\n".join(body) + "\n</body>\n</html>\n"
    assert html.count('class="stage"') == 6
    for t, _, _, _ in OPTIONS:
        assert "__H%s__" % t[1] in html, "no height slot for %s" % t
        html = html.replace("__H%s__" % t[1], "%dpx" % HEIGHTS[t[1]])
    assert not re.search(r"__H\d__", html), "a height slot went unfilled"
    # every link on the sheet is a real live URL
    links = re.findall(r'<a [^>]*?href="([^"]*)"', html)
    assert links and all(l.startswith("/our-services/") for l in links), \
        [l for l in links if not l.startswith("/our-services/")]
    assert len(links) == 6 * (13 + 2), "expected 90 links, got %d" % len(links)
    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)))
