#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# sidebar-nav.html — option sheet for the inner service sidebar once it is
# NAVIGATION ONLY and STICKY (Adi, 2026-08-30: "bisakah sidebar hanya bagian
# navigation seperti Injury claims nya saja.. namun dibuat sticky scroll.
# kemudian buat beberapa opsi tampilan untuk sidebar navigation nya tersebut").
#
# The form is gone from every option — that decision is made. What varies is
# how thirteen sibling claims are drawn in a 384px column that has to STAY ON
# SCREEN, which turns a styling question into a measuring one: a sticky block
# taller than the viewport has a bottom the reader can never reach.
#
# Every stage is the real geometry (832 + 64 + 384 = 1280) with the real
# article beside it, long enough that the stickiness is visible by scrolling
# this sheet rather than being asserted in a caption.
# ---------------------------------------------------------------------------
import io, json, os, sys

OUT   = "/srv/apps/leaguelaw/wp-content/prototype/sidebar-nav.html"
R1GEN = "/srv/apps/leaguelaw/prototyping/gen_heroinner.py"

CLAIMS = [
    ("Car Accident Claims",        True),  ("Motorcycle Accident Claims", False),
    ("Cyclist Accident Claims",    False), ("Passenger Injury Claims",    False),
    ("Pedestrian Injury Claims",   False), ("Hit and Run Injury Claims",  False),
    ("Slip and Fall",              False), ("Premises Liability Claims",  False),
    ("School Accident Claims",     False), ("Product Liability",          False),
    ("Dog Attack",                 False), ("Boat Accidents",             False),
    ("Assault &amp; Battery",      False),
]

# A PROPOSED grouping, not the firm's. Derived from the names alone; the sheet
# says so, because a taxonomy is the firm's to confirm, not the prototype's.
GROUPS = [
    ("Vehicle &amp; travel", ["Car Accident Claims", "Motorcycle Accident Claims",
                              "Cyclist Accident Claims", "Passenger Injury Claims",
                              "Pedestrian Injury Claims", "Hit and Run Injury Claims",
                              "Boat Accidents"]),
    ("Property &amp; premises", ["Slip and Fall", "Premises Liability Claims",
                                 "School Accident Claims", "Dog Attack"]),
    ("Other claims", ["Product Liability", "Assault &amp; Battery"]),
]

# the article's real copy, in full — the stage has to be long enough that the
# stickiness is something you watch by scrolling, not something a caption claims
PROSE = """<p>Being involved in a car accident can be scary and often those injured in motor vehicle accidents are unaware of how to ensure the best recovery possible.&nbsp; Those injured in car accidents in British Columbia, whether as a driver, passenger, pedestrian or cyclist are entitled to pursue compensation for their injuries.&nbsp; Car accident victims who are injured may claim for pain and suffering, out-of-pocket expenses, loss of housekeeping capacity, loss of income and diminished earning capacity, and for cost of future care.&nbsp; The kind of compensation available and the amounts will depend on the extent of the injuries, the impact they have had on the victim’s life, and whether or not the victim was partly at fault for their injuries.</p>
<p>Typically those involved in car&nbsp;accidents in British Columbia may make two types of claims – a Tort claim and a Part 7 claim.</p>
<p>A Tort claim is available for those who are injured and who can prove that another person was, at least partly, at fault or responsible for causing the accident.&nbsp; The person must also establish that they suffered damages, and that those damages were the result of the accident.&nbsp; It is important to note that in order to make a tort claim, that claim must also be filed within 2 years of the date of the accident (this is the limitation date).&nbsp; If a person cannot prove that their injuries were caused by the accident, then the injured person is limited in their tort claim to the damages that they can prove were the result of the accident. &nbsp;If a person cannot prove that another person is at least partly at fault, then their claims for their injuries will be limited to a Part 7 claim, also known as a no-fault benefits claim.</p>
<p>A Part 7 claim refers to the “no-fault” benefits that are payable to accident victims in BC, even if they are wholly at fault for their injuries.&nbsp; To receive these benefits, the person must have an insurance policy with ICBC; or, live in a household with someone who has an insurance policy with ICBC; or, was the occupant of a vehicle licensed in BC; or was a cyclist or pedestrian who has collided with a vehicle insured in BC.&nbsp; It is important to note that notice and limitation periods apply to Part 7 claims.&nbsp; Although the claimant does not need to prove that someone else was at fault for the accident, they must still prove that the injuries that they sustained were the result of the accident.</p>
<p>Under Part 7, a person is entitled to the following benefits:</p>
<ul><li>Up to $150,000 in medical and rehabilitation expenses</li><li>Up to $300 per week in wage loss benefits for those who were totally disabled from work within 20 days of the accident. Note these benefits are payable for a maximum of 104 weeks.</li><li>Homemaker benefits (limits apply).</li><li>Death benefits – limited amounts paid to surviving spouses and children.</li></ul>
<p>If you, or someone you know was injured in a car accident, speaking with an experienced injury lawyer and making informed decisions about their claim is critical.&nbsp; The lawyers at League and Williams provide free consultations to those injured in car accidents and have the experience needed to help injured victims recover to their fullest potential. &nbsp;We <a href="/contact-our-lawyers/">may be contacted</a> by phone 250-888-0002 or via email at&nbsp;<a href="mailto:info@leaguelaw.com">info@leaguelaw.com.</a></p>"""


def lift_chrome():
    src = io.open(R1GEN, encoding="utf-8").read()
    i = src.index('CSS = r"""') + len('CSS = r"""')
    j = src.index("  /* ---------- header stand-in", i)
    css = src[i:j]
    assert ".page-head h1" in css and ".opt-label .tag" in css, "sheet chrome did not survive the lift"
    return css


