# -*- coding: utf-8 -*-
# 1:1 low-fidelity: the real page, geometry untouched, colour removed.
import re
SRC = "/srv/apps/leaguelaw/wp-content/prototype/home-v2.html"
OUT = "/srv/apps/leaguelaw/wp-content/lofi/home-v2-lofi.html"
h = open(SRC, encoding="utf-8").read()

def lin(c):
    c = c/255
    return c/12.92 if c <= 0.03928 else ((c+0.055)/1.055)**2.4
def unlin(L):
    c = L*12.92 if L <= 0.0031308 else 1.055*(L**(1/2.4)) - 0.055
    return max(0, min(255, round(c*255)))
def grey(r, g, b):
    """The grey with the same relative luminance — value structure preserved exactly."""
    return unlin(0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b))

def hex_repl(m):
    v = m.group(1)
    if len(v) == 3: r, g, b = (int(c*2, 16) for c in v)
    else:           r, g, b = (int(v[i:i+2], 16) for i in (0, 2, 4))
    n = grey(r, g, b)
    return "#%02X%02X%02X" % (n, n, n)

def rgb_repl(m):
    parts = [p.strip() for p in m.group(2).split(",")]
    r, g, b = (float(parts[0]), float(parts[1]), float(parts[2]))
    n = grey(r, g, b)
    if len(parts) == 4:
        return "rgba(%d,%d,%d,%s)" % (n, n, n, parts[3])
    return "rgb(%d,%d,%d)" % (n, n, n)

HEXRE = re.compile(r'#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})\b')
RGBRE = re.compile(r'\b(rgba?)\(([^)]*)\)')

# ---- 1. desaturate the stylesheet
i = h.index("<style>"); j = h.index("</style>", i)
css = h[i+len("<style>"):j]
css_g = RGBRE.sub(rgb_repl, HEXRE.sub(hex_repl, css))
# the hexagon clip-paths carry %-coordinates, not colours — untouched by the above.
# SVG data-URIs inside CSS use %23RRGGBB; convert those too.
def svgcol(m):
    v = m.group(1); r, g, b = (int(v[k:k+2], 16) for k in (0, 2, 4)); n = grey(r, g, b)
    return "%%23%02X%02X%02X" % (n, n, n)
css_g = re.sub(r'%23([0-9A-Fa-f]{6})', svgcol, css_g)
h = h[:i+len("<style>")] + css_g + h[j:]

# ---- 2. any colour sitting in an inline style attribute in the body
def inline(m):
    s = m.group(0)
    s = RGBRE.sub(rgb_repl, HEXRE.sub(hex_repl, s))
    return s
b0 = h.index("<body>")
head, body = h[:b0], h[b0:]
body = re.sub(r'style="[^"]*"', inline, body)

# ---- 3. photographs become placeholders that occupy EXACTLY the same box
PH = ("data:image/svg+xml;utf8,"
      "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' viewBox='0 0 100 100'>"
      "<rect width='100' height='100' fill='%23E4E4E4'/>"
      "<path d='M0 0 L100 100 M100 0 L0 100' stroke='%23CFCFCF' stroke-width='0.7' vector-effect='non-scaling-stroke'/>"
      "</svg>")
n_img = [0]
def img(m):
    tag = m.group(0)
    if 'src=' not in tag: return tag
    n_img[0] += 1
    return re.sub(r'src="[^"]*"', 'src="%s"' % PH, tag, count=1)
body = re.sub(r'<img\b[^>]*>', img, body)

h = head + body

# ---- 4. title, and a marker so the file is never mistaken for the prototype
h = h.replace("<title>League Law &mdash; home v2 (zeina branch)</title>",
              "<title>League Law &mdash; home v2, low fidelity (1:1)</title>", 1)
h = re.sub(r'<title>.*?</title>', "<title>League Law &mdash; home v2, low fidelity (1:1)</title>", h, count=1, flags=re.S)
h = h.replace("<style>", """<style>
  /* ================= LOW FIDELITY, 1:1 =================
     This file is home-v2.html with its geometry untouched and its colour removed.
     Nothing was redrawn: every width, height, padding, gap, grid column,
     font-family, font-size, weight, letter-spacing and clip-path is the value
     the prototype uses, because this IS the prototype's stylesheet.
     What changed, and only this:
       · every colour — hex, rgb(), rgba() and the hexes inside CSS SVG data-URIs
         — was replaced by the GREY OF THE SAME RELATIVE LUMINANCE, so the value
         structure (which block is darker than which) survives exactly.
       · every <img> keeps its element, its box and its object-fit, and points at
         a placeholder SVG, so photographs are absent without a single pixel of
         layout moving.
     One thing here is NOT the prototype's geometry, and it is deliberate: the
     hero's scroll-pin is flattened (see the note at the foot of this sheet).
     Regenerate with scratchpad/gen_lofi_12.py after any change to home-v2.html —
     do NOT hand-edit this file; it is output. ==================== */
""", 1)