NAV_CSS = r"""
  /* ---------- the page, as context ---------- */
  /* =====================================================================
     TWO THINGS THAT SILENTLY KILL position:sticky — both were in this sheet.

     1. `.stage { overflow:hidden }`, inherited from the option-sheet chrome.
        Any ancestor with a non-visible overflow becomes the sticky element's
        scrolling box. That box never scrolls, so the element never sticks —
        and nothing anywhere reports an error. Overridden below.

     2. `align-items:flex-start` on the row. It shrinks the rail to its own
        content, so the sticky block exactly fills its containing block and
        has NO ROOM TO TRAVEL. It is stuck from the first pixel, which looks
        exactly like not being sticky at all. The row must stretch, so the
        rail is as tall as the article and the block has somewhere to go.

     Both were invisible in a screenshot. This is why the sheet is measured
     while scrolling rather than looked at.
     ===================================================================== */
  .stage { overflow:visible; }
  .pg { background:#fff; padding:40px 0 46px; }
  .pg .cols { display:flex; }
  .pg .col { width:832px; flex:none; }
  .pg .rail { width:384px; margin-left:64px; flex:none; }
  .pg .prose { font-size:16.5px; line-height:1.9; color:#4A453F; opacity:.40; }
  .pg .prose p { margin-bottom:22px; }
  .pg .prose ul { margin:0 0 22px 20px; }
  .pg .prose li { margin-bottom:8px; }
  .pg .h2 { font-family:Raleway,sans-serif; font-weight:400; font-size:22px; line-height:1.5;
            color:var(--brown); margin-bottom:16px; opacity:.40; }
  /* THE STICKY. top:24 is the gap from the top of the viewport; if the site
     ever gets a sticky header this number is that header's height + 24. */
  .stick { position:sticky; top:24px; }

  /* shared: the "up" row — the parent category. The live sidebar has this and
     none of the earlier options did: without it there is no way back to the
     Injury Claims index from an inner claim. */
  /* =====================================================================
     ORANGE MARKS, BROWN READS.
     Brand orange on white is 3.03:1 — it clears the 3:1 floor for a NON-TEXT
     mark and fails the 4.5:1 floor for 13px type. So in every option below the
     current claim is carried by its orange hexagon plus weight, and the words
     themselves are brown (10.86:1). Same rule the hero already follows.
     The 'up' link was #8D867E — 3.59:1, also under the floor — now #6B6B6B
     (5.33:1). Teal was worse still (2.60:1) and is gone from the type entirely.
     ===================================================================== */
  .up { display:flex; align-items:center; gap:10px; font-family:Raleway,sans-serif; font-weight:700;
        font-size:11.5px; line-height:16px; letter-spacing:.14em; text-transform:uppercase;
        color:#6B6B6B; padding-bottom:12px; border-bottom:1px solid var(--line); }
  /* grey, not orange: on this sheet orange means "you are here", and the
     parent link is the one row that is explicitly NOT here */
  .up i { width:9px; height:10.392px; background:#C6C0B8; flex:none; display:block;
          clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
          transition:background .14s ease; }
  .up:hover i { background:#8D867E; }
  .up:hover { color:var(--brown); }

  /* ---------- N1 · hairline rows ---------- */
  .n1 .rlist a { display:block; position:relative; font-size:13.5px; line-height:18px; font-weight:600;
                 color:#4A453F; padding:11px 2px 11px 19px; border-bottom:1px solid #EEECE7;
                 transition:color .14s ease; }
  .n1 .rlist a:before { content:""; position:absolute; left:0; top:50%; width:9px; height:10.392px;
        background:var(--orange); clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%) translateX(-7px) scale(0); transition:transform .14s ease; }
  .n1 .rlist a:hover { color:var(--brown); }
  .n1 .rlist a:hover:before, .n1 .rlist a.cur:before { transform:translateY(-50%) translateX(0) scale(1); }
  .n1 .rlist a.cur { color:var(--brown); font-weight:700; }

  /* ---------- N2 · compact ---------- */
  .n2 .rlist a { display:block; position:relative; font-size:12.5px; line-height:16px; font-weight:600;
                 color:#4A453F; padding:7px 2px 7px 17px; transition:color .14s ease; }
  .n2 .rlist a:before { content:""; position:absolute; left:0; top:50%; width:8px; height:9.238px;
        background:#D6D1C9; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n2 .rlist a:hover { color:var(--brown); } .n2 .rlist a:hover:before { background:var(--orange); }
  .n2 .rlist a.cur { color:var(--brown); font-weight:700; } .n2 .rlist a.cur:before { background:var(--orange); }
  .n2 .rlist { padding-top:6px; }

  /* ---------- N3 · grouped ---------- */
  .n3 .grp { margin-top:18px; }
  /* was teal — 2.60:1 at 10.5px, the worst number on the sheet */
  .n3 .gh { font-family:Raleway,sans-serif; font-weight:700; font-size:10.5px; line-height:14px;
            letter-spacing:.16em; text-transform:uppercase; color:#6B6B6B; margin-bottom:6px; }
  .n3 .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                 color:#4A453F; padding:7px 2px 7px 17px; transition:color .14s ease; }
  .n3 .rlist a:before { content:""; position:absolute; left:0; top:50%; width:8px; height:9.238px;
        background:#D6D1C9; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n3 .rlist a:hover { color:var(--brown); } .n3 .rlist a:hover:before { background:var(--orange); }
  .n3 .rlist a.cur { color:var(--brown); font-weight:700; } .n3 .rlist a.cur:before { background:var(--orange); }

  /* ---------- N4 · solid panel ---------- */
  .n4 .panel { background:var(--brown); padding:22px 24px 24px; }
  .n4 .up { color:#B7AFA6; border-bottom-color:rgba(255,255,255,.14); }
  .n4 .up i { background:rgba(255,255,255,.34); }
  .n4 .up:hover { color:#fff; } .n4 .up:hover i { background:rgba(255,255,255,.7); }
  .n4 .rlist { padding-top:4px; }
  .n4 .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                 color:#D8D4CF; padding:8px 2px 8px 17px; transition:color .14s ease; }
  .n4 .rlist a:before { content:""; position:absolute; left:0; top:50%; width:8px; height:9.238px;
        background:rgba(255,255,255,.30); clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n4 .rlist a:hover { color:#fff; } .n4 .rlist a:hover:before { background:var(--orange); }
  .n4 .rlist a.cur { color:#fff; }
  .n4 .rlist a.cur:before { background:var(--orange); }

  /* ---------- N5 · the live site's own shape, corrected ---------- */
  .n5 .wtitle { font-family:Raleway,sans-serif; font-weight:700; font-size:19px; line-height:26px;
                color:var(--brown); letter-spacing:.02em; padding-bottom:12px;
                border-bottom:1px solid var(--line); margin-bottom:2px; }
  /* same treatment as the N5x variants, so the four stay comparable */
  .n5 .parent { display:flex; align-items:center; justify-content:space-between; width:100%;
                background:none; border:0; border-bottom:1px solid #EEECE7; cursor:pointer;
                text-align:left; font-family:Raleway,sans-serif; font-weight:700; font-size:12px;
                line-height:18px; letter-spacing:.12em; text-transform:uppercase; color:#6B6B6B;
                padding:14px 2px; transition:color .14s ease; }
  .n5 .parent:hover { color:var(--brown); }
  .n5 .parent .chev { color:#8D867E; font-size:10px; letter-spacing:0;
                      transition:transform .16s ease; display:inline-block; }
  .n5.shut .rlist { display:none; }
  .n5.shut .parent .chev { transform:rotate(180deg); }
  .n5 .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                 color:#4A453F; padding:10px 2px 10px 26px; border-bottom:1px solid #EEECE7; }
  .n5 .rlist a:before { content:""; position:absolute; left:9px; top:50%; width:8px; height:9.238px;
        background:#D6D1C9; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n5 .rlist a:hover { color:var(--brown); } .n5 .rlist a:hover:before { background:var(--orange); }
  /* the live site fills the current row with a solid teal bar. Teal carries
     white at 2.60:1 — it fails even the 3:1 large-text floor — so the bar is
     brand orange here, which at 13px bold is still under 4.5:1 and therefore
     drawn as a LEFT RULE plus orange type instead of a filled bar. */
  .n5 .rlist a.cur { color:var(--brown); font-weight:700; background:#FBF6F1;
                     box-shadow:inset 3px 0 0 var(--orange); }
  .n5 .rlist a.cur:before { background:var(--orange); }

  /* the fold ruler for a sticky column: where the viewport ends */
  .fold { position:absolute; left:0; right:0; border-top:2px dashed rgba(180,74,51,.75);
          pointer-events:none; z-index:4; }
  .fold span { position:absolute; right:56px; top:-19px; background:#B44A33; color:#fff;
               font-family:"Open Sans",sans-serif; font-size:10.5px; line-height:15px;
               padding:1px 7px; letter-spacing:.03em; }
  .stage { position:relative; }
  .tabghost { position:absolute; right:0; top:50%; transform:translateY(-50%); background:var(--orange);
              color:#fff; font-family:Raleway,sans-serif; font-weight:700; font-size:13px;
              letter-spacing:.1em; writing-mode:vertical-rl; padding:20px 14px; z-index:5;
              box-shadow:-4px 4px 16px rgba(0,0,0,.22); }
"""


def up(dark=False):
    return ('<a class="up" href="/our-services/bc-injury-claims/"><i></i>All injury claims</a>')


def rows(items=None):
    items = items or CLAIMS
    return "".join('<a href="#"%s>%s</a>' % (' class="cur"' if c else '', n) for n, c in items)


def N1():
    return ('<div class="rail n1"><div class="stick">' + up() +
            '<div class="rlist">' + rows() + '</div></div></div>')


def N2():
    return ('<div class="rail n2"><div class="stick">' + up() +
            '<div class="rlist">' + rows() + '</div></div></div>')


def N3():
    cur = dict(CLAIMS)
    out = []
    for gh, names in GROUPS:
        r = "".join('<a href="#"%s>%s</a>' % (' class="cur"' if cur[n] else '', n) for n in names)
        out.append('<div class="grp"><div class="gh">%s</div><div class="rlist">%s</div></div>' % (gh, r))
    return ('<div class="rail n3"><div class="stick">' + up() + "".join(out) + '</div></div>')


def N4():
    return ('<div class="rail n4"><div class="stick"><div class="panel">' + up(True) +
            '<div class="rlist">' + rows() + '</div></div></div></div>')


def N5():
    subs = "".join('<a href="#"%s>%s</a>' % (' class="cur"' if c else '', n) for n, c in CLAIMS)
    return ('<div class="rail n5"><div class="stick">'
            '<div class="wtitle">Navigation</div>'
            '<button class="parent" type="button" aria-expanded="true">Injury Claims'
              '<span class="chev">&#9660;</span></button>'
            '<div class="rlist">' + subs + '</div></div></div>')


OPTIONS = [
    ("N1", "Hairline rows", "the baseline",
     "One row per claim, separated by hairlines, the hexagon bullet sliding in on hover, the current "
     "claim in orange. Topped by an &ldquo;All injury claims&rdquo; link back to the parent.",
     "This is the shape the earlier sheet&rsquo;s S5 used, plus the parent link. It is the most "
     "comfortable to read and the tallest &mdash; which on a sticky column is the thing that decides "
     "whether it works at all.", N1),

    ("N2", "Compact", "shortest",
     "The same thirteen rows with the hairlines dropped, the type a step down and the padding halved, "
     "so the whole column clears the shortest viewport with room to spare.",
     "Dropping the hairlines is what buys most of the height &mdash; the bullets already separate the "
     "rows, so the rules were doing the job twice. The bullet is grey until hover or current, which "
     "keeps thirteen orange marks from reading as thirteen calls to action.", N2),

    ("N3", "Grouped", "scannable",
     "The thirteen claims split into three named sets &mdash; vehicle &amp; travel, property &amp; "
     "premises, other &mdash; so the reader scans three things instead of thirteen.",
     "The only option that makes the list <b>mean</b> something rather than just fit. <b>The grouping "
     "is a proposal, not the firm&rsquo;s:</b> it is derived from the claim names alone, and a taxonomy "
     "of what the firm does is the firm&rsquo;s to confirm. Note it also puts Boat Accidents under "
     "vehicle &amp; travel, which is a judgement someone should agree with.", N3),

    ("N4", "Solid panel", "present",
     "The list inside a brown block, type reversed out, current claim in white with an orange bullet.",
     "The heaviest presence: a sticky brown block is a permanent object on the page rather than a list "
     "that happens to stay put. It is also the only option whose contrast has to be checked &mdash; the "
     "rows are <code>#D8D4CF</code> on brown (<b>7.37:1</b>), the current one white on brown "
     "(<b>10.86:1</b>) and the &ldquo;All injury claims&rdquo; link <code>#B7AFA6</code> on brown "
     "(<b>5.02:1</b>). All three clear 4.5:1 &mdash; measured, not assumed.", N4),

    ("N5", "The live shape, corrected", "familiar",
     "What the live site does &mdash; a &ldquo;Navigation&rdquo; title, the parent category as a row "
     "with a chevron, then the sub-list &mdash; redrawn in the new vocabulary.",
     "Kept so the familiar structure can be compared rather than abandoned by default. <b>One thing "
     "could not be reproduced:</b> the live site fills the current row with a solid teal bar and white "
     "text, which is <b>2.60:1</b> &mdash; below even the 3:1 large-text floor. Brand orange at 13px "
     "bold is no better &mdash; 3.03:1 against a 4.5:1 requirement &mdash; so the current row is marked "
     "by an orange left rule and a pale ground with the words in brown (<b>10.12:1</b>).", N5),
]