# ---- 5. a low-fidelity sheet is a static document: remove every response to input
def strip_rule_blocks(css, pred):
    """Drop whole rule blocks whose selector matches pred. Brace-aware, so nested
       at-rules survive."""
    out, i, n = [], 0, len(css)
    while i < n:
        b = css.find('{', i)
        if b == -1:
            out.append(css[i:]); break
        sel = css[i:b]
        # comments live in front of the selector; they must not be read AS the
        # selector. Without this, a comment sitting before an @media block hides
        # the '@' from the test below, the block is parsed as an ordinary rule
        # ending at its first inner '}', and the rule that follows it is eaten.
        selc = re.sub(r'/\*.*?\*/', '', sel, flags=re.S)
        # an at-rule with a block of rules inside — recurse into it
        if selc.strip().startswith('@') and not selc.strip().startswith('@import'):
            depth, j = 1, b + 1
            while j < n and depth:
                if css[j] == '{': depth += 1
                elif css[j] == '}': depth -= 1
                j += 1
            inner = css[b+1:j-1]
            kept = strip_rule_blocks(inner, pred)
            if kept.strip():
                out.append(sel + '{' + kept + '}')
            i = j
            continue
        e = css.find('}', b)
        if e == -1:
            out.append(css[i:]); break
        if not pred(selc):
            out.append(css[i:e+1])
        i = e + 1
    return ''.join(out)

INTERACTIVE = (':hover', ':focus', ':active', ':focus-within', ':focus-visible', ':checked', ':target')
css2 = h[h.index('<style>')+len('<style>'):h.index('</style>')]
css2 = strip_rule_blocks(css2, lambda sel: any(k in sel for k in INTERACTIVE))
# no motion of any kind
# @keyframes holds percentage stops, not rules, so the brace-aware walker steps
# into it rather than dropping it. Cut the whole block by balancing braces.
def drop_at(css, name):
    out, i = [], 0
    while True:
        k = css.find(name, i)
        if k == -1:
            out.append(css[i:]); break
        b = css.find('{', k)
        depth, j = 1, b + 1
        while j < len(css) and depth:
            if css[j] == '{': depth += 1
            elif css[j] == '}': depth -= 1
            j += 1
        out.append(css[i:k]); i = j
    return ''.join(out)
css2 = drop_at(css2, '@keyframes')
css2 = re.sub(r'\s*(?:-webkit-)?(?:transition|animation)(?:-[a-z-]+)?\s*:[^;}]*;', '', css2)
css2 = re.sub(r'\s*cursor\s*:[^;}]*;', '', css2)
# the reduced-motion block existed only to switch the hero animation off; with the
# animation gone it is empty noise, and slide 1 has to be pinned visible instead.
css2 = strip_rule_blocks(css2, lambda sel: 'prefers-reduced-motion' in sel)
css2 += """
  /* ---------- LOW FIDELITY: nothing here responds to input ----------
     Every :hover / :focus / :active rule was removed, along with every
     transition, animation and @keyframes block, and every cursor declaration.
     The hero script is gone, so the click-to-snap, the minimise control and the
     auto-minimise tab do nothing. The FAQ panels are forced open because
     <details> is a control. Pointer events are switched off on the things that
     would otherwise still react — links, buttons, form fields, summaries — but
     NOT on the page as a whole, so the text stays selectable and copyable.
     The hero slider no longer cycles; slide 1 is pinned visible.

     THE HERO'S SCROLL-PIN IS FLATTENED (Adi, 2026-08-24). In the prototype the
     image pins and the form TRAVELS up over it as you scroll — that is a scroll
     response, and a low-fidelity sheet does not have those. Here the image is an
     ordinary block and the form sits in flow directly under it, pulled up by the
     same 84px it overlaps by at page load, so the composition you see is exactly
     the composition the prototype loads with. The section is no longer 200vh-292px
     tall; it is as tall as its contents, which is why there is no empty run of
     scroll after it. Four declarations, and only four, differ from the prototype
     because of this — they are the ones below. Every other width, height, padding
     and font in this sheet is still the prototype's. ---------------------------- */
  .nhero-scroll { height: auto; }
  .nhero-sticky { position: relative; top: auto; }
  .peekwrap     { position: relative; top: auto; margin-top: -84px; }
  .nhero-sticky .fsl { opacity: 0; }
  .nhero-sticky .fsl:nth-child(1) { opacity: 1; }
  a, button, input, textarea, summary, label, .rbtn, .swarmbtn, .minbtn, .ctatab { pointer-events: none; }
  summary { list-style: none; }
"""
# rules and at-rules left empty by the strip are dead weight in a file that
# claims to have no motion — take them out so the sheet reads clean.
for _ in range(3):
    css2 = re.sub(r'[^{}@/]*\{\s*\}', '', css2)
    css2 = re.sub(r'@media[^{]*\{\s*\}', '', css2)
css2 = re.sub(r'\n{3,}', '\n\n', css2)
h = h[:h.index('<style>')+len('<style>')] + css2 + h[h.index('</style>'):]

# markup: no destinations, no toggles, no script
body_i = h.index('<body>')
head2, body2 = h[:body_i], h[body_i:]
body2 = re.sub(r'\s+href="[^"]*"', '', body2)
body2 = re.sub(r'<details(?![^>]*\bopen\b)', '<details open', body2)
body2 = re.sub(r'<script\b.*?</script>', '', body2, flags=re.S)
h = head2 + body2

open(OUT, "w", encoding="utf-8").write(h)
print("wrote", OUT, len(h), "bytes; images replaced:", n_img[0])