# ===========================================================================
# ROUND 2 — five more, drawn in a more contemporary idiom (Adi, 2026-08-30:
# "tambahkan beberapa opsi lagi tampilan modern").
#
# "Modern" here is not a mood word: each of these does something structural the
# first five do not — a connecting rail, a soft hit-area, an ordinal index, a
# disclosure, a filter. All five obey the rules the first round established:
# sticky, no form, orange marks / brown reads, hexagons on ratio, and a way
# back up to the parent category.
# ===========================================================================
ROUND2_CSS = r"""
  /* ---------- N6 · connecting rail ---------- */
  .n6 .rlist { position:relative; padding-left:24px; padding-top:6px; }
  /* the rail is a CONNECTOR, not a progress bar — these thirteen claims are
     siblings, not steps, and nothing about them is sequential */
  .n6 .rlist:before { content:""; position:absolute; left:5.5px; top:14px; bottom:14px; width:2px;
                      background:var(--line); }
  .n6 .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                 color:#4A453F; padding:8px 2px; transition:color .14s ease; }
  .n6 .rlist a:before { content:""; position:absolute; left:-20.5px; top:50%; width:9px; height:10.392px;
        background:#C6C0B8; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease, transform .14s ease; box-shadow:0 0 0 4px #fff; }
  .n6 .rlist a:hover { color:var(--brown); } .n6 .rlist a:hover:before { background:#8D867E; }
  .n6 .rlist a.cur { color:var(--brown); font-weight:700; }
  .n6 .rlist a.cur:before { background:var(--orange); width:13px; height:15.01px; left:-22.5px; }

  /* ---------- N7 · soft rows ---------- */
  .n7 .rlist { padding-top:6px; margin:0 -12px; }
  .n7 .rlist a { display:block; position:relative; font-size:13.5px; line-height:18px; font-weight:600;
                 color:#4A453F; padding:10px 12px; border-radius:8px;
                 transition:background .14s ease, color .14s ease; }
  .n7 .rlist a:hover { background:#F4F2ED; color:var(--brown); }
  .n7 .rlist a.cur { background:#FBEFE8; color:var(--brown); font-weight:700;
                     box-shadow:inset 3px 0 0 var(--orange); }

  /* ---------- N8 · ordinal index ---------- */
  .n8 .rlist { padding-top:6px; }
  .n8 .rlist a { display:flex; gap:13px; align-items:baseline; font-size:13px; line-height:18px;
                 font-weight:600; color:#4A453F; padding:8px 2px; transition:color .14s ease; }
  .n8 .rlist a .no { flex:none; width:17px; font-family:Raleway,sans-serif; font-weight:700;
                     font-size:11px; line-height:18px; color:#6B6B6B; font-variant-numeric:tabular-nums; }
  .n8 .rlist a:hover { color:var(--brown); }
  .n8 .rlist a.cur { color:var(--brown); font-weight:700; }
  /* the current row swaps its numeral for the hexagon — the mark carries
     "you are here", the numeral carries "which of thirteen" */
  .n8 .rlist a.cur .no { position:relative; text-indent:-9999px; overflow:hidden; }
  .n8 .rlist a.cur .no:before { content:""; position:absolute; left:0; top:4px; width:10px;
        height:11.547px; background:var(--orange); text-indent:0;
        clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%); }

  /* ---------- N9 · disclosure by group ---------- */
  .n9 .grp { border-bottom:1px solid #EEECE7; }
  .n9 .gh { display:flex; align-items:center; justify-content:space-between; width:100%;
            background:none; border:0; cursor:pointer; text-align:left; padding:13px 2px;
            font-family:Raleway,sans-serif; font-weight:700; font-size:11.5px; line-height:16px;
            letter-spacing:.13em; text-transform:uppercase; color:#6B6B6B; transition:color .14s ease; }
  .n9 .gh:hover { color:var(--brown); }
  .n9 .grp.open .gh { color:var(--brown); }
  .n9 .gh .n { font-family:"Open Sans",sans-serif; font-weight:400; font-size:11px; line-height:16px;
               letter-spacing:0; color:#6B6B6B; }
  .n9 .gh .n:after { content:" \25be"; display:inline-block; transition:transform .16s ease; }
  .n9 .grp.open .gh .n:after { transform:rotate(180deg); }
  .n9 .body { display:none; padding-bottom:10px; }
  .n9 .grp.open .body { display:block; }
  .n9 .body a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                color:#4A453F; padding:7px 2px 7px 17px; transition:color .14s ease; }
  .n9 .body a:before { content:""; position:absolute; left:0; top:50%; width:8px; height:9.238px;
        background:#D6D1C9; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n9 .body a:hover { color:var(--brown); } .n9 .body a:hover:before { background:var(--orange); }
  .n9 .body a.cur { color:var(--brown); font-weight:700; } .n9 .body a.cur:before { background:var(--orange); }

  /* ---------- N10 · filter ---------- */
  .n10 .filt { position:relative; margin:14px 0 4px; }
  .n10 .filt input { width:100%; border:1px solid var(--line); background:#fff; padding:10px 12px 10px 34px;
                     font-family:"Open Sans",sans-serif; font-size:13px; line-height:18px; color:#423C37;
                     outline:none; transition:border-color .14s ease; }
  .n10 .filt input:focus { border-color:var(--orange); }
  .n10 .filt input::placeholder { color:#6B6B6B; }
  .n10 .filt .fa { position:absolute; left:12px; top:11px; font-size:13px; color:#8D867E; }
  .n10 .count { font-size:11px; line-height:16px; color:#6B6B6B; padding:6px 2px 2px; }
  .n10 .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                  color:#4A453F; padding:7px 2px 7px 17px; transition:color .14s ease; }
  .n10 .rlist a:before { content:""; position:absolute; left:0; top:50%; width:8px; height:9.238px;
        background:#D6D1C9; clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
        transform:translateY(-50%); transition:background .14s ease; }
  .n10 .rlist a:hover { color:var(--brown); } .n10 .rlist a:hover:before { background:var(--orange); }
  .n10 .rlist a.cur { color:var(--brown); font-weight:700; } .n10 .rlist a.cur:before { background:var(--orange); }
  .n10 .rlist a.hide { display:none; }
  .n10 .none { display:none; font-size:12.5px; line-height:18px; color:#6B6B6B; padding:8px 2px; }
  .n10 .none.on { display:block; }
"""


def N6():
    return ('<div class="rail n6"><div class="stick">' + up() +
            '<div class="rlist">' + rows() + '</div></div></div>')


def N7():
    return ('<div class="rail n7"><div class="stick">' + up() +
            '<div class="rlist">' + rows() + '</div></div></div>')


def N8():
    r = "".join('<a href="#"%s><span class="no">%02d</span><span>%s</span></a>'
                % (' class="cur"' if c else '', i + 1, n)
                for i, (n, c) in enumerate(CLAIMS))
    return ('<div class="rail n8"><div class="stick">' + up() +
            '<div class="rlist">' + r + '</div></div></div>')


def N9():
    cur = dict(CLAIMS)
    out = []
    for gh, names in GROUPS:
        open_ = any(cur[n] for n in names)
        rws = "".join('<a href="#"%s>%s</a>' % (' class="cur"' if cur[n] else '', n) for n in names)
        out.append('<div class="grp%s"><button class="gh" type="button">%s'
                   '<span class="n">%d</span></button><div class="body">%s</div></div>'
                   % (" open" if open_ else "", gh, len(names), rws))
    return ('<div class="rail n9"><div class="stick">' + up() + "".join(out) + '</div></div>')


def N10():
    return ('<div class="rail n10"><div class="stick">' + up() +
            '<div class="filt"><i class="fa fa-search"></i>'
              '<input type="text" placeholder="Filter claims" aria-label="Filter injury claims"></div>'
            '<div class="count"><b>13</b> of 13</div>'
            '<div class="rlist">' + rows() + '</div>'
            '<div class="none">No claim matches that.</div></div></div>')


ROUND2 = [
    ("N6", "Connecting rail", "one object",
     "A hairline rail down the left with a node on every claim, the current one a larger orange "
     "hexagon sitting on a white gap in the line.",
     "The rail makes thirteen links read as <b>one thing</b> rather than thirteen things, which is what "
     "a long sibling list needs most. <b>It is a connector, not a progress bar</b> &mdash; these claims "
     "are siblings, not steps, so nothing above the current node is coloured in. Getting that wrong "
     "would tell the reader they are part-way through a sequence that does not exist.", N6),

    ("N7", "Soft rows", "generous",
     "No rules and no bullets at all &mdash; each claim is a rounded hit-area that fills on hover, and "
     "the current one is a warm tinted block with an orange bar down its left edge.",
     "The quietest of the ten at rest and the most responsive in use: the whole row is the target, not "
     "the words. The tint is <code>#FBEFE8</code> and carries brown at <b>9.63:1</b>; the hover tint "
     "carries it at <b>8.48:1</b>. The rows are pulled 12px into the gutter so the fill lines up with "
     "the column edge rather than floating inside it.", N7),

    ("N8", "Ordinal index", "editorial",
     "Every claim numbered 01&ndash;13 in tabular figures, and the current row swaps its numeral for "
     "the orange hexagon.",
     "The numbers do a job no other option does: they say <b>how many there are</b> and how far down "
     "any one of them sits &mdash; useful on a list this long. Swapping the numeral for the mark on the "
     "current row keeps the rule intact (the mark says <i>here</i>, the numeral says <i>which of "
     "thirteen</i>) without setting anything in orange type. Numerals are <code>#6B6B6B</code>, "
     "<b>5.33:1</b> &mdash; they are text, so they get a text ratio.", N8),

    ("N9", "Disclosure by group", "shortest",
     "N3&rsquo;s three groups, but only the group holding the current claim is open. The others are one "
     "row each with a count. Click to open.",
     "By far the shortest thing that still reaches all thirteen &mdash; and the only option whose height "
     "does not grow when the firm adds a fourteenth claim. The cost is a click between a reader and "
     "eleven of the thirteen links, which on a sidebar whose whole job is lateral movement is a real "
     "cost. <b>It inherits N3&rsquo;s open question:</b> the grouping is still a proposal, not the "
     "firm&rsquo;s.", N9),

    ("N10", "Filter", "scales",
     "A filter field above the list; typing narrows the thirteen live and the count updates.",
     "<b>This is the one that solves a problem the page does not have yet.</b> Thirteen items are "
     "scannable; a filter earns its place at thirty. It is here because it is the answer if the claim "
     "list keeps growing &mdash; and because it is worth seeing the cost: it puts an input back into a "
     "sidebar we just cleared of inputs, and a reader who sees a field in a sidebar has to work out "
     "that this one does not send anything.", N10),
]


# ===========================================================================
# ROUND 3 — N5 without the hexagon (Adi, 2026-08-30: "N5 juga saya suka namun
# buat tanpa hexagon").
#
# N5 uses the hexagon in exactly one place: the bullet on each claim row. It is
# NOT what marks the current claim — that is the orange left rule, the pale
# ground and the brown bold type, none of which involve a hexagon. So removing
# it costs nothing structural; it just leaves a slot, and something has to
# decide what lives there. Three answers, same option.
# ===========================================================================
ROUND3_CSS = r"""
  /* the shell N5a/b/c share — N5's own title, parent row and current-row
     treatment, with the bullet slot left empty for each variant to fill */
  .n5x .wtitle { font-family:Raleway,sans-serif; font-weight:700; font-size:19px; line-height:26px;
                 color:var(--brown); letter-spacing:.02em; padding-bottom:12px;
                 border-bottom:1px solid var(--line); margin-bottom:2px; }
  /* =====================================================================
     THE PARENT ROW IS A CONTROL, SO IT IS A <button>.
     It carried a chevron and did nothing — an affordance that lies. It now
     collapses the thirteen claims, which is the only thing a chevron on a
     parent row can honestly mean.
     Set uppercase and greyer (Adi, 2026-08-30): #6B6B6B is 5.33:1, so it
     still clears 4.5:1 at 12px bold. Uppercase needs the letter-spacing —
     caps set at body tracking close up and stop being scannable.
     ===================================================================== */
  .n5x .parent { display:flex; align-items:center; justify-content:space-between; width:100%;
                 background:none; border:0; border-bottom:1px solid #EEECE7; cursor:pointer;
                 text-align:left; font-family:Raleway,sans-serif; font-weight:700; font-size:12px;
                 line-height:18px; letter-spacing:.12em; text-transform:uppercase;
                 color:#6B6B6B; padding:14px 2px; transition:color .14s ease; }
  .n5x .parent:hover { color:var(--brown); }
  .n5x .parent .chev { color:#8D867E; font-size:10px; letter-spacing:0;
                       transition:transform .16s ease; display:inline-block; }
  /* closed = the list is gone and the chevron has turned over */
  .n5x.shut .rlist { display:none; }
  .n5x.shut .parent .chev { transform:rotate(180deg); }
  .n5x .rlist a { display:block; position:relative; font-size:13px; line-height:17px; font-weight:600;
                  color:#4A453F; border-bottom:1px solid #EEECE7; transition:color .14s ease; }
  .n5x .rlist a:hover { color:var(--brown); }
  /* the current row never needed the bullet: an orange left rule (3.03:1 —
     a non-text indicator only needs 3:1), a pale ground, and brown bold type
     at 10.12:1 */
  .n5x .rlist a.cur { color:var(--brown); font-weight:700; background:#FBF6F1;
                      box-shadow:inset 3px 0 0 var(--orange); }

  /* ---------- N5a · nothing in the slot ---------- */
  .n5a .rlist a { padding:10px 2px 10px 14px; }

  /* ---------- N5b · a chevron, which is what the live site itself uses ---- */
  .n5b .rlist a { padding:10px 2px 10px 24px; }
  .n5b .rlist a:before { content:"\203A"; position:absolute; left:9px; top:50%;
        transform:translateY(-50%); font-family:"Open Sans",sans-serif; font-size:15px; line-height:1;
        color:#B7AFA6; transition:color .14s ease, transform .14s ease; }
  .n5b .rlist a:hover:before { color:var(--orange); transform:translateY(-50%) translateX(2px); }
  .n5b .rlist a.cur:before { color:var(--orange); }

  /* ---------- N5c · a short rule ---------- */
  .n5c .rlist a { padding:10px 2px 10px 26px; }
  .n5c .rlist a:before { content:""; position:absolute; left:6px; top:50%; width:10px; height:2px;
        background:#D6D1C9; transform:translateY(-50%);
        transition:background .14s ease, width .14s ease; }
  .n5c .rlist a:hover:before { background:var(--orange); width:14px; }
  .n5c .rlist a.cur:before { background:var(--orange); width:14px; }
"""


def n5x(variant):
    subs = "".join('<a href="#"%s>%s</a>' % (' class="cur"' if c else '', n) for n, c in CLAIMS)
    return ('<div class="rail n5x %s"><div class="stick">'
            '<div class="wtitle">Navigation</div>'
            '<button class="parent" type="button" aria-expanded="true">Injury Claims'
              '<span class="chev">&#9660;</span></button>'
            '<div class="rlist">' + subs + '</div></div></div>') % variant


def N5A(): return n5x("n5a")
def N5B(): return n5x("n5b")
def N5C(): return n5x("n5c")


ROUND3 = [
    ("N5&middot;a", "Nothing in the slot", "quietest",
     "N5 with the bullet simply gone. Thirteen rows of type separated by hairlines; the current claim "
     "still carried by its orange left rule, pale ground and brown bold.",
     "The honest reading of &ldquo;without the hexagon&rdquo;: take it out and put nothing back. "
     "<b>Nothing is lost</b> &mdash; the bullet was never what marked the current claim, and the "
     "hairlines already separate the rows, so the bullet was the third thing doing a job two things "
     "were already doing. It is also the only one of the three with no decoration to explain.", N5A),

    ("N5&middot;b", "A chevron", "what the live site uses",
     "A small &rsaquo; in the slot, grey at rest, orange and nudged right on hover, orange on the "
     "current row.",
     "<b>This is the closest thing to &ldquo;the live shape&rdquo; that is not a hexagon</b> &mdash; the "
     "live sidebar already puts a chevron on its parent row, so the mark is the site&rsquo;s own, just "
     "moved down a level. It is the only one of the three that says <i>this goes somewhere</i> rather "
     "than just <i>this is an item</i>, which on a list whose entire job is lateral movement is worth "
     "something.", N5B),

    ("N5&middot;c", "A short rule", "most neutral",
     "A 10px hairline dash in the slot that lengthens to 14px and turns orange on hover and on the "
     "current row.",
     "The mark with no vocabulary of its own &mdash; it is neither a brand shape nor a direction, just "
     "a tick that grows when you touch it. Useful if the hexagon is being removed <b>because the page "
     "already has enough shapes in it</b>, since a rule adds none. The growth on hover is doing the "
     "work the colour change alone would not.", N5C),
]


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; sidebar navigation 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">
<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>Sidebar navigation &mdash; options</h1>
  <p>The form is gone from all ten: the sidebar is the thirteen sibling injury claims and nothing
  else, and it <b>sticks</b> &mdash; measured while scrolling, not assumed. <b>Scroll this sheet to see it</b> &mdash; each frame carries the real
  article at real length, so the sticking is something you watch rather than something a caption
  claims. <b>Round&nbsp;1 (N1&ndash;N5)</b> works within the vocabulary the rest of the site already
  uses; <b>round&nbsp;2 (N6&ndash;N10)</b> is a more contemporary idiom, each option doing something
  structural the first five do not &mdash; a connecting rail, a soft hit-area, an ordinal index, a
  disclosure, a filter. <b>N9 and N10 are interactive</b> &mdash; click a group, type in the filter.</p>

  <div class="box warn"><b>Sticky turns a styling question into a measuring one.</b>
  A sticky block taller than the viewport has a bottom the reader can never reach &mdash; it stops
  scrolling with the page and the last claims sit permanently below the screen edge. So the number
  that decides these options is not how they look but <b>how tall they are</b>.
  <br><br>The budget is <b>the viewport minus <code>top</code></b>, not the viewport minus the header:
  once the block has stuck, the header has already scrolled away. At <code>top:24px</code> that is
  <b>676px</b> on a 700px viewport and <b>744px</b> on a 768px one.
  <table>
    <tr><th>option</th><th>height</th><th>fits a 700px viewport?<br><span style="font-weight:400;text-transform:none;letter-spacing:0">676px usable</span></th><th>fits 768px?<br><span style="font-weight:400;text-transform:none;letter-spacing:0">744px usable</span></th></tr>
    <tr><td>N2 &mdash; compact</td><td>__H2__</td><td>__F2A__</td><td>__F2B__</td></tr>
    <tr><td>N3 &mdash; grouped</td><td>__H3__</td><td>__F3A__</td><td>__F3B__</td></tr>
    <tr><td>N4 &mdash; solid panel</td><td>__H4__</td><td>__F4A__</td><td>__F4B__</td></tr>
    <tr><td>N5 &mdash; the live shape</td><td>__H5__</td><td>__F5A__</td><td>__F5B__</td></tr>
    <tr><td>N1 &mdash; hairline rows</td><td>__H1__</td><td>__F1A__</td><td>__F1B__</td></tr>
    <tr><th colspan="4" style="background:#F6F4EF">round 2</th></tr>
    <tr><td>N9 &mdash; disclosure by group</td><td>__H9__</td><td>__F9A__</td><td>__F9B__</td></tr>
    <tr><td>N6 &mdash; connecting rail</td><td>__H6__</td><td>__F6A__</td><td>__F6B__</td></tr>
    <tr><td>N8 &mdash; ordinal index</td><td>__H8__</td><td>__F8A__</td><td>__F8B__</td></tr>
    <tr><td>N10 &mdash; filter</td><td>__H10__</td><td>__F10A__</td><td>__F10B__</td></tr>
    <tr><td>N7 &mdash; soft rows</td><td>__H7__</td><td>__F7A__</td><td>__F7B__</td></tr>
    <tr><th colspan="4" style="background:#F6F4EF">round 3 &mdash; N5 without the hexagon</th></tr>
    <tr><td>N5&middot;a &mdash; nothing in the slot</td><td>__H5a__</td><td>__F5aA__</td><td>__F5aB__</td></tr>
    <tr><td>N5&middot;b &mdash; a chevron</td><td>__H5b__</td><td>__F5bA__</td><td>__F5bB__</td></tr>
    <tr><td>N5&middot;c &mdash; a short rule</td><td>__H5c__</td><td>__F5cA__</td><td>__F5cB__</td></tr>
  </table>
  Removing the bullet does not change the height: all three are <b>582px</b>, exactly N5&rsquo;s. The
  bullet sat inside the row&rsquo;s own line box, so it was never paying for any of it.
  <br><br><b>Collapsed, all four drop to 88px</b> &mdash; the title and the parent row alone. That is the
  cheapest the sidebar can be while still offering a way back into the thirteen.
  <b>Read the headroom, not the pass mark.</b> The margin is what a sticky header would eat first
  (next box), and what a fourteenth claim would eat next &mdash; every option except N9 grows by one
  row when the firm adds one.
  <br><br><b>If a future option does not fit</b>, the fix is one rule &mdash;
  <code>max-height:calc(100vh - 48px); overflow-y:auto</code> on the sticky block, so the list scrolls
  inside itself instead of hiding its own tail. It costs a scrollbar inside a sidebar, which is why it
  is a fallback rather than the default.
  </div>

  <div class="box"><b>What the sticky is measured from.</b> <code>top:24px</code> &mdash; the gap from
  the top of the viewport. The page&rsquo;s header does not stick today, so 24 is the whole number. If a
  sticky header is ever added, this becomes that header&rsquo;s height plus 24, and every option here
  loses exactly that much room. Worth knowing before the header question is decided.</div>

  <div class="box warn"><b>The parent row in N5 was an affordance that lied &mdash; fixed.</b>
  It carried a chevron and did nothing: it was a <code>&lt;div&gt;</code>, not a control, and no script
  ever looked at it. It is now a <code>&lt;button&gt;</code> that actually collapses the thirteen
  claims, with <code>aria-expanded</code> tracking the state and the chevron turning over. A mark that
  promises an action has to perform one; the alternative was to take the chevron off, which would have
  made the row a label. <b>Also restyled at Adi&rsquo;s ask:</b> &ldquo;Injury Claims&rdquo; is now
  uppercase and greyer &mdash; <code>#6B6B6B</code>, <b>5.33:1</b>, so it still clears 4.5:1 at 12px
  bold. The capitals come from <code>text-transform</code>, not from retyping the words, and the
  letter-spacing is <code>.12em</code> because caps set at body tracking close up and stop being
  scannable. Applied to N5 as well as N5&middot;a/b/c so the four stay comparable.</div>

  <div class="box"><b>One thing all five add that none of the earlier options had: a way back up.</b>
  N1&ndash;N4 carry an explicit <b>&ldquo;All injury claims&rdquo;</b> link at the top; N5 carries the
  parent as its own row, the way the live site does. Without one of the two there is no route from an
  inner claim back to the category it belongs to &mdash; the breadcrumb has one, but the breadcrumb
  scrolls away and a sticky sidebar does not.</div>

  <div class="box ask"><b>Two things only the firm can settle:</b>
    <ul>
      <li><b>N3&rsquo;s grouping is a proposal, not a fact.</b> It is derived from the thirteen claim
      names alone. Whether &ldquo;vehicle &amp; travel / property &amp; premises / other&rdquo; is how
      the firm thinks about its own injury work &mdash; and whether Boat Accidents belongs with vehicles
      &mdash; is theirs to confirm.</li>
      <li><b>Whether the order is meaningful.</b> All five keep the live site&rsquo;s order. If it is
      alphabetical-by-accident rather than by volume or importance, the sidebar is the place that shows
      it up.</li>
    </ul>
  </div>

  <div class="box"><b>Reading the frames:</b> real geometry &mdash;
  <code>832 + 64 + 384 = 1280</code> &mdash; real article copy at real size, dimmed. The dashed red line
  in each frame is 676px below the top of the sticky block &mdash; the bottom of a 700px viewport once
  the block has stuck. Anything below that line is the part a reader on a small laptop would never
  reach. No option crosses it. The orange tab is on the right edge
  because it is on the page now, and it is why the sidebar no longer needs a form.</div>
</div>

<div class="roundhead">Round 1 &mdash; N1 &hellip; N5</div>
"""


def stage(tag, name, hint, what, note, fn):
    return (
            '\n<div class="opt">\n'
            '  <div class="opt-label"><span class="tag">%s</span><span class="name">%s</span>'
            '<span class="h">%s</span><span class="what">%s</span></div>\n'
            '  <div class="stage"><span class="tabghost">WE&rsquo;VE GOT YOUR BACK</span>'
            '<div class="pg"><div class="wrap"><div class="cols">'
            '<div class="col"><div class="h2">League &amp; Williams are Experienced Car Accident '
            'Injury Claims Lawyers</div><div class="prose">%s</div></div>'
            '%s</div></div></div></div>\n'
            '  <p class="opt-note">%s</p>\n'
            '</div>\n' % (tag, name, hint, what, PROSE, fn(), note))


def build(fill):
    out = [HEAD.replace("__CSS__", lift_chrome() + NAV_CSS + ROUND2_CSS + ROUND3_CSS)]
    for k, v in fill.items():
        out[0] = out[0].replace(k, v)
    for o in OPTIONS:
        out.append(stage(*o))
    out.append('\n<div class="roundhead">Round 2 &mdash; N6 &hellip; N10 &middot; '
               'a more contemporary idiom</div>\n')
    for o in ROUND2:
        out.append(stage(*o))
    out.append('\n<div class="roundhead">Round 3 &mdash; N5 without the hexagon</div>\n')
    for o in ROUND3:
        out.append(stage(*o))
    out.append("""
<script>
// the fold line is drawn from the TOP OF THE STICKY BLOCK, not the top of the
// stage: what matters is how much of the list is visible once it has stuck.
document.querySelectorAll('.stage').forEach(function(st){
  var s = st.querySelector('.stick'); if (!s) return;
  var line = document.createElement('div');
  line.className = 'fold';
  line.innerHTML = '<span>676px \\u2014 bottom of a 700px viewport, once stuck</span>';
  line.style.top = (s.offsetTop + 676) + 'px';
  st.querySelector('.pg').appendChild(line);
});

// N9 — disclosure. Opening one does not close the others: a reader who wants
// two groups open should be allowed two groups open.
document.querySelectorAll('.n9 .gh').forEach(function(b){
  b.addEventListener('click', function(){ b.parentNode.classList.toggle('open'); });
});

// N5 and its variants — the parent row collapses the thirteen claims. It had
// a chevron and no behaviour, which is an affordance that lies; now the mark
// and the action agree.
document.querySelectorAll('.n5 .parent, .n5x .parent').forEach(function(b){
  b.addEventListener('click', function(){
    var rail = b.closest('.rail');
    var shut = rail.classList.toggle('shut');
    b.setAttribute('aria-expanded', shut ? 'false' : 'true');
  });
});

// N10 — filter. Matches anywhere in the name, not just the start: someone
// looking for "cyclist accident" and someone typing "accident" are both right.
document.querySelectorAll('.n10').forEach(function(rail){
  var box   = rail.querySelector('.filt input'),
      links = [].slice.call(rail.querySelectorAll('.rlist a')),
      count = rail.querySelector('.count'),
      none  = rail.querySelector('.none');
  box.addEventListener('input', function(){
    var q = box.value.trim().toLowerCase(), n = 0;
    links.forEach(function(a){
      var hit = !q || a.textContent.toLowerCase().indexOf(q) > -1;
      a.classList.toggle('hide', !hit);
      if (hit) n++;
    });
    count.innerHTML = '<b>' + n + '</b> of ' + links.length;
    none.classList.toggle('on', n === 0);
  });
});
</script>
""")
    out.append("\n</body>\n</html>\n")
    return "".join(out)


if __name__ == "__main__":
    fill = {}
    for i in range(1, 6):
        fill["__H%d__" % i] = "&mdash;"
        fill["__F%dA__" % i] = "&mdash;"
        fill["__F%dB__" % i] = "&mdash;"
    if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "sidebarnav_heights.json")):
        fill.update(json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                                "sidebarnav_heights.json"))))
    html = build(fill)
    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)))
