đ§° La boĂźte Ă outils
Dans l'ordre oĂč ils se rejouent sur une page. PrĂ©requis : python3 et pip install playwright && playwright install chromium. Chaque script est lisible ici et se copie d'un geste.
audit_statique.py audit_statique.py page.html251 lignes
Balises Ă©quilibrĂ©es, ids en double, ancres mortes, variables, hex hors palette (ÎE2000 vers la pierre la plus proche), italiques, capitales en Cormorant, orthographes, doublons de paragraphes, sĂ©lecteurs nus â audit_statique.txt / .json
â§ Copier le script
# -*- coding: utf-8 -*-
"""Audit statique d'une page du corpus aMi contre la Charte de Communication
(prompt de contrÎle §3 + checklists XLVIII / LV). Ne corrige rien : relÚve."""
import re, sys, json, collections, html as H
from html.parser import HTMLParser
sys.path.insert(0, '../verif/verificateur')
import canon as K, couleur as C
SRC = sys.argv[1]
raw = open(SRC, encoding='utf-8').read()
txt = re.sub(r'data:image/[a-z+]+;base64,[A-Za-z0-9+/=]+', 'data:IMG', raw)
lines = txt.split('\n')
def lno(pos): return txt.count('\n', 0, pos) + 1
R = collections.OrderedDict()
def note(fam, quoi, ou='', mesure='', grave='corriger'):
R.setdefault(fam, []).append(dict(quoi=quoi, ou=ou, mesure=mesure, grave=grave))
# ââ 0 · identitĂ© âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
title = re.search(r'<title>(.*?)</title>', txt, re.S)
title = H.unescape(title.group(1)).strip() if title else ''
fname_num = re.search(r'(\d+\.\d+)_', SRC.split('/')[-1])
title_num = re.search(r'\b(\d\.\d)\b', title)
note('identité', 'titre <title>', title, '')
if fname_num and title_num and fname_num.group(1) != title_num.group(1):
note('identité', 'le numéro du <title> ne suit pas le nom du fichier',
'<title>', f'{title_num.group(1)} dans le titre · {fname_num.group(1)} dans le nom de fichier')
for k in ('ami:version', 'ami:date'):
m = re.search(r'<meta name="%s" content="([^"]*)"' % k, txt)
note('identité', f'meta {k}', '<head>', m.group(1) if m else 'ABSENT', 'corriger' if not m else 'ok')
# numéros de volume cités dans le corps
nums = collections.Counter(re.findall(r'Vol(?:ume|\.)\s*(\d\.\d)', txt))
note('identité', 'numéros de volume écrits dans la page', 'corps', dict(nums), 'regarder')
# ââ 1 · structure ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
VIDES = {'meta','link','br','hr','img','input','source','area','base','col','embed','param','track','wbr','path','circle','rect','line','polyline','polygon','ellipse','stop','use'}
class P(HTMLParser):
def __init__(s):
super().__init__(convert_charrefs=False); s.stack=[]; s.faults=[]; s.ids=collections.Counter(); s.idpos={}
s.maxdepth=0; s.in_svg=0; s.hrefs=[]; s.tags=collections.Counter(); s.fixed=0
def handle_starttag(s, tag, attrs):
a=dict(attrs)
if tag=='svg': s.in_svg+=1
if 'id' in a: s.ids[a['id']]+=1; s.idpos.setdefault(a['id'], s.getpos()[0])
if 'href' in a and a['href'] and a['href'].startswith('#'): s.hrefs.append((a['href'][1:], s.getpos()[0]))
s.tags[tag]+=1
if tag in VIDES: return
s.stack.append((tag, s.getpos()[0])); s.maxdepth=max(s.maxdepth,len(s.stack))
def handle_startendtag(s, tag, attrs):
a=dict(attrs)
if 'id' in a: s.ids[a['id']]+=1; s.idpos.setdefault(a['id'], s.getpos()[0])
s.tags[tag]+=1
def handle_endtag(s, tag):
if tag=='svg': s.in_svg-=1
if tag in VIDES: return
if s.stack and s.stack[-1][0]==tag: s.stack.pop(); return
# chercher plus haut
for i in range(len(s.stack)-1,-1,-1):
if s.stack[i][0]==tag:
for t,l in s.stack[i+1:]: s.faults.append(f'<{t}> ouvert l.{l} jamais fermé (fermé de force par </{tag}> l.{s.getpos()[0]})')
del s.stack[i:]; return
s.faults.append(f'</{tag}> l.{s.getpos()[0]} sans ouvrant')
p=P(); p.feed(txt)
for t,l in p.stack: p.faults.append(f'<{t}> ouvert l.{l} jamais fermé (fin de fichier)')
note('structure', 'balises équilibrées (parcours en pile, hors SVG vides)', '', p.faults if p.faults else 'aucune faute', 'corriger' if p.faults else 'ok')
dbl = {i:n for i,n in p.ids.items() if n>1}
note('structure', 'identifiants en double', {i:f'l.{p.idpos[i]}' for i in dbl} , dbl if dbl else 'aucun', 'corriger' if dbl else 'ok')
# ancres mortes â ids posĂ©s en JS aussi
js_ids = set(re.findall(r"""id\s*[:=]\s*['"]([^'"]+)['"]""", txt)) | set(re.findall(r"""\.id\s*=\s*['"]([^'"]+)['"]""", txt))
mortes = sorted({(h,l) for h,l in p.hrefs if h and h not in p.ids and h not in js_ids})
note('structure', 'ancres mortes (href="#x" sans id="x" ni id posé en JS)', [f'#{h} l.{l}' for h,l in mortes][:40], f'{len(mortes)} sur {len(p.hrefs)} liens internes', 'regarder' if mortes else 'ok')
note('structure', 'inventaire', '', {k:p.tags[k] for k in ('div','section','table','svg','script','style','button','a','h1','h2','h3','h4','img','iframe','blockquote')})
# onglets
tabs = re.findall(r'<button[^>]*class="[^"]*\btab-btn\b[^"]*"[^>]*>(.*?)</button>', txt, re.S)
tabs_txt=[H.unescape(re.sub(r'<[^>]+>',' ',t)).split() for t in tabs]
note('structure', 'onglets .tab-btn', '', [' '.join(t)[:60] for t in tabs_txt], 'ok' if 3<=len(tabs)<=7 else 'corriger')
for cls in ('tab-bar','sub-host','sbar','som','portail','regle','bande-registres','couv','ami-topbar','ami-barre','ami-pied','ami-coin','sec-header','partie-header','cadre-epist','checklist'):
n=len(re.findall(r'class="[^"]*\b%s\b'%re.escape(cls), txt))
note('anatomie', f'.{cls}', '', n, 'ok' if n else 'regarder')
# sec-num / romains
secnums = [(H.unescape(m.group(1)).strip(), lno(m.start())) for m in re.finditer(r'class="sec-num"[^>]*>(.*?)<', txt, re.S)]
note('numĂ©rotation', 'numĂ©ros de section (.sec-num) dans lâordre du fichier', '', [f'{n} l.{l}' for n,l in secnums][:80])
romains = collections.Counter(re.findall(r'>\s*((?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3}))\s*<', txt))
romains = {k:v for k,v in romains.items() if k}
note('numérotation', 'chiffres romains isolés dans une balise', '', romains, 'regarder')
# sommaires : comptes annoncés
for m in re.finditer(r'(\d+)\s*(?:parties?|chapitres?)\s*·\s*(\d+)\s*(?:entrées?|sections?)', txt):
note('numérotation', 'compte annoncé par un sommaire', f'l.{lno(m.start())}', m.group(0), 'regarder')
# ââ 2 · couleur ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
styles = [(m.start(), m.group(1)) for m in re.finditer(r'<style[^>]*>(.*?)</style>', txt, re.S)]
css_all = '\n'.join(s for _,s in styles)
inline = re.findall(r'style="([^"]*)"', txt)
scripts = '\n'.join(m.group(1) for m in re.finditer(r'<script[^>]*>(.*?)</script>', txt, re.S))
svgs = re.findall(r'<svg.*?</svg>', txt, re.S)
svg_all='\n'.join(svgs)
declared = collections.OrderedDict()
for m in re.finditer(r'(--[a-z][a-z0-9-]*)\s*:\s*([^;}]+)', css_all + '\n' + '\n'.join(inline)):
declared.setdefault(m.group(1), []).append(m.group(2).strip())
used = set(re.findall(r'var\((--[a-z0-9-]+)', css_all + ' '.join(inline) + scripts))
orph = sorted(used - set(declared))
note('couleur', 'variables employĂ©es sans ĂȘtre dĂ©clarĂ©es', '', orph if orph else 'aucune', 'corriger' if orph else 'ok')
multi = {k:sorted(set(v)) for k,v in declared.items() if len(set(v))>1}
note('couleur', 'variables déclarées avec PLUSIEURS valeurs (redéclaration, impression, cantons)', '', multi, 'regarder')
# palette : comparer les déclarations aux pierres/neutres/standard
idx={}
for pz,trois in K.PALETTE.items():
for niv,h in zip(K.NIVEAUX,trois): idx[h.lower()]=pz+niv
for h,n in K.NEUTRES.items(): idx[h.lower()]=n
std={h.lower():n for h,n in K.STANDARD.items()}
labs=[(n,C.hex2lab(h)) for h,n in idx.items()]
def nearest(h):
r,g,b=C.hex2rgb(h); lab=C.rgb2lab((r,g,b))
return min(((n,C.de2000(lab,l)) for n,l in labs), key=lambda t:t[1])
def norm_hex(h):
h=h.lower()
if len(h)==4: h='#'+''.join(c*2 for c in h[1:])
return h[:7]
pal_rows=[]
for k,vals in declared.items():
for v in sorted(set(vals)):
m=re.match(r'#([0-9a-fA-F]{3,8})\b',v)
if not m: continue
h=norm_hex('#'+m.group(1))
if h in idx: verdict=f'= {idx[h]}'
elif h in std: verdict=f'= standard {std[h]}'
else:
n,d=nearest(h); verdict=f'HORS CHARTE · le plus proche {n} ÎE {d:.1f}'
pal_rows.append(f'{k}: {h} â {verdict}')
note('couleur', 'les variables de la page, confrontées à la palette de la Charte', '', pal_rows)
# hex hors bloc de variables, par contexte
def hexes(s): return [norm_hex(x) for x in re.findall(r'#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b', s)]
css_hors = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*[^;}]+', '', css_all)
ctx = {'CSS (hors déclarations de variables)': hexes(css_hors),
'style="âŠ" en ligne': hexes(re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*[^;"]+','',' '.join(inline))),
'SVG': hexes(svg_all), 'JavaScript': hexes(scripts)}
for c,hs in ctx.items():
cnt=collections.Counter(hs)
def tag(h):
if h in idx: return idx[h]
if h in std: return 'std:'+std[h]
r,g,b=C.hex2rgb(h)
if max(r,g,b)-min(r,g,b)<=6: return 'gris'
n,d=nearest(h); return f'â{n} ÎE{d:.0f}' if d<8 else 'HORS'
note('couleur', f'hexadĂ©cimaux Ă©crits en dur â {c}', '', {h:f'{n}Ă ({tag(h)})' for h,n in cnt.most_common(60)}, 'regarder' if cnt else 'ok')
rgba = re.findall(r'rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}', css_hors)
note('couleur', 'rgb()/rgba() écrits avec un triplet en dur dans les rÚgles (au lieu de var(--x-rgb))', '', f'{len(rgba)} occurrences · {len(set(rgba))} triplets distincts : {sorted(set(rgba))[:20]}', 'regarder' if rgba else 'ok')
# fond de page
for k in ('--noir','--onyx','--encre','--voile','--or','--or-pale','--or-somb','--nacre','--gris','--gris-cl','--labradorite'):
note('couleur', f'valeur de {k}', '', declared.get(k,'â'))
# ââ 3 · forme ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
EMO = re.compile('[\U0001F300-\U0001FAFFâ-âżâŹ-âŻżêŠ -êŠż]')
ital = [(lno(m.start()), H.unescape(re.sub(r'<[^>]+>','',m.group(0)))[:60]) for m in re.finditer(r'<(i|em)\b[^>]*>(.*?)</\1>', txt, re.S) if EMO.search(m.group(2))]
note('forme', 'émoji dans une balise <i>/<em> (rÚgle absolue : jamais en italique)', [f'l.{l} « {t} »' for l,t in ital][:30], len(ital), 'corriger' if ital else 'ok')
# classes italiques contenant emoji â approximation statique : sĂ©lecteurs avec font-style:italic
ital_sel = re.findall(r'([^{}]+)\{[^{}]*font-style\s*:\s*italic', css_all)
note('forme', 'sélecteurs en font-style:italic (à confronter au rendu : un émoji dedans est une faute)', '', [s.strip()[:80] for s in ital_sel][:40], 'regarder')
vh = [lno(m.start()) for m in re.finditer(r'min-height\s*:\s*100vh', txt)]
note('forme', 'min-height:100vh (la Charte V16 lâa retirĂ© de la une)', [f'l.{l}' for l in vh], len(vh), 'regarder' if vh else 'ok')
kf = re.findall(r'@keyframes\s+([a-zA-Z0-9_-]+)\s*\{(.*?)\}\s*\}', css_all, re.S)
anims = re.findall(r'animation\s*:\s*([a-zA-Z0-9_-]+)\s+([\d.]+)s', css_all)
note('forme', 'animations (nom · durĂ©e) â rĂšgle : cycle â„ 7 s, opacitĂ© seule ou Ă©chelle †1,05, jamais de translation/rotation', '', [f'{n} {d}s' + (' â < 7 s' if float(d)<7 else '') for n,d in anims], 'regarder')
tr = [n for n,b in kf if re.search(r'translate|rotate', b)]
note('forme', 'keyframes avec translate/rotate', '', tr if tr else 'aucun', 'corriger' if tr else 'ok')
prm = txt.find('prefers-reduced-motion')
first_style = styles[0][0] if styles else 0
note('forme', 'prefers-reduced-motion', f'l.{lno(prm)}' if prm>0 else 'ABSENT', ('prĂ©sent' + (' â mais pas en tĂȘte de la premiĂšre feuille' if prm>first_style+2000 else ' â en tĂȘte')) if prm>0 else 'ABSENT', 'corriger' if prm<0 else 'regarder')
imp = re.search(r'family=([^"]+)', txt)
note('forme', 'import Google Fonts', '<head>', imp.group(1) if imp else 'ABSENT', 'ok' if imp and 'Jost' in imp.group(1) and 'Cormorant' in imp.group(1) else 'corriger')
# uppercase + Cormorant
up_cor = [s.strip()[:70] for s in re.findall(r'([^{}]+)\{[^{}]*text-transform\s*:\s*uppercase[^{}]*font-family\s*:[^;]*Cormorant', css_all)]
up_cor += [s.strip()[:70] for s in re.findall(r'([^{}]+)\{[^{}]*font-family\s*:[^;]*Cormorant[^{}]*text-transform\s*:\s*uppercase', css_all)]
note('forme', 'rĂšgles en capitales ET Cormorant dans le mĂȘme bloc (jamais de sĂ©rif sur des capitales)', '', up_cor if up_cor else 'aucune dans un mĂȘme bloc â le rendu tranchera les hĂ©ritages', 'regarder' if up_cor else 'ok')
tables = [(lno(m.start())) for m in re.finditer(r'<table\b', txt)]
tw = len(re.findall(r'class="[^"]*\btw\b', txt))
note('forme', 'tableaux', '', f'{len(tables)} <table> · {tw} conteneurs .tw', 'regarder' if tw < len(tables) else 'ok')
fixed = [lno(m.start()) for m in re.finditer(r'position\s*:\s*fixed', css_all)]
note('forme', 'position:fixed (permis en Page, interdit en Bloc)', [f'l.{l}' for l in fixed], len(fixed), 'regarder')
grids = re.findall(r'grid-template-columns\s*:\s*([^;}]+)', css_all)
note('forme', 'grilles déclarées (auto-fit/auto-fill produisent des orphelines : à vérifier au rendu)', '', collections.Counter(g.strip() for g in grids).most_common(15), 'regarder')
# ââ 4 · langue âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
body = re.sub(r'<script.*?</script>|<style.*?</style>|<svg.*?</svg>', lambda m: '\n'*m.group(0).count('\n'), txt, flags=re.S)
plain = H.unescape(re.sub(r'<[^>]+>', ' ', body))
TU = re.compile(r"(?<![\w'â])(tu|toi|ton|ta|tes)(?=[\s,.;:!?'â])", re.I)
tu_hits=[]
for i,l in enumerate(body.split('\n'),1):
if re.search(r'<blockquote|class="[^"]*(cit|axiome|devise|fable|c-dev)', l): continue
t=H.unescape(re.sub(r'<[^>]+>',' ',l))
for m in TU.finditer(t):
tu_hits.append(f'l.{i} « âŠ{t[max(0,m.start()-40):m.end()+40].strip()}⊠»')
note('langue', 'tutoiement hors citation (Ă lire : le corpus sâadresse parfois au lecteur volontairement)', tu_hits[:40], len(tu_hits), 'regarder')
MOTS = ['PsychaTrice','ParDonnance','MétaMatrice','AlgoRythme','EncycloPédie','GéoColoVoluMétrique','AstroGlobe','CosmoGonie','CosmoLogie','ConScience','CoNaissance','RéfleXion','EssenCiel','InCorporation','UniVerCelle','ThermoCognition','ReSentir','TransMission','PsychoBiologie']
ortho=collections.Counter()
for mot in MOTS:
rx=re.compile(re.escape(mot), re.I)
for m in rx.finditer(plain):
if m.group(0)!=mot and m.group(0).upper()!=m.group(0): ortho[(m.group(0),mot)]+=1
note('langue', 'orthographes arrĂȘtĂ©es Ă©crites autrement (hors titres en capitales)', '', {f'{a} â {b}':n for (a,b),n in ortho.items()}, 'regarder' if ortho else 'ok')
bulles = len(re.findall(r'class="[^"]*\b(?:def|lx-mot|bulle|tip)\b', txt)) + len(re.findall(r'<abbr|data-def=|title="', txt))
note('langue', 'porteurs de définition au survol (abbr / title / data-def / .def)', '', bulles, 'regarder')
for pat in ('Ă venir','reste Ă faire','provisoire','Ă valider','TODO','XXX','lorem'):
n=len(re.findall(pat, plain, re.I))
if n: note('honnĂȘtetĂ©', f'mention « {pat} »', '', n, 'regarder')
dates = collections.Counter(re.findall(r'\b\d{1,2}\s+(?:janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)\s+20\d\d', plain))
note('honnĂȘtetĂ©', 'dates Ă©crites dans la page', '', dict(dates.most_common(20)), 'regarder')
years = collections.Counter(re.findall(r'\b(20[12]\d)\b', plain))
note('honnĂȘtetĂ©', 'annĂ©es citĂ©es', '', dict(years.most_common(10)))
# doublons de paragraphes
paras=[H.unescape(re.sub(r'<[^>]+>','',m.group(1))).strip() for m in re.finditer(r'<p\b[^>]*>(.*?)</p>', body, re.S)]
paras=[re.sub(r'\s+',' ',x) for x in paras if len(x)>80]
dup=[(x[:110],n) for x,n in collections.Counter(paras).items() if n>1]
note('langue', f'paragraphes (>80 signes) Ă©crits deux fois ou plus â {len(dup)} doublons sur {len(paras)} paragraphes', '', [f'{n}à « {x}⊠»' for x,n in dup][:30], 'regarder' if dup else 'ok')
longs=sorted(((len(x),x[:100]) for x in paras), reverse=True)[:8]
note('langue', 'les paragraphes les plus longs (candidats « collĂ©s par lâextracteur »)', '', [f'{n} signes « {x}⊠»' for n,x in longs], 'regarder')
# caractÚres de zone privée / ligatures cassées
pua=len(re.findall(r'[î-ïŁż]', txt))
note('langue', 'caractĂšres de zone privĂ©e U+E000âF8FF', '', pua, 'corriger' if pua else 'ok')
# emoji sans FE0F pour les glyphes texte-par-défaut
for e,nom in (('âĄ','⥠(C1)'),('â','â (C6)'),('â','â'),('â','â'),('â€','â€')):
n=len(re.findall(e+'(?!ïž)', txt)); n2=len(re.findall(e+'ïž', txt))
if n: note('langue', f'{nom} : {n} sans U+FE0F · {n2} avec â deux graphies du mĂȘme signe', '', '', 'regarder')
# ââ 6 · bloc / widget ââââââââââââââââââââââââââââââââââââââââââââââââââ
racine = re.search(r'<div id="(ami-[a-z0-9-]+)"', txt)
note('bloc', 'racine', '', racine.group(1) if racine else 'aucune #ami-âŠ')
note('bloc', 'document complet ? (<html>, <head>, <body>)', '', all(t in txt for t in ('<html','<head>','<body')))
onclick=len(re.findall(r'\sonclick=', txt))
note('bloc', 'onclick= dans le HTML (fuite : exige une fonction globale)', '', onclick, 'corriger' if onclick else 'ok')
glob = re.findall(r'^\s*function\s+([A-Za-z_$][\w$]*)\s*\(', scripts, re.M)
iife = len(re.findall(r'\(function\s*\(\)\s*\{', scripts))
note('bloc', 'scripts : IIFE · fonctions déclarées au premier niveau du texte', '', f'{iife} IIFE · fonctions nommées : {sorted(set(glob))[:30]}', 'regarder')
for k in ('localStorage','sessionStorage','fetch(','XMLHttpRequest','navigator.sendBeacon','<form','postMessage','ResizeObserver','requestFullscreen'):
n=txt.count(k)
note('bloc', f'présence de {k}', '', n)
nu = [s.strip()[:60] for s in re.findall(r'(?:^|\})\s*([a-z][a-z0-9]*|\*|body|html)\s*\{', css_all)]
note('bloc', 'sélecteurs nus (non préfixés par la racine) dans les feuilles', '', collections.Counter(nu).most_common(20), 'corriger' if nu else 'ok')
allrevert = len(re.findall(r'all\s*:\s*revert', css_all))
note('bloc', 'all:revert sur la racine (perd lâhĂ©ritage de la taille : rappeler la formule du zoom)', '', allrevert, 'regarder')
zoom = len(re.findall(r'--zoom', txt))
note('bloc', 'formule du zoom (--zoom)', '', zoom, 'regarder' if not zoom else 'ok')
size = len(raw)/1e6
note('bloc', 'poids du fichier', '', f'{size:.2f} Mo â {len(re.findall("data:image",raw))} images base64 ({(len(raw)-len(txt))/1e6:.2f} Mo)', 'regarder' if size>3 else 'ok')
json.dump(R, open('audit_statique.json','w'), ensure_ascii=False, indent=1, default=str)
for fam,items in R.items():
print('\n## '+fam)
for it in items:
g={'corriger':'â','regarder':'?','ok':'â'}.get(it['grave'],'·')
print(f"{g} {it['quoi']} â {it['ou'] if it['ou'] else ''} :: {it['mesure']}")
audit_rendu.py audit_rendu.py page.html138 lignes
Chromium : ouvre tous les onglets et sous-onglets ; mesure les contrastes sur le fond rĂ©el composĂ©, le texte < 11,5 px, les Ă©mojis en italique, les cibles < 24 px, les tableaux sans rouleau, les grilles Ă orphelines, les dĂ©bordements Ă 320/390/768/1280 â audit_rendu.json
â§ Copier le script
# -*- coding: utf-8 -*-
"""Audit au rendu (Chromium) â on mesure ce que le navigateur FAIT, tous
les onglets ouverts, Ă plusieurs largeurs. Ne corrige rien."""
import sys, json, collections, time
from playwright.sync_api import sync_playwright
SRC = sys.argv[1]
URL = 'file://' + __import__('os').path.abspath(SRC)
TOPS = sys.argv[2] if len(sys.argv) > 2 else '.tab-btn-full, .tab-bar .tab-btn' # les onglets de premier niveau de la page
JS_MESURE = r"""
(opts) => {
const seen = opts.seen; const out = {texte:[], petits:[], ital:[], cibles:[], tables:[], grilles:[], nb:0};
const EMO = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{A9A0}-\u{A9FF}]/u;
function key(el){ const p=[]; let e=el; while(e && e.nodeType===1 && e!==document.body){ let i=0,s=e; while((s=s.previousElementSibling)) i++; p.push(e.tagName+i); e=e.parentElement;} return p.join('/'); }
function vis(el){ const r=el.getBoundingClientRect(); if(r.width<1||r.height<1) return false; const cs=getComputedStyle(el); if(cs.visibility==='hidden'||cs.display==='none'||parseFloat(cs.opacity)===0) return false; return true; }
function parse(c){ const m=c.match(/rgba?\(([^)]+)\)/); if(!m) return null; const a=m[1].split(',').map(parseFloat); return {r:a[0],g:a[1],b:a[2],a:a.length>3?a[3]:1}; }
function lum({r,g,b}){ const f=v=>{v/=255; return v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055,2.4)}; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
function comp(top,bot){ const a=top.a; return {r:top.r*a+bot.r*(1-a), g:top.g*a+bot.g*(1-a), b:top.b*a+bot.b*(1-a), a:1}; }
function bgOf(el){ // compose les fonds translucides jusqu'au premier opaque
const layers=[]; let e=el;
while(e && e.nodeType===1){ const cs=getComputedStyle(e); const c=parse(cs.backgroundColor); const img=cs.backgroundImage;
if(c && c.a>0){ layers.push(c); if(c.a>=1) break; }
if(img && img!=='none' && /gradient/.test(img)){ layers.push({r:9,g:9,b:14,a:1, grad:true}); break; }
e=e.parentElement; }
if(!layers.length || layers[layers.length-1].a<1) layers.push({r:9,g:9,b:14,a:1, fallback:true});
let bg=layers[layers.length-1]; for(let i=layers.length-2;i>=0;i--) bg=comp(layers[i],bg);
return {bg, grad: layers.some(l=>l.grad), fallback: layers.some(l=>l.fallback)};
}
function ratio(fg,bg){ const l1=lum(fg), l2=lum(bg); return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05); }
const walker=document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while((n=walker.nextNode())){
const t=n.textContent.replace(/\s+/g,' ').trim(); if(t.length<2) continue;
const el=n.parentElement; if(!el || ['SCRIPT','STYLE','NOSCRIPT','TITLE'].includes(el.tagName)) continue;
if(el.closest('svg')) continue;
if(!vis(el)) continue;
const k=key(el)+'|'+t.slice(0,40); if(seen[k]) continue; seen[k]=1; out.nb++;
const cs=getComputedStyle(el); const fs=parseFloat(cs.fontSize); const fw=parseInt(cs.fontWeight)||400;
const fg=parse(cs.color); const B=bgOf(el); const r=ratio(comp(fg,B.bg),B.bg);
const large = fs>=24 || (fs>=18.66 && fw>=700);
const seuil = large?3:4.5;
const tag=el.tagName.toLowerCase()+(el.className&&typeof el.className==='string'?'.'+el.className.trim().split(/\s+/).slice(0,2).join('.'):'');
if(r<seuil) out.texte.push({tag, t:t.slice(0,70), fs:+fs.toFixed(1), fw, r:+r.toFixed(2), seuil, fg:cs.color, bg:`rgb(${B.bg.r|0},${B.bg.g|0},${B.bg.b|0})`, grad:B.grad});
if(fs<11.5) out.petits.push({tag, t:t.slice(0,50), fs:+fs.toFixed(1)});
if(cs.fontStyle==='italic' && EMO.test(t)) out.ital.push({tag, t:t.slice(0,60)});
if(cs.textTransform==='uppercase' && /Cormorant/i.test(cs.fontFamily)) out.texte.push({tag, t:t.slice(0,50), capsCormorant:true, fs:+fs.toFixed(1)});
}
document.querySelectorAll('a[href],button,[role=button],[role=tab]').forEach(el=>{
if(!vis(el)) return; const k='c|'+key(el); if(seen[k]) return; seen[k]=1;
const r=el.getBoundingClientRect(); if(r.width<24||r.height<24) out.cibles.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], t:(el.textContent||'').trim().slice(0,30), w:+r.width.toFixed(1), h:+r.height.toFixed(1)});
});
document.querySelectorAll('table').forEach(tb=>{
if(!vis(tb)) return; const k='t|'+key(tb); if(seen[k]) return; seen[k]=1;
let e=tb.parentElement, roule=false; while(e && e!==document.body){ const o=getComputedStyle(e).overflowX; if(o==='auto'||o==='scroll'){roule=true;break;} e=e.parentElement; }
const r=tb.getBoundingClientRect(); const pr=tb.parentElement.getBoundingClientRect();
if(r.width>pr.width+1 && !roule) out.tables.push({t:(tb.textContent||'').trim().slice(0,50), w:+r.width.toFixed(0), pw:+pr.width.toFixed(0)});
});
document.querySelectorAll('*').forEach(el=>{
const cs=getComputedStyle(el); if(cs.display!=='grid'||!vis(el)) return; const k='g|'+key(el); if(seen[k]) return; seen[k]=1;
const cols=cs.gridTemplateColumns.split(' ').length; const kids=[...el.children].filter(vis).length;
if(cols>1 && kids>cols && kids%cols!==0) out.grilles.push({cls:(el.className||'').toString().trim().slice(0,40), cols, kids, orphelines: kids%cols});
});
return out;
}
"""
JS_OVERFLOW = r"""
() => { const d=document.documentElement; const w=d.clientWidth; const bad=[];
document.querySelectorAll('body *').forEach(el=>{ if(el.closest('svg')) return; const r=el.getBoundingClientRect(); if(r.width<1) return;
if(r.right>w+1 || r.left<-1){ let e=el.parentElement, clip=false; while(e){ const o=getComputedStyle(e).overflowX; if(o==='hidden'||o==='clip'||o==='auto'||o==='scroll'){clip=true;break;} e=e.parentElement;}
bad.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], right:+r.right.toFixed(0), left:+r.left.toFixed(0), clip}); } });
return {scrollW:d.scrollWidth, clientW:w, n:bad.length, ex:bad.slice(0,12)}; }
"""
def ouvre_tout(page, seen, acc):
"""Clique chaque onglet et chaque sous-onglet, mesure aprÚs chaque état."""
def mesure():
o = page.evaluate(JS_MESURE, {'seen': seen})
for k in ('texte','petits','ital','cibles','tables','grilles'): acc[k].extend(o[k])
acc['nb'] += o['nb']
mesure()
tops = page.query_selector_all(TOPS)
for b in tops:
try: b.click(timeout=2000); page.wait_for_timeout(150)
except Exception as e: acc['clics_rates'].append(str(e)[:80])
mesure()
# sous-onglets visibles
subs = page.query_selector_all('button')
for s in subs:
try:
if not s.is_visible(): continue
cls = (s.get_attribute('class') or '')
if 'tab-btn-full' in cls or 'pt-tab' in cls or 'som' in cls: continue
if any(x in cls for x in ('tri-b','tb','tab','onglet','nav','btn-t','sb','sp-h','pt-h','psb-')):
s.click(timeout=1500); page.wait_for_timeout(120); mesure()
except Exception: pass
# sommaires : on les ouvre
for h in page.query_selector_all('.som-h, .som-hd, [aria-expanded]'):
try:
if h.is_visible(): h.click(timeout=800)
except Exception: pass
mesure()
with sync_playwright() as p:
br = p.chromium.launch()
res = {}
for W,H in ((390,844),(1280,900)):
ctx = br.new_context(viewport={'width':W,'height':H}, device_scale_factor=1)
page = ctx.new_page()
errs=[]; page.on('pageerror', lambda e: errs.append(str(e)[:200])); page.on('console', lambda m: errs.append('console.'+m.type+': '+m.text[:160]) if m.type in ('error','warning') else None)
page.goto(URL, wait_until='load'); page.wait_for_timeout(1200)
seen={}; acc=dict(texte=[],petits=[],ital=[],cibles=[],tables=[],grilles=[],nb=0,clics_rates=[])
ouvre_tout(page, seen, acc)
ov = page.evaluate(JS_OVERFLOW)
# fonts réellement chargées ?
fonts = page.evaluate("() => [...document.fonts].filter(f=>f.status==='loaded').map(f=>f.family+' '+f.weight+' '+f.style)")
res[W] = dict(erreurs=errs, mesures=acc, overflow=ov, fonts=sorted(set(fonts)))
page.screenshot(path=f'capture_{W}.png', full_page=False)
ctx.close()
for W in (320,768):
ctx = br.new_context(viewport={'width':W,'height':800}); page=ctx.new_page(); page.goto(URL, wait_until='load'); page.wait_for_timeout(800)
for b in page.query_selector_all(TOPS):
try: b.click(timeout=1500); page.wait_for_timeout(100)
except Exception: pass
res[W]=dict(overflow=page.evaluate(JS_OVERFLOW)); ctx.close()
br.close()
json.dump(res, open('audit_rendu.json','w'), ensure_ascii=False, indent=1)
for W,r in res.items():
print(f'\n=== {W} px ===')
if 'erreurs' in r:
print('erreurs JS/console :', len(r['erreurs'])); [print(' ',e) for e in r['erreurs'][:10]]
m=r['mesures']; print('nĆuds texte mesurĂ©s :', m['nb'], '· clics ratĂ©s :', len(m['clics_rates']))
print('contraste sous seuil :', len([x for x in m['texte'] if not x.get('capsCormorant')]), '· caps en Cormorant :', len([x for x in m['texte'] if x.get('capsCormorant')]))
print('texte < 11,5 px :', len(m['petits']), '· émoji en italique :', len(m['ital']), '· cibles < 24 px :', len(m['cibles']), '· tableaux qui débordent sans rouleau :', len(m['tables']), '· grilles à orphelines :', len(m['grilles']))
print('polices chargées :', r['fonts'])
print('overflow :', r['overflow']['scrollW'], '/', r['overflow']['clientW'], '· éléments hors cadre :', r['overflow']['n'], r['overflow']['ex'][:5])
p1_texte.py p1_texte.py179 lignes
Les gestes de texte d'une page, chacun comptĂ© par assert (identitĂ©, liens, libellĂ©s, redites â renvois, notation). Ă réécrire pour chaque page : c'est la partie qui se lit Ă la main.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · Texte & structure â 0.3 L'Union du TouT V21 â V22.
Chaque remplacement dit combien de fois il doit trouver sa cible ; sinon le script s'arrĂȘte sans Ă©crire."""
import re, sys
SRC, DST = 'v22.html', 'v22.html'
h = open(SRC, encoding='utf-8').read()
J = []
def rep(old, new, n=1, flags=0, why=''):
global h
if flags:
c = len(re.findall(old, h, flags))
else:
c = h.count(old)
if c != n:
raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = re.sub(old, new, h, flags=flags) if flags else h.replace(old, new)
J.append(f'{why or old[:60]} Ă{n}')
# ââ A·1 / A·2 · identitĂ© : le numĂ©ro 0.3 lu depuis une source ââââââââââ
rep('<meta name="viewport" content="width=device-width,initial-scale=1">',
'<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="ami:volume" content="0.3">\n<meta name="ami:version" content="V22">\n<meta name="ami:date" content="2026-09-09">',
why='metas ami:volume / version / date')
rep("<title>đ 0.1 L'Union du TouT", "<title>đ 0.3 L'Union du TouT", why='title 0.3')
rep('<div class="vol-label">Protocole de ConScience aMi · Volume 0.1</div>',
'<div class="vol-label">Protocole de ConScience aMi · Volume <span data-ami="volume">0.3</span></div>', why='vol-label 0.3')
rep('aria-label="Cinq parties du Volume 0.1"', 'aria-label="Cinq parties du Volume 0.3"', why='aria-label 0.3')
rep("â Sommaire â Vol. 0.1 · L'Union du TouT", "â Sommaire â Vol. <span data-ami=\"volume\">0.3</span> · L'Union du TouT", why='sommaire 0.3')
rep("<p class=\"footer-nav\">đ 0.1 L'Union du TouT", "<p class=\"footer-nav\">đ <span data-ami=\"volume\">0.3</span> L'Union du TouT", why='pied 0.3')
# ââ A·3 / A·4 / A·5 / A·6 · coquilles et comptes ââââââââââââââââââââââ
rep("l'ThermoCognition le dit", "la ThermoCognition le dit", why="l'ThermoCognition")
rep('Les 11 notions du consentement', 'Les 12 notions du consentement', why='nav 11â12')
rep('Trois lectures des mĂȘmes onze notions', 'Trois lectures des mĂȘmes douze notions', why='h2 onzeâdouze')
rep('Une fois les onze notions passĂ©es en revue', 'Une fois les douze notions passĂ©es en revue', why='onzeâdouze')
rep('Elle est passĂ©e au volet 4, oĂč elle scelle', 'Elle est passĂ©e au cinquiĂšme volet, oĂč elle scelle', why='volet 4â5')
rep("la Fable ferme le troisiĂšme volet, et non le deuxiĂšme", "la Fable ferme le cinquiĂšme volet, et non le deuxiĂšme", why='troisiĂšmeâcinquiĂšme volet')
rep("<b>on entre</b> (l'Apokalypse), <b>on établit sur quoi l'on se base</b> (les Lois de La Vie), <b>on dit ce que l'on veut</b> (les Chartes du Vivant), <b>puis on s'engage</b>.",
"<b>on entre</b> (la Quintessence), <b>on établit sur quoi l'on se base</b> (les Lois de La Vie), <b>on dit ce que l'on veut</b> (les Chartes du Vivant), <b>on apprend à le dire</b> (la Communication), <b>puis on s'engage</b>.", why='ordre des cinq volets')
rep('négentropique', 'néguentropique', n=3, why='négentropique')
rep('négentropie', 'néguentropie', n=1, why='négentropie')
# ââ A·7 · le caractĂšre parasite U+030B âââââââââââââââââââââââââââââââââ
c = h.count('Ì'); assert c == 2, c
h = h.replace('<h4>Ì Le Point ZĂ©ro', '<h4><span class="pz">â</span> Le Point ZĂ©ro').replace('<span class="pz">Ì</span>', '<span class="pz">â</span>'); J.append('U+030B â â (glyphe du Point ZĂ©ro, Ă valider) Ă2')
assert 'Ì' not in h
# ââ A·8 · les deux </b> sans ouvrant ââââââââââââââââââââââââââââââââââ
rep("<b>ce qui t'est arrivé</b>. »</b></em>", "<b>ce qui t'est arrivé</b>. »</em>", why='</b> orphelin 1')
rep("<b>ce que tu as fait subir</b>. »</b></em>", "<b>ce que tu as fait subir</b>. »</em>", why='</b> orphelin 2')
# ââ A·9 / B·20 · les liens et l'adresse unique des contrats ââââââââââââ
rep('<a href="../Initiation_aMi_DONNER_VIE_V17.html#pan-contrat">đâïž Le Contrat Tacite Explicite</a>, en toit de <b>L\'Initiation</b>, et dans <a href="../Le_Respect_La_Loi_du_Reel_aMi.html#pv-contrat">Le Respect · La Loi du RĂ©el</a>.',
'<a href="Initiation_aMi_DONNER_VIE_V18.html#pan-contrat">đâïž Le Contrat Tacite Explicite</a>, l\'onglet de <b>L\'Initiation</b> qui leur est dĂ©diĂ©.', why='contrats : une adresse (intro)')
rep('<b>Le Contrat Tacite Explicite</b>, dans <a href="../Le_Respect_La_Loi_du_Reel_aMi.html#pv-contrat">Le Respect · La Loi du Réel</a>.',
'<a href="Initiation_aMi_DONNER_VIE_V18.html#pan-contrat"><b>Le Contrat Tacite Explicite</b></a>, l\'onglet de <b>L\'Initiation</b> qui leur est dédié.', why='contrats : une adresse (chapitre)')
# ââ B·5 · la phrase d'IA âââââââââââââââââââââââââââââââââââââââââââââââ
rep("<p style=\"margin:.7rem 0 0;color:var(--gris);font-size:.85rem\">Ces articles sont dans le texte source et mĂ©ritent d'ĂȘtre dĂ©veloppĂ©s ici â dis-moi si tu veux que je les reprenne intĂ©gralement depuis le PDF.</p>", '', why='phrase IA retirĂ©e')
# ââ B·4 · le tutoiement adressĂ© Ă l'autrice ââââââââââââââââââââââââââââ
rep("<strong>Tes polarités sont donc les trois axes du systÚme visuel humain.</strong> Quand tu poses la Nature contre la Culture en vert contre rouge, tu utilises l'axe a*",
"<strong>Les polarités de la Quintessence sont donc les trois axes du systÚme visuel humain.</strong> Poser la Nature contre la Culture en vert contre rouge, c'est employer l'axe a*", why='tutoiement adressé (LabCIE)')
# ââ B·15 / B·16 · celleux, dĂ©chĂ©ance ââââââââââââââââââââââââââââââââââ
rep('celleux qui osent parler', 'les personnes qui osent parler'); rep('celleux qui ne font rien', 'les personnes qui ne font rien'); rep('celleux qui crachent', 'les personnes qui crachent')
rep("C'est le cas de celleux qui ont besoin des autres", "C'est le cas des personnes qui ont besoin des autres")
rep('<small>on déchéance</small>', '<small>on déchoit</small>'); rep('<b>En violence : on déchéance</b>', '<b>En violence : on déchoit</b>')
# ââ B·13 · V%ler, voulu â glosĂ© et dĂ©fini au survol âââââââââââââââââââ
rep('<div class="mag-nom">V%ler</div>', '<div class="mag-nom"><span class="def" title="Voler & violer dans un seul mot : la transaction psychophysique se lit dans le mot lui-mĂȘme.">V%ler</span> <small class="mag-glose">(voler & violer)</small></div>', why='V%ler glosĂ© (Table I)')
rep('<div class="champ-arme">V%ler : L\'Arme de La Force du DĂ©lire', '<div class="champ-arme"><span class="def" title="Voler & violer dans un seul mot : la transaction psychophysique se lit dans le mot lui-mĂȘme.">V%ler</span> (voler & violer) : L\'Arme de La Force du DĂ©lire', why='V%ler glosĂ© (Table III)')
# ââ B·8 / B·9 / B·10 / B·21 / A·11 · Tables Magistrales : une source, des noms tranchĂ©s
rep('Création & Procréation', 'Création & ProCréation', n=3, why='ProCréation dans les Tables')
rep('Conscience & Gnose', 'Conscience & Science', n=2, why='GnoseâScience (I et II)')
rep("L'Art de Sublimer la Nutrition", "L'Art de La Nutrition", n=1, why='Nutrition')
rep("L'Arme de L'Industrie", "L'Arme de L'Obsolescence", n=1, why='IndustrieâObsolescence')
rep("L'Arme de La Destruction", "L'Arme de Destruction", n=1, why='Destruction (I)')
rep("L'Art de Faire NaĂźtre", "L'Art de ProCrĂ©ation", n=1, why='Faire NaĂźtreâProCrĂ©ation (I)')
rep("L'Art de Faire</div>", "L'Art de ProCrĂ©ation</div>", n=1, why='FaireâProCrĂ©ation (III)')
rep('La Langue de Bois</div>', 'La Langue de Bois / le Charabia</div>', n=2, why='Charabia (I et II)')
for a, b in (("L'Arme de l'Aliénation", "L'Arme de L'Aliénation"), ("L'Arme de l'Impunité", "L'Arme de L'Impunité"), ("L'Arme de l'Intoxication", "L'Arme de L'Intoxication"), ("L'Arme de l'Oubli", "L'Arme de L'Oubli"), ("L'Arme de la Disgrùce", "L'Arme de La Disgrùce")):
h = h.replace(a, b)
J.append('majuscule au dĂ©terminant (Arme de LââŠ)')
# la numérotation de la Table I suit celle des Tables II et III (les deux extras en 17 et 18)
renum = {'06':'17','07':'06','08':'07','09':'08','10':'09','11':'10','12':'18','13':'11','14':'12','15':'13','16':'14','17':'15','18':'16'}
def renum_I(m):
return m.group(1) + renum.get(m.group(2), m.group(2)) + m.group(3)
seg_a, seg_b = h.find('â I â La Table Magistrale'), h.find('â II â Table 16 Ă 8')
assert 0 < seg_a < seg_b
seg = h[seg_a:seg_b]
n_before = len(re.findall(r'(<strong>\S+ )(\d\d)(</strong>)', seg))
seg = re.sub(r'(<strong>\S+ )(\d\d)(</strong>)', renum_I, seg)
assert n_before == 18, n_before
h = h[:seg_a] + seg + h[seg_b:]; J.append('Table I renumĂ©rotĂ©e comme II et III (extras en 17-18) Ă18')
# « Champ » â « Domaine » dans les Tables ; « 8 Lois du Vivant » â « 8 Lois Culturelles »
rep('<div class="mn">Champ ', '<div class="mn">Domaine ', n=16, why='Table II : Domaine')
rep(r'(<span class="champ-n"[^>]*>)Champ ', r'\1Domaine ', n=18, flags=re.S, why='Table III : Domaine')
rep('Chaque champ Ă chacune des 8 Lois du Vivant. Si... Alors... universel.', 'Chaque domaine Ă chacune des 8 Lois Culturelles â les Commandements. Si⊠alors⊠universel.', why='8 Lois Culturelles (II)')
rep('16 Champs Ă 8 Lois = <strong style="color:var(--or);">128 Commandements</strong>', '16 Domaines Ă 8 Lois Culturelles = <strong style="color:var(--or);">128 Commandements</strong>', why='16 Domaines')
rep('â II â Table 16 Ă 8 · 128 Commandements du Vivant', 'â II â Table 16 Ă 8 · 128 Commandements du Vivant â les 8 Lois Culturelles', why='titre Table II')
c = h.count('Déployer les 18 Champs à 8 Lois'); assert c == 1; h = h.replace('Déployer les 18 Champs à 8 Lois', 'Déployer les 18 Domaines à 8 Lois Culturelles'); J.append('Table III : Domaines à Lois Culturelles')
rep('18 paires ArmeâșArt qui structurent les 128 Commandements du Vivant.', '18 paires ArmeâșArt qui structurent les 128 Commandements du Vivant â les 8 Lois Culturelles, face aux 7 Lois Naturelles de l\'onglet 2.', why='pont Lois Culturelles/Naturelles')
# les 7 Lois du Vivant deviennent les 7 Lois Naturelles du Vivant (titres)
rep('Les 7 Lois du Vivant', 'Les 7 Lois Naturelles du Vivant', n=5, why='7 Lois Naturelles du Vivant (sommaire, nav, h3, h2, triade)')
# ââ B·11 · les redites entre cantons : on garde une fois, on renvoie ââ
# (a) Diagnostic opposable / souverain : la maison est le Diagnostic ; la Charte Mathématique renvoie
old_a = h[h.find('<h4>MĂ©thode clinique, statut souverain</h4>'):h.find("<em>On ne dit la vĂ©ritĂ© sur soi que dans un espace oĂč cette vĂ©ritĂ© ne sera pas saisie.</em></p>")+len("<em>On ne dit la vĂ©ritĂ© sur soi que dans un espace oĂč cette vĂ©ritĂ© ne sera pas saisie.</em></p>")]
assert old_a.count('<table>') == 1 and len(old_a) < 2000, len(old_a)
h = h.replace(old_a, '<p class="renvoi">â <b>MĂ©thode clinique, statut souverain</b> â ce que cette charte refuse n\'est pas le diagnostic, c\'est son statut opposable. La distinction est posĂ©e une fois, au <a href="#diag-souverain">Diagnostic · Le statut de la lecture</a>.</p>')
rep('<details class="cm2-sec" open><summary><span class="cm2-em">đ</span><span class="cm2-t"><span class="cm2-lab2">â
· Le statut de la lecture</span>',
'<details class="cm2-sec" open id="diag-souverain"><summary><span class="cm2-em">đ</span><span class="cm2-t"><span class="cm2-lab2">â
· Le statut de la lecture</span>', why='ancre diag-souverain')
J.append('redite (a) diagnostic opposable â renvoi')
# (b) la balance qui ne pĂšse rien : la maison est l'Arborescence (le Poids) ; le Jardin renvoie
rep('<h4>Et quand la balance ne pĂšse rien</h4>\n<p>Tout ceci suppose quâon <em>ressente</em>.', '<h4>Et quand la balance ne pĂšse rien</h4>\n<p class="renvoi">Le cas est traitĂ© une fois, au <a href="#arbo-balance">Poids · le quatriĂšme cas</a> : ne rien sentir n\'est pas une preuve d\'innocence â la balance intĂ©rieure ne sert plus, on passe par l\'extĂ©rieur.</p><p hidden>Tout ceci suppose quâon <em>ressente</em>.', why='redite (b) balance')
# retirer le paragraphe caché (on le supprime entiÚrement)
i = h.find('<p hidden>Tout ceci suppose'); j = h.find('</p>', i) + 4
h = h[:i] + h[j:]
rep('<div class="note eth"><b>Le quatriĂšme cas â quand la balance ne pĂšse rien.</b>', '<div class="note eth" id="arbo-balance"><b>Le quatriĂšme cas â quand la balance ne pĂšse rien.</b>', why='ancre arbo-balance')
# (c) l'acrasie : la maison est le Jardin §II ; le raccord de l'Arborescence renvoie en une ligne
old_c = h[h.find('<div class="note key"><b>đ± Le Jardin ne peut pas voir l\'acrasie tout seul.</b>'):]
old_c = old_c[:old_c.find('</div>')+6]
assert 'indice d\'acrasie' in old_c and len(old_c) < 700
h = h.replace(old_c, '<div class="note key"><b>đ± Le Jardin ne peut pas voir l\'acrasie tout seul</b> â il mesure des dispositions dĂ©clarĂ©es, C7 · La Trace comble l\'angle mort. L\'indice et ses trois cas sont posĂ©s une fois, au <a href="#jardin-acrasie">Jardin · L\'Indice d\'Acrasie</a>.</div>')
rep('<details class="cm2-sec"><summary><span class="cm2-em">âïž</span><span class="cm2-t"><span class="cm2-lab2">â
Ą · Lâangle mort, et son remĂšde</span>', '<details class="cm2-sec" id="jardin-acrasie"><summary><span class="cm2-em">âïž</span><span class="cm2-t"><span class="cm2-lab2">â
Ą · Lâangle mort, et son remĂšde</span>', why='ancre jardin-acrasie')
J.append('redite (c) acrasie â renvoi')
# (d) jauger / juger : la maison est « Les mots à leur juste place » ; la Porte renvoie
old_d = h[h.find('<p style="font-size:.92rem"><b>Ce qui se juge, et ce qui se jauge.</b>'):]
old_d = old_d[:old_d.find('puis il refuse de se faire juger.</b></p>')+len('puis il refuse de se faire juger.</b></p>')]
assert old_d.count('<p') == 2 and len(old_d) < 1200, len(old_d)
h = h.replace(old_d, '<p style="font-size:.92rem;margin-bottom:0" class="renvoi"><b>Ce qui se juge, et ce qui se jauge.</b> On juge une seule chose â la violence ; tout le reste se jauge. Le complexe d\'infĂ©rioritĂ© Ă©choue aux deux : il refuse d\'abord de jauger, puis de se faire juger. Les deux gestes sont posĂ©s une fois, aux <a href="#arbo-jauger">mots Ă leur juste place · Jauger & Juger</a>.</p>')
rep('<div class="card or"><h4>âïž JAUGER & JUGER â deux gestes, jamais le mĂȘme</h4>', '<div class="card or" id="arbo-jauger"><h4>âïž JAUGER & JUGER â deux gestes, jamais le mĂȘme</h4>', why='ancre arbo-jauger')
J.append('redite (d) jauger/juger â renvoi')
# (e) les deux garde-fous : la maison est le chapitre des Pactes ; l'intro renvoie
old_e = h[h.find('<div class="alerte"><strong>Les deux garde-fous, valables pour les quatre.</strong>'):]
old_e = old_e[:old_e.find('</div>')+6]
assert 'révocable' in old_e and len(old_e) < 900
h = h.replace(old_e, '<div class="alerte"><strong>Les deux garde-fous, valables pour les quatre</strong> â un pacte doit <strong>ouvrir</strong>, et il doit ĂȘtre <strong>rĂ©vocable</strong>. Ils sont posĂ©s une fois, en tĂȘte des <a href="#ctr-gardefous">Pactes du Respect</a>.</div>')
rep('<b>â Les deux garde-fous, avant tout le reste.</b>', '<b id="ctr-gardefous">â Les deux garde-fous, avant tout le reste.</b>', why='ancre ctr-gardefous')
J.append('redite (e) garde-fous â renvoi')
# (f) la signature écrite deux fois : elle reste à la Fable, qui scelle
rep('<blockquote><p>En Sachant Qui Je Suis Vraiment<br>đȘ Rejoins Moi pour Radicalement ProspĂ©rer đïž<br>âïž Entre Respect & VĂ©ritĂ©. âïž</p></blockquote>\n</div>', '</div>', why='redite (f) signature (intro) retirĂ©e')
# ââ B·12 · C pour l'indice, Ί pour la constante (canton Charte MathĂ©matique) â
seg_a, seg_b = h.find('đ« Fractale · Charte Physique Structurelle</div><div class="bs">'), h.find('đł Arborescence · Charte Psychique Imaginative</div><div class="bs">')
assert 0 < seg_a < seg_b
seg = h[seg_a:seg_b]
# on ne touche qu'au texte, jamais aux scripts
parts = re.split(r'(<script.*?</script>)', seg, flags=re.S)
n = 0
for k in range(0, len(parts), 2):
p = parts[k]
for old, new in (
('Ί est un rapport : il est sans dimension. Le Dian, lui, s\'accumule dans le temps.', 'C est un rapport : il est sans dimension. Le Dian, lui, s\'accumule dans le temps.'),
('Ί dépend du coût du refus : il n\'est pas une propriété du systÚme seul.', 'C dépend du coût du refus : il n\'est pas une propriété du systÚme seul.'),
('Ί = ÎI / ÎE = (U Ă I) / (R Ă P) Ă t', 'C = ÎI / ÎE = (U Ă I) / (R Ă P) Ă t'),
('<b>Ί</b> · la Cohérence, la Superfluidité', '<b>C</b> · la Cohérence, la Superfluidité'),
('mĂȘmes entrĂ©es, mĂȘme Ί, Ă toute heure', 'mĂȘmes entrĂ©es, mĂȘme C, Ă toute heure'),
('Ί répond à une question précise', 'C répond à une question précise'),
('Ί · Cohérence<', 'C · Cohérence<'),
('Ί haut · RS haut', 'C haut · RS haut'), ('Ί bas · RS bas', 'C bas · RS bas'), ('Ί bas · QS haut', 'C bas · QS haut'), ('Ί bas · QS bas · RS haut', 'C bas · QS bas · RS haut'),
('Ίâ = ÎI / (ÎI + ÎE)', 'Câ = ÎI / (ÎI + ÎE)'),
('<p>Ί est un rapport : il n\'a pas d\'unité.', '<p>C est un rapport : il n\'a pas d\'unité.'),
('D = ⫠Ί(t) dt', 'D = ⫠C(t) dt'), ('Ί est au Dian ce que le Watt', 'C est au Dian ce que le Watt'),
('La Relation a trois Ί', 'La Relation a trois C'),
('Ί<sub>', 'C<sub>'),
('Ί de A', 'C de A'), ('Ί de B', 'C de B'), ('Ί du lien', 'C du lien'),
('deux personnes de mĂȘme Ί mais de QS', 'deux personnes de mĂȘme C mais de QS'),
('Ί · CohĂ©rence â l\'instant', 'C · CohĂ©rence â l\'instant'), ('Ί + RS croisĂ©s', 'C + RS croisĂ©s'),
):
c = p.count(old)
if c: p = p.replace(old, new); n += c
parts[k] = p
seg = ''.join(parts)
h = h[:seg_a] + seg + h[seg_b:]
J.append(f'C pour lâindice, Ί pour la constante (Charte MathĂ©matique) Ă{n}')
assert n >= 28, n
# la phrase qui le dit une fois, en tĂȘte du chapitre â
Š
rep('Ce que la conscience fait, une Ă©quation le mesure', 'Ce que la conscience fait, une Ă©quation le mesure â C est l\'indice, Ί la constante qui lui sert d\'Ă©chelle', why='C/Ί dit une fois')
# ââ les styles des nouveautĂ©s : .renvoi, .def, .mag-glose âââââââââââââ
rep('#ami-union *{margin:0;padding:0;box-sizing:border-box}', '#ami-union *{margin:0;padding:0;box-sizing:border-box}\n#ami-union .renvoi{font-size:.92rem;color:var(--gris-cl);border-left:2px solid var(--or-somb);padding-left:.8rem;margin:.8rem 0}\n#ami-union .renvoi a,#ami-union .note a,#ami-union .alerte a{color:var(--or-pale);text-decoration:none;border-bottom:1px dotted var(--or)}\n#ami-union .def{color:var(--or);font-weight:500;border-bottom:1px dotted var(--or);cursor:help}\n#ami-union .mag-glose{display:block;font-size:.72rem;color:var(--gris-cl);font-weight:300}', why='styles renvoi/def')
open(DST, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J)); print(len(J), 'gestes')
p2_forme.py p2_forme.py96 lignes
Forme et accessibilitĂ© : fond, 100vh, animations â„ 7 s, reduced-motion global, or-sombre â gris quand il sert d'encre, Ă©mojis hors italique, tableaux dans .tw, role=tablist, clavier, un lien #ancre ouvre son onglet.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · Forme & lisibilitĂ© â Ă©dits statiques. Le plancher de taille est fait par p2_plancher.py (au rendu)."""
import re
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# A·12 · le fond de page est l'onyx
rep('body{margin:0;padding:22px;background:#07070c;min-height:100vh}', 'body{margin:0;padding:22px;background:#09090e;min-height:100vh}', why='body â onyx')
# A·23 · 100vh retiré de la couverture
rep('#ami-union .couverture{min-height:100vh;', '#ami-union .couverture{', why='100vh couverture')
# A·24 · animations ℠7 s
rep('animation:amipulse 4s ease-in-out infinite', 'animation:amipulse 11s ease-in-out infinite', why='logo 4 s â 11 s')
rep('animation:amipulse 6s ease-in-out infinite', 'animation:amipulse 11s ease-in-out infinite', why='Ă©toile 6 s â 11 s')
rep('animation:amiPulse 3.2s ease-in-out infinite', 'animation:amiPulse 7s ease-in-out infinite', why='point zĂ©ro 3,2 s â 7 s')
# A·25 · prefers-reduced-motion global, en tĂȘte de la premiĂšre feuille
rep('<style>\n#ami-union{', '<style>\n@media(prefers-reduced-motion:reduce){#ami-union *,#ami-union *::before,#ami-union *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}\n#ami-union{', why='reduced-motion global en tĂȘte')
# A·26 · import Google Fonts canonique
rep('family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Jost', 'family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&family=Jost', why='import fonts 1,600')
# A·13 · --or-somb n'est pas une encre sous 22 px
for sel, old, new, *k in (
('.cmd-loi', "color:var(--or-somb);margin-bottom:.3rem;display:block;}", "color:var(--gris-cl);margin-bottom:.3rem;display:block;}"),
('.champ-n', "font-size:.58rem;letter-spacing:.2em;color:var(--or-somb);}", "font-size:.58rem;letter-spacing:.2em;color:var(--gris-cl);}"),
('.mn', "#ami-union .src-tri .mag-128-card .mn{font-size:.56rem;color:var(--or-somb);", "#ami-union .src-tri .mag-128-card .mn{font-size:.56rem;color:var(--gris-cl);"),
('.lw-n', "#ami-union .src-lois .lw-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--or-somb);", "#ami-union .src-lois .lw-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--gris-cl);"),
('.pk-n', "#ami-union .src-contrats .pk-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--or-somb);", "#ami-union .src-contrats .pk-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--gris-cl);"),
('.qs-cap', "text-transform:uppercase;color:var(--or-somb);margin-bottom:.7rem}", "text-transform:uppercase;color:var(--gris-cl);margin-bottom:.7rem}"),
('.tst-n', "letter-spacing:.14em;color:var(--or-somb);text-transform:uppercase}", "letter-spacing:.14em;color:var(--gris-cl);text-transform:uppercase}"),
('.tri-foot', "font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1rem;color:var(--or-somb)}", "font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1rem;color:var(--gris-cl)}", 2),
):
rep(old, new, n=(k[0] if k else 1), why=f'{sel} : or-somb â gris-cl')
rep('<span style="color:var(--or-somb)">â extension</span>', '<span style="color:var(--gris-cl)">â extension</span>', n=2, why='â extension')
rep('<span style="color:var(--or-somb);font-size:.5rem;">· nouveau ·</span>', '<span style="color:var(--gris-cl);font-size:.72rem;">· nouveau ·</span>', n=2, why='· nouveau ·')
# A·14 · les polarités d'onglet se lisent
rep("font-weight:400;line-height:1.25;color:rgba(226,226,240,.26);transition:color .2s}", "font-weight:400;line-height:1.25;color:var(--gris-cl);opacity:.8;transition:color .2s}", why='tb-pol lisible')
rep('#ami-union .tab-btn-full .tb-pol{font-size:.42rem}', '#ami-union .tab-btn-full .tb-pol{font-size:.72rem}', why='tb-pol mobile')
rep('#ami-union .tab-btn-full .tb-lbl{font-size:.66rem;letter-spacing:.03em}', '#ami-union .tab-btn-full .tb-lbl{font-size:.74rem;letter-spacing:.03em}', why='tb-lbl mobile')
# B·7 · les onglets n'ont pas de chiffre dans leur libellé (le chiffre vit au sommaire et au toc)
for k in range(1, 6):
rep(f'<span class="tb-lbl">{k} · ', '<span class="tb-lbl">', why=f'onglet {k} sans chiffre')
# A·15 · portail
rep('<h3 class="pp-pol" id="ppPol">', '<h3 class="pp-pol" id="ppPol" style="font-size:.78rem">', why='pp-pol taille')
# A·17 · olive un cran plus sombre (4,19 â â„ 4,5:1 pour la nacre)
rep('<div style="background:#577c05"><b>đŸ L\'Olive</b>', '<div style="background:#4d6b02"><b>đŸ L\'Olive</b>', why='olive')
# A·19 · pas de capitales en Cormorant : les h4 des cantons serif retrouvent leur casse
rep("#ami-union .src-tri h4{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .45rem;color:var(--or-pale)}", "#ami-union .src-tri h4{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .45rem;color:var(--or-pale);text-transform:none;letter-spacing:0}", why='h4 src-tri sans capitales')
rep("#ami-union .src-contrats .pk-t{font-size:1.26rem;font-weight:600;color:var(--or-pale);line-height:1.16}", "#ami-union .src-contrats .pk-t{font-size:1.26rem;font-weight:600;color:var(--or-pale);line-height:1.16;text-transform:none;letter-spacing:0}", why='pk-t sans capitales')
# A·18 · les émojis sortent de l'italique
EMO = re.compile(r'([\U0001F300-\U0001FAFFâ-âżâŹ-⯿](?:ïž|â[\U0001F300-\U0001FAFFâ-âż]ïž?)*)')
def desital(s): return EMO.sub(r'<span class="em">\1</span>', s)
n = 0
for old in ("âïž L'Art Est Mon Arme đïž", "đȘ Rejoins Moi pour Radicalement ProspĂ©rer đïž", "âïž Entre Respect & VĂ©ritĂ©. âïž", "par Le Silence â€ïž\u200dđ„</span>", "Demain toujours plus fort, encore & encore. đ±</p>"):
c = h.count(old); assert c >= 1, old
h = h.replace(old, desital(old)); n += c
J.append(f'Ă©mojis hors italique Ă{n}')
rep("if(ppD){ if(d[4]){ ppD.textContent=d[4]; ppD.hidden=false; }", "if(ppD){ if(d[4]){ ppD.innerHTML=d[4].replace(/[<>&]/g,function(c){return {'<':'<','>':'>','&':'&'}[c]}).replace(/([\\u{1F300}-\\u{1FAFF}\\u2600-\\u27BF](?:\\uFE0F|\\u200D[\\u{1F300}-\\u{1FAFF}\\u2600-\\u27BF]\\uFE0F?)*)/gu,'<span class=\"em\">$1</span>'); ppD.hidden=false; }", why='devise du portail : émojis hors italique (JS)')
rep('#ami-union *{margin:0;padding:0;box-sizing:border-box}', '#ami-union *{margin:0;padding:0;box-sizing:border-box}\n#ami-union .em{font-style:normal}', why='.em')
# A·28 · les romains orphelins du canton Lois deviennent des glyphes, comme leurs voisins (choix d'émoji à valider)
rep('<span class="num">â
</span><h2>đ± Les 7 Lois Naturelles du Vivant</h2>', '<span class="num">đ±</span><h2>Les 7 Lois Naturelles du Vivant</h2>', why='â
â đ±')
rep("<span class=\"num\">â
Ą</span><h2>Leur place d'origine : la triade du Champ 7</h2>", "<span class=\"num\">đ</span><h2>Leur place d'origine : la triade du Champ 7</h2>", why='â
Ą â đ')
rep('<span class="num">â
ą</span><h2>Et les deux Lois de mesure</h2>', '<span class="num">đ§ź</span><h2>Et les deux Lois de mesure</h2>', why='â
ą â đ§ź')
rep('<span class="num">â
Ș</span><h2>Les douze notions</h2>', '<span class="num">đ§©</span><h2>Les douze notions</h2>', why='â
Ș â đ§©')
# A·22 · cibles tactiles ℠24 px : les boutons de mode des planches
rep("padding:.22rem .6rem;border-radius:var(--r);cursor:pointer}", "padding:.4rem .7rem;min-height:24px;border-radius:var(--r);cursor:pointer}", why='.qs-m 24 px')
# B·19 · les liens du pied grossissent
rep('#ami-union .footer-nav{font-size:.62rem;letter-spacing:.1em;color:var(--gris);text-transform:uppercase;margin-top:1.5rem;line-height:2}', '#ami-union .footer-nav{font-size:.74rem;letter-spacing:.1em;color:var(--gris-cl);text-transform:uppercase;margin-top:1.5rem;line-height:2.3}', why='footer-nav lisible')
# B·18 · grilles sans orphelines : 8 cartes â 4 colonnes, 16 â 2
rep('#ami-union .src-tri .cmd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.5rem;}', '#ami-union .src-tri .cmd-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;}\n@media(max-width:900px){#ami-union .src-tri .cmd-grid{grid-template-columns:repeat(2,1fr)}}', why='cmd-grid 4 colonnes')
rep('#ami-union .src-tri .mag-128-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.4rem;}', '#ami-union .src-tri .mag-128-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.4rem;}\n@media(max-width:900px){#ami-union .src-tri .mag-128-grid{grid-template-columns:repeat(2,1fr)}}', why='mag-128-grid 4/2 colonnes')
# A·21 · chaque tableau dans son rouleau
tw_before = h.count('class="tw"')
def wrap_tables(html):
out = []; i = 0; n = 0
for m in re.finditer(r'<table\b.*?</table>', html, re.S):
before = html[max(0, m.start()-160):m.start()]
out.append(html[i:m.start()])
if re.search(r'class="(?:[^"]*\s)?(?:tw|cm2-tw|tw-x|scroll)(?:\s[^"]*)?"[^>]*>\s*$', before) or 'overflow' in before[-120:]:
out.append(m.group(0))
else:
out.append('<div class="tw">' + m.group(0) + '</div>'); n += 1
i = m.end()
out.append(html[i:]); return ''.join(out), n
h, n = wrap_tables(h); J.append(f'tableaux mis dans un .tw Ă{n}')
rep('#ami-union .em{font-style:normal}', '#ami-union .em{font-style:normal}\n#ami-union .tw{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}\n#ami-union .tw>table{min-width:100%}', why='.tw rĂšgle')
# A·27 · l'accessibilité des onglets : les six lignes
rep('<div class="tab-bar-full" aria-label="Cinq parties du Volume 0.3">', '<div class="tab-bar-full" role="tablist" aria-label="Cinq parties du Volume 0.3">', why='role=tablist')
c = len(re.findall(r'<button class="tab-btn-full( active)?" type="button" data-c="t\d">', h)); assert c == 5, c
h = re.sub(r'<button class="tab-btn-full( active)?" type="button" data-c="(t\d)">', lambda m: f'<button class="tab-btn-full{m.group(1) or ""}" type="button" role="tab" aria-selected="{"true" if m.group(1) else "false"}" data-c="{m.group(2)}">', h); J.append('role=tab + aria-selected Ă5')
rep('#ami-union .em{font-style:normal}', '#ami-union .em{font-style:normal}\n#ami-union .tab-btn-full:focus-visible,#ami-union button:focus-visible,#ami-union a:focus-visible{outline:2px solid var(--or);outline-offset:-2px}', why=':focus-visible')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J)); print(len(J), 'gestes')
p2_plancher.py p2_plancher.py page.html 390 · puis 1280 · puis 390 · puis 32086 lignes
Demande au navigateur qui est sous 11,5 px, Ă©crit la rĂšgle qui le relĂšve, remesure â jusqu'Ă zĂ©ro. Un Ă©lĂ©ment qui hĂ©rite sa petitesse reçoit max(.72rem,1em) â 19/09 : dĂ©plie aussi .tab-section et .lect-panel (CosmoLogie).
â§ Copier le script
# -*- coding: utf-8 -*-
"""Le plancher de lisibilité : plus aucun texte sous 11,5 px. On demande au navigateur QUI est sous le
plancher, on Ă©crit la rĂšgle qui le relĂšve, on remesure â jusqu'Ă ce que le FAIT soit acquis."""
import os, sys, json, re, collections
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
RID = sys.argv[3] if len(sys.argv) > 3 else 'ami-union' # l'id de la racine de la page
PLANCHER = 11.5
JS_OUVRE = r"""
(rid) => { const R=document.getElementById(rid);
R.querySelectorAll('.tab-content,.tab-panel,.tab-section,.lect-panel').forEach(p=>p.classList.add('active')); R.querySelectorAll('.reveal').forEach(p=>p.classList.add('visible'));
R.querySelectorAll('.pane,.tri-p,.tab-pane,.pt-pane,.psb-pane,.sp,.pt-champ,[data-c]').forEach(p=>{ if(p.tagName!=='BUTTON') p.classList.add('active','on'); });
R.querySelectorAll('details').forEach(d=>d.open=true);
R.querySelectorAll('[hidden]').forEach(e=>{ if(!e.matches('.pp-devise')) e.hidden=false; });
R.querySelectorAll('.som-corps,.toc-panel').forEach(e=>{e.style.display='block';e.style.maxHeight='none'});
return R.querySelectorAll('*').length; }
"""
JS_PETITS = r"""
([plancher, rid]) => {
const R=document.getElementById(rid); const out=[]; const seen=new Set();
const w=document.createTreeWalker(R, NodeFilter.SHOW_TEXT); let n;
function key(el){ // chemin de classes jusqu'au canton
const parts=[]; let e=el; let depth=0;
while(e && e!==R && depth<6){ const cls=(e.className&&typeof e.className==='string')?e.className.trim().split(/\s+/).filter(c=>!/^(active|on|open|ici|vide)$/.test(c)):[];
parts.unshift(e.tagName.toLowerCase()+(cls.length?'.'+cls[0]:'')); e=e.parentElement; depth++; }
return parts; }
while((n=w.nextNode())){ const t=n.textContent.trim(); if(t.length<2) continue; const el=n.parentElement; if(!el||el.closest('svg,script,style')) continue;
const cs=getComputedStyle(el); if(cs.display==='none'||cs.visibility==='hidden') continue;
const fs=parseFloat(cs.fontSize); if(fs>=plancher) continue;
const k=key(el); const id=k.join('>')+'|'+(el.getAttribute('style')?'S':''); if(seen.has(id)) continue; seen.add(id);
const pfs=el.parentElement?parseFloat(getComputedStyle(el.parentElement).fontSize):fs;
out.push({path:k, fs:+fs.toFixed(2), herite:Math.abs(pfs-fs)<0.01, inline:!!(el.getAttribute('style')&&/font-size/.test(el.getAttribute('style'))), t:t.slice(0,40)}); }
return out; }
"""
def selecteur(path, niveau):
# niveau 0 : dernier élément + son canton ; 1 : deux derniers ; 2 : trois derniers ; 3 : !important
canton = next((p for p in path if p.split('.')[-1] in ('div.src-tri','div.src-lois','div.src-contrats','div.src-arbo') or '.src-' in p), None)
tail = path[-(1+min(niveau,2)):]
# UN ĂLĂMENT SANS CLASSE NE FAIT JAMAIS UNE RĂGLE SEUL (« #x span{.72rem} » rapetisserait toute la page â vu sur 0.1 et l'Initiation) :
# on remonte jusqu'Ă un ancĂȘtre classĂ©, et s'il n'y en a pas dans les trois derniers, la rĂšgle reste nulle (le plancher hĂ©ritera : max(.72rem,1em)).
k = 1 + min(niveau, 2)
while '.' not in tail[-1] and not any('.' in p for p in tail) and k < 4 and k < len(path): k += 1; tail = path[-k:]
if not any('.' in p for p in tail): return None
sel = '#' + RID + ' ' + ((canton.split('.')[0]+'.'+canton.split('.')[1]+' ') if canton and canton not in tail else '') + ' '.join(tail)
return sel
html = open(SRC, encoding='utf-8').read()
W = int(sys.argv[2]) if len(sys.argv) > 2 else 390
regles = collections.OrderedDict()
ENTETE = ''
m0 = re.search(r'<style id="ami-plancher">\n(.*?)</style>\n', html, re.S)
if m0:
corps = m0.group(1)
i = corps.find('/* ââ LE PLANCHER')
ENTETE = corps[:i] if i >= 0 else corps # sans marqueur, tout le bloc est l'entĂȘte de non-rĂ©gression
for l in corps.split('\n'):
if l.endswith('}') and ('{font-size:.72rem' in l or '{font-size:max(.72rem' in l) and not os.environ.get('PLANCHER_NEUF'): regles[l[:l.index('{')]] = l
with sync_playwright() as p:
br = p.chromium.launch(); ctx = br.new_context(viewport={'width':W,'height':844}); page = ctx.new_page()
for passe in range(5):
page.set_content(html, wait_until='load'); page.wait_for_timeout(300)
page.evaluate(JS_OUVRE, RID)
petits = page.evaluate(JS_PETITS, [PLANCHER, RID])
print(f'passe {passe} : {len(petits)} éléments porteurs de texte sous {PLANCHER} px')
if not petits: break
for e in petits:
sel = selecteur(e['path'], passe if not e['inline'] else 3)
imp = '!important' if (e['inline'] or passe >= 3) else ''
# un Ă©lĂ©ment qui HĂRITE sa petitesse (strong dans une note) ne doit pas rapetisser ailleurs : max(.72rem,1em)
val = 'max(.72rem,1em)' if e.get('herite') else '.72rem'
if sel is None: # sans ancĂȘtre classĂ© : une rĂšgle sur le tag seul, mais qui hĂ©rite â jamais une taille absolue
sel = '#' + RID + ' ' + e['path'][-1]; val = 'max(.72rem,1em)'
if '.' not in e['path'][-1] and val == '.72rem': val = 'max(.72rem,1em)' # tag nu en bout de rÚgle : il hérite aussi
regles[sel] = f'{sel}{{font-size:{val}{imp}}}'
bloc = '<style id="ami-plancher">\n' + ENTETE + '/* ââ LE PLANCHER DE LISIBILITĂ â 11,5 px, mesurĂ© au rendu Ă 390 puis 1280 px. Ăcrit par p2_plancher.py ; se rejoue. ââ */\n' + '\n'.join(regles.values()) + '\n</style>\n'
html = re.sub(r'<style id="ami-plancher">.*?</style>\n', '', html, flags=re.S)
html = html.replace('</body>', bloc + '</body>')
# mesure finale
page.set_content(html, wait_until='load'); page.wait_for_timeout(300); page.evaluate(JS_OUVRE, RID)
reste = page.evaluate(JS_PETITS, [PLANCHER, RID])
print('reste :', len(reste), reste[:5])
br.close()
open(SRC, 'w', encoding='utf-8').write(html)
print(len(regles), 'rÚgles de plancher écrites')
p3_palette.py p3_palette.py rapatrier · puis repeindre194 lignes
M1 : chaque couleur en dur devient une variable (rendu inchangé, mesuré). M2 : les redéclarations de canton s'effacent et la racine prend les 13 pierres de la Charte.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P3 · La palette, en deux temps.
M1 · rapatrier : chaque couleur Ă©crite en dur reprend le nom de sa variable â sans rien changer au rendu.
M2 · une valeur, un lieu : les cantons cessent de redéclarer la palette.
REPEINDRE : la racine passe aux pierres de la Charte de Communication (6 sept 2026)."""
import re, sys, collections
sys.path.insert(0, '../verif/verificateur'); import canon as K, couleur as C
ETAPE = sys.argv[1] if len(sys.argv) > 1 else 'rapatrier'
FILE = sys.argv[2] if len(sys.argv) > 2 else 'v22.html'
ROOT = sys.argv[3] if len(sys.argv) > 3 else '#ami-union' # le sélecteur de la racine de la page
h = open(FILE, encoding='utf-8').read()
RX_ROOT = re.escape(ROOT) + r'\{\n(.*?)\n\}'
def norm_hex(x):
x = x.lower()
if len(x) == 4: x = '#' + ''.join(c*2 for c in x[1:])
return x[:7]
def rgb(hx): return C.hex2rgb(norm_hex(hx))
# ââ le bloc racine ââââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(RX_ROOT, h, re.S); root = m.group(1)
old_root = root
# la table ancien nom â valeur (avant), et â pierre (aprĂšs)
ANCIENS = { # nom : (valeur d'avant, cible Charte)
'--or':'#ffc545','--or-pale':'#ffd06a','--or-somb':'#7a611a','--nacre':'#f2ede2','--gris':'#9595a2','--gris-cl':'#b0b0bd',
'--vert':'#0b8d38','--vert-cl':'#32b15e','--rubis':'#e33232','--rubis-cl':'#fe6969','--bleu':'#0077ee','--bleu-cl':'#7697ee',
'--fuchsia':'#cf458a','--fuch-cl':'#f56aaf','--fuchsia-cl':'#f56aaf','--violet':'#6b63ff','--violet-cl':'#9d8bff',
'--turquoise':'#009f9d','--turquoise-cl':'#31b4b3','--turq':'#31b4b3','--amethyste':'#a04afa','--amethyste-cl':'#bb7cfe',
'--jade':'#62a802','--jade-cl':'#80c027','--ambre':'#ff7800','--dore':'#e6ad2d','--dore-cl':'#ffd06a','--rose':'#f56aaf',
}
PIERRES = {p: v for p, v in K.PALETTE.items()}
NEUTRES = {'--noir':'#09090e','--onyx':'#09090e','--encre':'#111118','--voile':'#181826','--labradorite':'#b3b1a8','--or-gras':'#e6ad2d'}
# valeurs d'AVANT de chaque variable (premiÚre déclaration hex trouvée dans le fichier)
avant = {}
for mm in re.finditer(r'(--[a-z][a-z0-9-]*)\s*:\s*(#[0-9a-fA-F]{3,8})\b', h):
avant.setdefault(mm.group(1), norm_hex(mm.group(2)))
val2var = {}
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
val2var.setdefault(v, k)
# la palette de la Charte, pour nommer ce qui n'a pas de variable
idx = {}
for p, trois in K.PALETTE.items():
for niv, hx in zip(K.NIVEAUX, trois): idx[hx.lower()] = '--' + p + niv
for hx, n in K.NEUTRES.items(): idx[hx.lower()] = '--' + n
for hx, n in K.STANDARD.items(): idx.setdefault(hx.lower(), '--' + n)
labs = [(n, C.hex2lab(hx)) for hx, n in idx.items()]
def nearest(hx):
lab = C.rgb2lab(C.hex2rgb(hx)); return min(((n, C.de2000(lab, l)) for n, l in labs), key=lambda t: t[1])
# ââ dĂ©coupage : CSS hors SVG, attributs style hors SVG ââââââââââââââââ
svg_spans = [(m.start(), m.end()) for m in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
script_spans = [(m.start(), m.end()) for m in re.finditer(r'<script\b.*?</script>', h, re.S)]
def dans(pos, spans): return any(a <= pos < b for a, b in spans)
nouveaux = collections.OrderedDict() # valeur â nom créé
journal = collections.Counter()
def nom_pour(hx):
"""le nom de variable d'une valeur écrite en dur"""
hx = norm_hex(hx)
if hx in val2var: return val2var[hx]
r, g, b = C.hex2rgb(hx)
if max(r,g,b) - min(r,g,b) <= 6: return None # un gris : neutre technique, on laisse
if hx in nouveaux: return nouveaux[hx]
n, d = nearest(hx)
base = n.lstrip('-').replace('-', '')
nom = f'--anc-{base}'
k = 2
while nom in nouveaux.values(): nom = f'--anc-{base}{k}'; k += 1
nouveaux[hx] = nom; return nom
if ETAPE == 'rapatrier':
# 1 · les hex dans les feuilles (hors SVG, hors déclarations de variables)
out = []; i = 0
for m in re.finditer(r'<style[^>]*>(.*?)</style>', h, re.S):
if dans(m.start(), svg_spans): continue
css = m.group(1)
def sub_hex(mm):
hx = mm.group(0); nom = nom_pour(hx)
if not nom: return hx
journal['hexâvar (css)'] += 1; return f'var({nom})'
# ne pas toucher aux déclarations de variables : on masque « --x:#hex »
parts = re.split(r'(--[a-z][a-z0-9-]*\s*:\s*#[0-9a-fA-F]{3,8}\b)', css)
for k in range(0, len(parts), 2):
parts[k] = re.sub(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b(?![0-9a-fA-F])', sub_hex, parts[k])
# rgba(r,g,b,a) â rgba(var(--x-rgb),a)
def sub_rgba(mm):
r, g, b = int(mm.group(1)), int(mm.group(2)), int(mm.group(3))
hx = '#%02x%02x%02x' % (r, g, b); nom = nom_pour(hx)
if not nom: return mm.group(0)
journal['rgbaâvar (css)'] += 1; return f'rgba(var({nom}-rgb),{mm.group(4)})'
parts[k] = re.sub(r'rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([0-9.]+)\s*\)', sub_rgba, parts[k])
css2 = ''.join(parts)
out.append(h[i:m.start(1)]); out.append(css2); i = m.end(1)
out.append(h[i:]); h = ''.join(out)
svg_spans = [(m.start(), m.end()) for m in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
# 2 · les attributs style="âŠ" hors SVG
def sub_style(mm):
if dans(mm.start(), svg_spans): return mm.group(0)
s = mm.group(1)
parts = re.split(r'(--[a-z][a-z0-9-]*\s*:\s*#[0-9a-fA-F]{3,8}\b)', s)
for k in range(0, len(parts), 2):
def sh(m2):
nom = nom_pour(m2.group(0))
if not nom: return m2.group(0)
journal['hexâvar (inline)'] += 1; return f'var({nom})'
parts[k] = re.sub(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b(?![0-9a-fA-F])', sh, parts[k])
def sr(m2):
hx = '#%02x%02x%02x' % (int(m2.group(1)), int(m2.group(2)), int(m2.group(3))); nom = nom_pour(hx)
if not nom: return m2.group(0)
journal['rgbaâvar (inline)'] += 1; return f'rgba(var({nom}-rgb),{m2.group(4)})'
parts[k] = re.sub(r'rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([0-9.]+)\s*\)', sr, parts[k])
# et les dĂ©clarations inline --tc:#hex â var()
for j in range(1, len(parts), 2):
d = parts[j]; nm, hx = d.split(':')[0].strip(), norm_hex(d.split(':')[1].strip())
nom = val2var.get(hx)
if nom and nom != nm: parts[j] = f'{nm}:var({nom})'; journal['inline --x:#hex â var'] += 1
return 'style="' + ''.join(parts) + '"'
h = re.sub(r'style="([^"]*)"', sub_style, h)
# 3 · la racine déclare tout : les anciennes valeurs, les triplets rgb, les nouveaux noms
# â ïž la racine est RE-cherchĂ©e ici : les Ă©tapes 1-2 ont pu la réécrire (un hex dans un commentaire, un rgba) â
# sur L'Initiation, le remplacement échouait en silence et la page perdait toute sa palette (17 sept).
m = re.search(RX_ROOT, h, re.S); root = m.group(1); old_root = root
decl = []
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
decl.append(f'{k}:{v}')
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
r, g, b = C.hex2rgb(v); decl.append(f'{k}-rgb:{r},{g},{b}')
for hx, nom in nouveaux.items():
r, g, b = C.hex2rgb(hx); decl.append(f'{nom}:{hx}'); decl.append(f'{nom}-rgb:{r},{g},{b}')
# ce que la racine gardait d'autre que des couleurs (all:revert, display, fontâŠ) reste
reste = re.sub(r'--[a-z][a-z0-9-]*\s*:\s*#[0-9a-fA-F]{3,8}\s*;?', '', root)
reste = re.sub(r'--[a-z][a-z0-9-]*-rgb\s*:\s*\d+,\d+,\d+\s*;?', '', reste)
reste = re.sub(r'/\*.*?\*/', '', reste, flags=re.S); reste = re.sub(r'\n\s*\n', '\n', reste).strip()
if '--r:' not in reste: reste = '--r:4px;\n' + reste
root_new = ' /* ââ LA PALETTE â dĂ©clarĂ©e ici, et nulle part ailleurs (P3·M1) ââ */\n ' + ';'.join(decl) + ';\n ' + reste
assert h.count(ROOT + '{\n' + old_root + '\n}') == 1, 'â la racine nâa pas Ă©tĂ© retrouvĂ©e : rien nâest Ă©crit'
h = h.replace(ROOT + '{\n' + old_root + '\n}', ROOT + '{\n' + root_new + '\n}', 1)
assert '-rgb:' in h, 'â la racine nâa pas Ă©tĂ© Ă©crite'
# 4 · M2 : les cantons ne redéclarent plus ce que la racine porte
import os
if os.environ.get('SANS_M2'): open(FILE.replace('.html','_m1seul.html'),'w',encoding='utf-8').write(h); raise SystemExit('M1 seul écrit')
svg_spans = [(x.start(), x.end()) for x in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
script_spans = [(x.start(), x.end()) for x in re.finditer(r'<script\b.*?</script>', h, re.S)]
root_spans = [(x.start(), x.end()) for x in re.finditer(r':root\s*\{[^}]*\}', h)] # la racine du composant (barre/pied) garde ses valeurs
root_spans += [(x.start(), x.end()) for x in re.finditer(r'<style id="ami-composant">.*?</style>', h, re.S)] # et sa feuille entiĂšre (elle vit hors de la racine de la page)
m = re.search(RX_ROOT, h, re.S); old_root = m.group(1)
def sub_canton(mm):
nm = mm.group(1)
if nm in avant and nm not in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc') and not dans(mm.start(), svg_spans) and not dans(mm.start(), script_spans) and not dans(mm.start(), root_spans):
if mm.start() > m.start() + len(old_root) + 50: # hors racine
journal['redéclaration de canton retirée'] += 1; return ''
return mm.group(0)
h = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*#[0-9a-fA-F]{3,8}\s*;?', sub_canton, h)
h = re.sub(r'\{\s*\}', '{}', h)
print(dict(journal)); print('nouveaux noms :', dict(nouveaux))
elif ETAPE == 'repeindre':
m = re.search(RX_ROOT, h, re.S); root = m.group(1)
def peint(mm):
k, v = mm.group(1), mm.group(2)
if k in ANCIENS: return f'{k}:{ANCIENS[k]}'
if k in NEUTRES: return f'{k}:{NEUTRES[k]}'
if k.startswith('--anc-'):
n, d = nearest(v)
if d < 8:
cible = n
# le nom de la pierre ; la valeur vient de la Charte
for hx, nm in idx.items():
if nm == cible: return f'{k}:{hx}'
return mm.group(0)
root2 = re.sub(r'(--[a-z][a-z0-9-]*):(#[0-9a-f]{6})', peint, root)
# les triplets suivent la variable qu'ils doublent
def triplet(mm):
k = mm.group(1); base = k[:-4]
mv = re.search(re.escape(base) + r':(#[0-9a-f]{6})', root2)
if mv:
r, g, b = C.hex2rgb(mv.group(1)); return f'{k}:{r},{g},{b}'
return mm.group(0)
root2 = re.sub(r'(--[a-z][a-z0-9-]*-rgb):(\d+,\d+,\d+)', triplet, root2)
# les pierres de la Charte, déclarées par leur nom, pour la suite du corpus
pierres = ';'.join(f'--{p}:{v[0]};--{p}-gras:{v[1]};--{p}-ct:{v[2]}' for p, v in PIERRES.items())
pierres += ';' + ';'.join(f'--{p}-rgb:{",".join(str(x) for x in C.hex2rgb(v[0]))}' for p, v in PIERRES.items())
root2 = re.sub(r'--r:(\d+px);', lambda mm: '/* ââ les treize pierres, telles que la Charte les arrĂȘte ââ */\n ' + pierres + ';--labradorite:#b3b1a8;--or-gras:#e6ad2d;\n --r:' + mm.group(1) + ';', root2, count=1)
h = h.replace(root, root2, 1)
# journal
ch = 0
for k in ANCIENS:
if re.search(re.escape(k) + r':#', root): ch += 1
print('variables repeintes :', ch)
open(FILE, 'w', encoding='utf-8').write(h)
compare_couleurs.py compare_couleurs.py avant.html aprĂšs.html30 lignes
Couleur calculée de chaque élément, apparié par son chemin dans le DOM : ce qui a bougé, et de quoi à quoi.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Compare la couleur calculĂ©e de chaque Ă©lĂ©ment entre deux Ă©tats â appariĂ© par le chemin dans le DOM, jamais par l'index."""
import sys, os, json, collections
from playwright.sync_api import sync_playwright
A, B = sys.argv[1], sys.argv[2]
JS = r"""() => { const R=document.getElementById('ami-union'); const out={};
R.querySelectorAll('.tab-content').forEach(p=>p.classList.add('active'));
R.querySelectorAll('[data-c]').forEach(p=>{ if(p.tagName!=='BUTTON') p.classList.add('active','on'); });
R.querySelectorAll('details').forEach(d=>d.open=true);
function key(el){ const p=[]; let e=el; while(e && e!==document.body){ let i=0,s=e; while((s=s.previousElementSibling)) i++; p.push(e.tagName+i); e=e.parentElement;} return p.join('/'); }
R.querySelectorAll('*').forEach(el=>{ if(el.closest('svg,script,style')) return; const cs=getComputedStyle(el);
out[key(el)] = [cs.color, cs.backgroundColor, cs.borderTopColor, cs.borderLeftColor, cs.backgroundImage.slice(0,80), (el.className||'').toString().slice(0,40)]; });
return out; }"""
with sync_playwright() as p:
br = p.chromium.launch(); res = {}
for f in (A, B):
pg = br.new_page(viewport={'width':1280,'height':900}); pg.goto('file://' + os.path.abspath(f)); pg.wait_for_timeout(500)
res[f] = pg.evaluate(JS); pg.close()
br.close()
a, b = res[A], res[B]
print('éléments :', len(a), len(b), '· appariés :', len(set(a) & set(b)))
diff = collections.Counter(); ex = collections.defaultdict(list)
for k in set(a) & set(b):
for i, nom in enumerate(('color', 'background', 'border-top', 'border-left', 'background-image')):
if a[k][i] != b[k][i]:
key = (nom, a[k][i][:40], b[k][i][:40]); diff[key] += 1
if len(ex[key]) < 2: ex[key].append(a[k][5])
print('différences :', sum(diff.values()))
for (nom, x, y), n in diff.most_common(60): print(f'{n:5d}Ă {nom:16s} {x:40s} â {y:40s} ex {ex[(nom,x,y)]}')
p4_anatomie.py p4_anatomie.py117 lignes
Pose la barre et le pied V16, la bande des registres, #prog, le bloc Emporter, le contraste élevé et la lecture continue ; version et date écrites une fois (data-ami).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · L'anatomie de Page : la barre et le pied V16 (pris à la Charte V5, la source vivante du composant),
la bande des registres, la barre de progression, le bloc Emporter, les commandes đ§ â â¶ â % +."""
import re, json
d = json.load(open('barre_extrait.json'))
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · la feuille du composant, dans le <head> ââââââââââââââââââââââ
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items())
css = d['css']
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5 (9 sept 2026),\n'
' la source vivante du composant. On ne les corrige pas ici : on les reprend Ă la Charte. ââ */\n'
':root{' + rootvals + ';--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
'#ami-union .ami-fs{display:none}\n' # la barre porte le â¶ ; celui de la page se tait
'.emporter{max-width:900px;margin:2rem auto;padding:0 1.2rem}\n'
'.bande-registres{max-width:1080px;margin:0 auto}\n'
+ css + '\n</style>\n')
rep('</head>', feuille + '</head>', why='feuille du composant')
# ââ 2 · la barre et la progression, en tĂȘte ; le pied et lâemport, en queue ââ
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="ami-union-du-tout">')
rep('<body>\n<div id="ami-union">', '<body>\n' + d['prog'] + '\n' + nav + '\n<div id="ami-union">', why='barre + #prog')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>', '<div class="ap-sceau" title="đ Le sceau du volume">đ</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', "<p class=\"ap-titre\">L'Union du TouT</p>")
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">0.3</span> â La Vie</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">Une charte nâest pas une liste de rĂšgles : câest la forme quâon donne Ă ce quâon veut devenir.\nOn nâobĂ©it pas Ă une charte â on sây tient.</p>")
assert 'title="đ Le sceau du volume">đ' in pied and "L'Union du TouT" in pied and 'Volume <span' in pied and 'on sây tient' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong>, sans script distant : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â onglets, sous-onglets, planches, lecture continue, contraste.</p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V22</span> â arrĂȘtĂ©e au <span data-ami="date">9 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
# le pied vient aprĂšs la racine, avant les feuilles de fin
i = h.rfind('<style id="ami-plancher">')
assert i > 0
# la fermeture de #ami-union est le dernier </div> avant les styles de fin ; on pose l'emport DEDANS (dernier enfant) et le pied DEHORS
j = h.rfind('</div>', 0, i)
h = h[:j] + emport + h[j:j+6] + '\n' + pied + '\n' + h[j+6:]; J.append('emport (dans la racine) + pied (aprĂšs)')
# ââ 3 · la bande des registres, entre la une et les onglets ââââââââââ
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="Charte_Communication_aMi_BLOC_13.html" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('<div class="tab-bar-full" role="tablist"', bande + '\n<div class="tab-bar-full" role="tablist"', why='bande des registres')
# ââ 4 · les scripts du composant, adaptĂ©s Ă cette page âââââââââââââââ
js = d['js_barre']
js = js.replace("document.getElementById('ami-charte')||document.body", "document.getElementById('ami-union')||document.body")
# le sommaire â° de la barre n'est pas posĂ© ici : la page a le sien (â Dans cet onglet) â le code le saute (if(BT))
# l'emport : les onglets de CETTE page, et le nom du fichier
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('.tab-btn-full').forEach(function(o,i){ o.classList.toggle('active', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.tab-content').forEach(function(p,i){ p.classList.toggle('active', i===0); });""")
assert "'.tab-content').forEach" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';")
assert '0.3_LUNION_DU_TOUT' in js
shim = """
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue et le contraste Ă©levĂ© ââ
La barre ne connaßt pas le contenu : elle crie, la page répond. */
(function(){'use strict';
var R=document.getElementById('ami-union'); if(!R) return;
var memo=null;
document.addEventListener('ami:lecture',function(e){
var on=e.detail&&e.detail.actif;
var pans=[].slice.call(R.querySelectorAll('.tab-content'));
if(on){ memo=(R.querySelector('.tab-content.active')||{dataset:{c:'t1'}}).dataset.c;
pans.forEach(function(p){ p.classList.add('active'); }); R.classList.add('lecture'); }
else { R.classList.remove('lecture'); pans.forEach(function(p){ p.classList.toggle('active', p.dataset.c===memo); });
if(window.amiOuvreOnglet&&memo) window.amiOuvreOnglet(memo,false); }
});
})();
"""
contraste_css = """
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct, les graisses fines s'Ă©paississent ââ */
body.ami-contraste #ami-union{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;
--fuchsia:var(--grenat-ct);--fuch-cl:#ffa4d0;--fuchsia-cl:#ffa4d0;--violet:var(--tanzanite-ct);--violet-cl:#c3b8ff;
--turquoise:var(--amazonite-ct);--turquoise-cl:#7fdcd6;--amethyste:var(--amethyste-ct);--amethyste-cl:#d8b8ff;
--jade:var(--moldavite-ct);--jade-cl:#b6e27a;--dore:var(--ambre-ct);--dore-cl:#ffe2a3;--or-somb:#b09030}
body.ami-contraste #ami-union{font-weight:400}
body.ami-contraste #ami-union em,body.ami-contraste #ami-union .cit,body.ami-contraste #ami-union .couv-sub{font-style:normal}
</style>
"""
scripts = '<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n'
rep('<style id="ami-plancher">', contraste_css + scripts + '<style id="ami-plancher">', why='scripts du composant + contraste')
# ââ 5 · la date s'Ă©crit une fois : la une et la fin la lisent âââââââââ
rep('<p class="sig">Agdistys · Diane Serant & Claude · CC BY-SA 4.0</p>', '<p class="sig">Agdistys · Diane Serant & Claude · CC BY-SA 4.0 · <span data-ami="version">V22</span> â <span data-ami="date">9 septembre 2026</span></p>', why='date sur la une')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p5_correctifs.py p5_correctifs.py37 lignes
Ce que la non-rĂ©gression a rĂ©vĂ©lĂ© : la portĂ©e main â #ami-union, les rĂšgles #tv/.tvx ĂŽtĂ©es.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · Ce que la non-régression a révélé : la bande des registres était habillée pour <main> (la Charte),
pas pour #ami-union (cette page) ; et la feuille du composant charriait les rĂšgles de l'onglet #tv/.tvx de la Charte."""
import re
h = open('v22.html', encoding='utf-8').read()
m = re.search(r'(<style id="ami-composant">)(.*?)(</style>)', h, re.S)
css = m.group(2)
avant = css.count('{')
# 1 · la portĂ©e : main â #ami-union
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', r'#ami-union \1', css)
assert 'main .bande' not in css
# 2 · on Îte les rÚgles dont TOUS les sélecteurs vivent dans #tv / .tvx (l'onglet de la Charte, absent ici)
out = []; i = 0; n = len(css); gardees = 0; otees = 0
while i < n:
j = css.find('{', i)
if j < 0: out.append(css[i:]); break
sel = css[i:j]; depth = 1; k = j + 1
while k < n and depth:
if css[k] == '{': depth += 1
elif css[k] == '}': depth -= 1
k += 1
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts):
otees += 1
else:
# dans une liste mixte, on retire les membres #tv/.tvx
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
lead = sel[:len(sel) - len(sel.lstrip())]
sel = lead + ',\n'.join(keep)
out.append(sel + css[j:k]); gardees += 1
i = k
css2 = ''.join(out)
h = h[:m.start()] + m.group(1) + css2 + m.group(3) + h[m.end():]
open('v22.html', 'w', encoding='utf-8').write(h)
print(f'â portĂ©e main â #ami-union · rĂšgles : {avant} avant, {gardees} gardĂ©es, {otees} ĂŽtĂ©es (#tv/.tvx) · feuille {len(m.group(2))//1024} â {len(css2)//1024} Ko')
p5_mutations.py p5_mutations.py page.html94 lignes
Presse chaque commande (onglets clic/clavier, renvoi, đ§, â, â¶ + zoom, â°, â, progression, âŹïž export, dates, reduced-motion, sommaire, .v.ici) et vĂ©rifie que la page a changĂ© comme promis.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · La mutation des contrÎles : on presse chaque commande et on regarde si la page a CHANGà comme promis."""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(600)
print(f'=== {W} px ===')
E = lambda js: pg.evaluate(js)
# onglets : clic
E("()=>document.querySelectorAll('.tab-btn-full')[2].click()"); pg.wait_for_timeout(150)
ok('onglet 3 par clic', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t3' and E("()=>document.querySelectorAll('.tab-btn-full')[2].getAttribute('aria-selected')")=='true')
# onglets : clavier â
E("()=>{const b=document.querySelectorAll('.tab-btn-full')[2]; b.focus(); b.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('onglet suivant au clavier â', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t4')
E("()=>{const b=document.activeElement; b.dispatchEvent(new KeyboardEvent('keydown',{key:'Home',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('Home â premier onglet', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t1')
# lien interne vers un autre onglet ouvre l'onglet
E("()=>{let a=document.querySelector('a[href=\"#diag-souverain\"]'); if(!a){ a=document.createElement('a'); a.href='#diag-souverain'; a.textContent='renvoi'; document.querySelector('.tab-content.active').appendChild(a); } a.click();}"); pg.wait_for_timeout(300) # le seul renvoi vivait dans l'ancienne Charte Mathématique (partie le 19/09) : on en pose un pour éprouver le mécanisme
ok('renvoi #diag-souverain ouvre son onglet', E("()=>{const t=document.getElementById('diag-souverain'); return !!t && t.closest('.tab-content').classList.contains('active') && t.getBoundingClientRect().width>0;}"))
# lecture continue đ§
n0 = E("()=>document.querySelectorAll('.tab-content.active').length")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
n1 = E("()=>document.querySelectorAll('.tab-content.active').length")
ok('đ§ lecture continue : tous les onglets dĂ©roulĂ©s', n0 == 1 and n1 == 5, f'{n0}â{n1}')
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : un seul onglet', E("()=>document.querySelectorAll('.tab-content.active').length") == 1)
# contraste â
c0 = E("()=>getComputedStyle(document.querySelector('#ami-union .qs-c .tx, #ami-union p')).color")
col0 = E("()=>getComputedStyle(document.querySelector('#ami-union')).getPropertyValue('--or').trim()")
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
col1 = E("()=>getComputedStyle(document.querySelector('#ami-union')).getPropertyValue('--or').trim()")
ok('â contraste : --or monte au niveau -ct', E("()=>document.body.classList.contains('ami-contraste')") and col0 != col1, f'{col0}â{col1}')
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('â retour', not E("()=>document.body.classList.contains('ami-contraste')"))
# zoom â / +
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('ⶠplein écran (faux plein) posé', E("()=>document.getElementById('ami-union').classList.contains('ami-faux-fs')||!!document.fullscreenElement"))
fs0 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
fs1 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(200)
fs2 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('â¶ retour', not E("()=>document.getElementById('ami-union').classList.contains('ami-faux-fs')"))
ok('+ agrandit, â rend', fs1 > fs0 and abs(fs2 - fs0) < .05, f'{fs0}â{fs1}â{fs2}')
# sommaire â° de la page
E("()=>document.getElementById('tocBtn').click()"); pg.wait_for_timeout(200)
ok('â° sommaire sâouvre', E("()=>{const p=document.getElementById('tocPanel'); const cs=getComputedStyle(p); return cs.display!=='none' && p.getBoundingClientRect().height>10;}"))
E("()=>document.getElementById('tocBtn').click()"); pg.wait_for_timeout(200)
# â haut
E("()=>window.scrollTo(0,3000)"); pg.wait_for_timeout(200)
E("()=>{const h=document.querySelector('.ami-top'); if(h) h.click();}"); pg.wait_for_timeout(1500)
ok('â remonte', E("()=>window.scrollY") < 50, str(E("()=>window.scrollY")))
# progression
E("()=>window.scrollTo(0,document.body.scrollHeight/2)"); pg.wait_for_timeout(300)
ok('#prog progresse', E("()=>{const p=document.getElementById('prog'); const w=parseFloat(getComputedStyle(p).width)||parseFloat(p.style.width)||0; return w>10;}"))
# emport : le bouton compose un fichier
E("()=>{window.__urls=[]; const o=URL.createObjectURL; URL.createObjectURL=function(b){window.__blob=b; return o.call(URL,b);}; HTMLAnchorElement.prototype.click=function(){window.__dl=this.download;};}")
E("()=>document.getElementById('dlHtml').click()"); pg.wait_for_timeout(800)
VER = E("()=>document.querySelector('meta[name=\"ami:version\"]').content"); DATE = E("()=>document.querySelector('meta[name=\"ami:date\"]').content")
import datetime; MOIS=['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']; d=datetime.date.fromisoformat(DATE); DATE_FR=f'{d.day} {MOIS[d.month-1]} {d.year}'
ok('âŹïž Export HTML : fichier composĂ©, bien nommĂ©', E("()=>window.__dl")==f'0.3_LUNION_DU_TOUT_aMi_{VER}.html' and E("()=>window.__blob? window.__blob.size:0")>1000000, str(E("()=>[window.__dl, window.__blob&&window.__blob.size]")))
ok('âŹïž poids affichĂ©', E("()=>document.getElementById('dlPoids').textContent.trim().length")>0, E("()=>document.getElementById('dlPoids').textContent.trim()"))
# dates lues depuis les metas
ok('version/date = metas', E("()=>[...document.querySelectorAll('[data-ami=version]')].every(e=>e.textContent==='"+VER+"') && [...document.querySelectorAll('[data-ami=date]')].every(e=>e.textContent==='"+DATE_FR+"')"), E("()=>[...document.querySelectorAll('[data-ami=date]')].map(e=>e.textContent)"))
# reduced motion
pg.emulate_media(reduced_motion='reduce'); pg.wait_for_timeout(100)
ok('prefers-reduced-motion coupe les animations', E("()=>[...document.querySelectorAll('*')].every(e=>{const cs=getComputedStyle(e); return cs.animationDuration==='0s'||cs.animationDuration==='0.001s'||cs.animationName==='none';})"))
pg.emulate_media(reduced_motion='no-preference')
# sommaire : 5 · 28
ok('sommaire 5 onglets · 28 sections', E("()=>{const ch=document.querySelectorAll('#somCorps .som-partie').length; const s=document.querySelectorAll('#somCorps a[href^=\"#\"]').length; return ch+'·'+s;}") in ('5·33','5·28'), E("()=>{const ch=document.querySelectorAll('#somCorps .som-partie').length; const s=document.querySelectorAll('#somCorps a[href^=\"#\"]').length; return ch+'·'+s;}"))
ok('ancres du sommaire toutes présentes', E("()=>[...document.querySelectorAll('#somCorps a[href^=\"#\"]')].every(a=>document.getElementById(decodeURIComponent(a.getAttribute('href').slice(1))))"))
# le canton Arborescence : filtres et infobulles vivants
E("()=>document.querySelector('.tab-btn-full[data-c=\"t3\"]').click()"); pg.wait_for_timeout(150)
E("()=>document.querySelector('#ami-union .sub-bar-host button[data-c=psy]').click()"); pg.wait_for_timeout(150)
E("()=>document.querySelectorAll('#tp-psy .sub-b')[1].click()"); pg.wait_for_timeout(150)
f0 = E("()=>document.querySelectorAll('#tp-psy .dc-row[data-f]:not(.hid)').length")
E("()=>document.querySelector('#tp-psy .flt button[data-flt=urgence]').click()"); pg.wait_for_timeout(200)
f1 = E("()=>document.querySelectorAll('#tp-psy .dc-row[data-f]:not(.hid)').length")
ok('đł filtre « urgence » rĂ©duit les bifurcations', 0 < f1 < f0, f'{f0}â{f1}')
E("()=>{const e=document.querySelector('#tp-psy [data-champ]'); e.dispatchEvent(new Event('mouseenter'));}"); pg.wait_for_timeout(150)
ok('đł infobulle de Champ sâaffiche', E("()=>document.getElementById('chpop').classList.contains('show')"))
ok('.v.ici marqué sur 0.3', E("()=>{const v=document.querySelector('.ami-barre .v.ici'); return v && /0\\.3/.test(v.textContent);}"))
pg.close()
b.close()
print('erreurs JS :', errs)
print(sum(1 for r in R if r[1]), '/', len(R), 'réussis')
p6_retours.py p6_retours.py113 lignes
Les retours de Diane du 9 sept : onglets couleur qui replient, un seul titre, améthyste, Emporter, --tab-h, rÚgle * Îtée.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · Les retours de Diane du 9 sept 20h : onglets en couleur et qui replient (jamais de rouleau),
Les Chartes du Vivant en améthyste, un seul titre par partie (le portail, émoji au-dessus, glow),
l'ordre une â onglets â sommaire â titre, le bloc Emporter habillĂ©, la devise du pied propre au volume."""
import re
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · les titres en double : le portail est LE titre ; les cantons perdent le leur âââââââââ
# t1 · Quintessence
rep('<div class="section">\n <div class="sec-header" id="quintessence"><span class="sec-num">ê©</span><h2>Quintessence â La BeautĂ© de La Vie</h2></div>',
'<div class="section" id="quintessence">', why='t1 : titre du canton')
# t2 · Les Lois (pas d\'id)
rep('<div class="section">\n <div class="sec-header"><span class="sec-num">âïž</span><h2>Les Lois de La Vie đ±</h2></div>\n <blockquote><p>Comment ça fonctionne.</p></blockquote>',
'<div class="section">', why='t2 : titre + « Comment ça fonctionne. »')
# t3 · la une du canton + le titre
i = h.find('<div class="tri-une">'); m1 = h.find('</div>', h.find('<div class="tri-meta">', i)) + 6; j = h.find('</div>\n', m1) + len('</div>\n')
assert h[m1:j].strip() == '</div>', h[m1:j]
assert 0 < i < j and h[i:j].count('<div class="tri-une">') == 1
h = h[:i] + h[j:]; J.append('t3 : la une du canton (đžïžđ Union du TouT ⊠CC BY-SA) ĂŽtĂ©e')
rep('<div class="section">\n <div class="sec-header" id="chartes-cinq"><span class="sec-num">đ±</span><h2>Les Chartes du Vivant âš</h2></div>\n <blockquote><p>Notre Intention.</p></blockquote>',
'<div class="section" id="chartes-cinq">', why='t3 : titre + « Notre Intention. »')
# t4 · Communication
rep('<div class="section">\n <div class="sec-header" id="comm-liberte"><span class="sec-num">đđœ</span><h2>La Communication du Vivant</h2></div>\n <blockquote><p>Notre Attention.</p></blockquote>',
'<div class="section" id="comm-liberte">', why='t4 : titre + « Notre Attention. »')
# t5 · Contrats
rep('<div class="section">\n <div class="sec-header" id="contrats-quatre"><span class="sec-num">đ</span><h2>Les Contrats de ConScience âïž</h2></div>\n <blockquote><p>Ă quoi on s\'engage.</p></blockquote>',
'<div class="section" id="contrats-quatre">', why='t5 : titre + « Ă quoi on sâengage. »')
# le sous-titre de la une du canton t3 rejoint le portail
rep('''"Cinq chartes : ce qui se mesure, ce qui se traverse, ce qui se lit, ce qui se protĂšge, ce qui se sublime.",''',
'''"Cinq chartes : ce qui se mesure, ce qui se traverse, ce qui se lit, ce qui se protĂšge, ce qui se sublime.\\nLe rapport, la relation, la lecture, le pont, le sceau â cinq langues pour un seul objet, et un seul objet pour les tenir ensemble.",''',
why='t3 : sous-titre de la une versé au portail')
# ââ 2 · l'ordre : une â onglets â sommaire â titre (portail) â contenu âââââââââââââââââââââ
i = h.find('<section class="part-portail"'); j = h.find('</section>', i) + len('</section>')
portail = h[i:j]; h = h[:i] + h[j:]
rep('</main>\n\n<div class="corps">', '</main>\n' + portail + '\n<div class="corps">', why='portail déplacé aprÚs le sommaire')
# ââ 3 · la feuille des retours ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
css = '''
<style id="ami-retours">
/* ââ RETOURS DU 9 SEPT · onglets en couleur, qui replient ; titre unique ; amĂ©thyste pour Les Chartes ââ */
#ami-union .tab-bar-full{flex-wrap:wrap;overflow-x:visible;overflow:visible}
#ami-union .tab-btn-full{flex:1 1 18%;min-width:6.4rem;white-space:normal;color:var(--tc)}
#ami-union .tab-btn-full .tb-em{filter:none;opacity:.9}
#ami-union .tab-btn-full .tb-lbl{text-align:center}
#ami-union .tab-btn-full .tb-lbl{font-size:.86rem;font-weight:500}
#ami-union .tab-btn-full .tb-pol{color:var(--gris);opacity:1;font-size:.72rem;letter-spacing:.1em}
#ami-union .tab-btn-full.active .tb-pol{color:var(--gris-cl);opacity:1}
#ami-union .tab-btn-full:hover,#ami-union .tab-btn-full.active{color:var(--tcl)}
#ami-union .tab-btn-full.active{background:rgba(var(--tc-rgb,var(--or-rgb)),.12);border-bottom-color:var(--tc)}
#ami-union .tab-btn-full[data-c="t1"]{--tc-rgb:var(--or-rgb)}
#ami-union .tab-btn-full[data-c="t2"]{--tc-rgb:var(--vert-rgb)}
#ami-union .tab-btn-full[data-c="t3"]{--tc:var(--amethyste);--tcl:var(--amethyste-cl);--tc-rgb:var(--amethyste-rgb)}
#ami-union .tab-btn-full[data-c="t4"]{--tc-rgb:var(--fuchsia-rgb)}
#ami-union .tab-btn-full[data-c="t5"]{--tc-rgb:var(--bleu-rgb)}
/* les sous-onglets replient eux aussi : rien ne roule Ă droite */
#ami-union .sub-bar-host .in,#ami-union .sub-bar-host .tabs-in{flex-wrap:wrap!important;overflow-x:visible;justify-content:center!important}
#ami-union .sub-bar-host .tri-b,#ami-union .sub-bar-host .tb{white-space:normal}
#ami-union .chr-q{white-space:normal}
/* Les Chartes du Vivant : améthyste */
#ami-union .part-portail[data-c="t3"]{--ppc:var(--amethyste);--ppcl:var(--amethyste-cl)}
#ami-union .src-tri{border-top-color:var(--amethyste)}
#ami-union .tri-b.on,#ami-union .tri-b[data-c="intro"].on{background:rgba(var(--amethyste-rgb),.12)!important;color:var(--amethyste-cl)!important;border-bottom-color:var(--amethyste)!important}
/* le portail : lâĂ©moji au-dessus, le glow, une seule fois */
#ami-union .part-portail{flex-direction:column;gap:.45rem;padding:1.4rem 1.2rem 1.2rem;margin:1.2rem auto .6rem;
background:radial-gradient(ellipse at 50% 0%,rgba(var(--pp-rgb,var(--or-rgb)),.13),transparent 70%)}
#ami-union .part-portail .pp-icon{font-size:2.6rem;filter:drop-shadow(0 0 16px var(--ppc,var(--or)))}
#ami-union .part-portail .pp-mission{white-space:pre-line}
#ami-union .part-portail[data-c="t2"]{--pp-rgb:var(--vert-rgb)}
#ami-union .part-portail[data-c="t3"]{--pp-rgb:var(--amethyste-rgb)}
#ami-union .part-portail[data-c="t4"]{--pp-rgb:var(--fuchsia-rgb)}
#ami-union .part-portail[data-c="t5"]{--pp-rgb:var(--bleu-rgb)}
/* â Emporter â les valeurs de la Charte, portĂ©es dans la racine */
#ami-union .emporter{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);
border:1px solid var(--or-somb);border-radius:14px;text-align:center}
#ami-union .emporter h2{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none}
#ami-union .emporter p{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}
#ami-union .emporter p strong{color:var(--nacre);font-weight:500;font-size:inherit}
#ami-union .emporter .emp-b{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}
#ami-union .emporter .emp-b button{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);
border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:24px}
#ami-union .emporter .emp-b button:hover{background:rgba(255,255,255,.1);color:var(--or-pale)}
#ami-union .emporter .emp-d{font-size:.78rem;color:var(--gris)}
#ami-union .emporter .emp-d em{color:var(--gris);font-style:italic}
#ami-union .emporter [data-ami]{color:var(--or-pale);font-size:inherit}
</style>
'''
rep('<style id="ami-plancher">', css + '<style id="ami-plancher">', why='feuille des retours')
# ââ 4 · le sommaire : Les Chartes en amĂ©thyste âââââââââââââââââââââââââââââââââââââââââ
rep('<div class="som-partie" style="border-left:3px solid var(--or);margin-top:1rem">\n <div class="som-titre-btn open" data-som="t3" style="color:var(--or-pale)">',
'<div class="som-partie" style="border-left:3px solid var(--amethyste);margin-top:1rem">\n <div class="som-titre-btn open" data-som="t3" style="color:var(--amethyste-cl)">', why='sommaire t3 améthyste')
# ââ 5 · la devise du pied : celle du volume, pas celle des Chartes (elle est dĂ©jĂ au portail t3) ââ
rep("<p class=\"ap-cit\">Une charte nâest pas une liste de rĂšgles : câest la forme quâon donne Ă ce quâon veut devenir.\nOn nâobĂ©it pas Ă une charte â on sây tient.</p>",
"<p class=\"ap-cit\">Cinq domaines, trois langues, et Moi au carrĂ©.\nRien nâest en trop, rien ne manque : câest à ça quâon reconnaĂźt le Vivant.</p>", why='pied : devise du volume')
# ââ 6 · deux hauteurs, deux noms : --nav-h = la barre du site (racine), --tab-h = la barre des onglets (page) ââ
rep("#ami-union{--nav-h:66px}","#ami-union{--tab-h:66px}", why='--tab-h déclaré')
rep("R.style.setProperty('--nav-h', h+'px')","R.style.setProperty('--tab-h', h+'px')", why='--tab-h mesuré')
rep("#ami-union .sub-bar-host{position:sticky;top:var(--nav-h);","#ami-union .sub-bar-host{position:sticky;top:calc(var(--nav-h) + var(--tab-h));", why='sous-barre sous les deux')
rep("#ami-union .tab-bar-full{position:sticky;top:0;","#ami-union .tab-bar-full{position:sticky;top:var(--nav-h);", why='barre des onglets sous la barre du site')
# ââ 7 · la racine du composant portait « --r:4px}*{margin:0 » : une rĂšgle * qui posait --nav-h:0 et margin:0 sur tout ââ
rep('--r:4px}\n*{margin:0;--rubis:', '--r:4px;--rubis:', why='rÚgle * parasite Îtée de la racine')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p7_arbo_v13.py p7_arbo_v13.py v22.html ../arbo/v13.html116 lignes
Verse l'Arborescence V13 dans le canton đł de L'Union (sous-onglets psy1/2/3), rebranche ses scripts sur #tp-psy, ajoute ses styles, met les 13 Champs au canon, recorrige ce que P1 avait corrigĂ©.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P7 · L'Arborescence V13 remplace le canton đł de L'Union du TouT (sous-onglets psy1/psy2/psy3).
On garde l'enveloppe du canton (bandeau, sous-nav, ids des renvois), on verse le contenu de V13,
on rebranche les scripts (ils cherchaient #arbo-sante, absent : filtres et infobulles étaient morts),
on ajoute les styles que V13 apporte, et on met les 13 Champs au canon du 9 sept."""
import re, sys
U = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
V = sys.argv[2] if len(sys.argv) > 2 else '../arbo/v13.html'
h = open(U, encoding='utf-8').read(); v = open(V, encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
def inner(s, start_pat):
"""contenu d'un <div âŠ> jusqu'Ă sa fermeture (compte les div)"""
m = re.search(start_pat, s); i = m.end(); depth = 1; k = i
while depth:
n1 = s.find('<div', k); n2 = s.find('</div>', k)
if n2 < 0: raise SystemExit('div non fermée')
if 0 <= n1 < n2: depth += 1; k = n1 + 4
else: depth -= 1; k = n2 + 6
return m.start(), m.end(), k - 6, k # début balise, fin balise ouvrante, début </div>, fin
# ââ 1 · les trois volets de V13 âââââââââââââââââââââââââââââââââââââââââââââââ
b = v[v.find('<body'):]
panes = {}
for t in ('t0', 't1', 't2'):
a, bo, c, d = inner(b, rf'<div class="pane-tab[^"]*" id="{t}">')
panes[t] = b[bo:c]
print({t: len(p) for t, p in panes.items()})
# ââ 2 · dans l'Union : sp-psy1/2/3 reçoivent le contenu âââââââââââââââââââââââ
for t, sp in (('t0', 'sp-psy1'), ('t1', 'sp-psy2'), ('t2', 'sp-psy3')):
a, bo, c, d = inner(h, rf'<div class="sub-p[^"]*" id="{sp}">')
h = h[:bo] + '\n' + panes[t].strip() + '\n' + h[c:]
J.append(f'{sp} â V13 {t}')
# ââ 3 · le Point ZĂ©ro (U+030B â â), les ids des renvois âââââââââââââââââââââââ
n = h.count('Ì'); h = h.replace('Ì Le Point ZĂ©ro', '<span class="pz">â</span> Le Point ZĂ©ro').replace('<span class="pz">Ì</span>', '<span class="pz">â</span>'); J.append(f'U+030B â â Ă{n}')
for m in re.finditer('Ì', h): print('RESTE', repr(h[m.start()-60:m.start()+30]))
assert 'Ì' not in h
rep('<div class="note eth"><b>Le quatriĂšme cas â quand la balance ne pĂšse rien.</b>', '<div class="note eth" id="arbo-balance"><b>Le quatriĂšme cas â quand la balance ne pĂšse rien.</b>', why='id arbo-balance')
i = h.find('âïž JAUGER & JUGER â deux gestes'); assert i > 0
j = h.rfind('<div class="card or">', 0, i); assert i - j < 40, i - j
h = h[:j] + '<div class="card or" id="arbo-jauger">' + h[j + len('<div class="card or">'):]; J.append('id arbo-jauger')
# ââ 4 · les scripts du canton cherchaient #arbo-sante : ils trouvent #tp-psy âââ
rep('var R=document.getElementById("arbo-sante");if(!R||R.__v6)return;R.__v6=1;', 'var R=document.getElementById("tp-psy");if(!R||R.__v6)return;R.__v6=1;', why='script filtres/infobulles rebranché')
rep('var R=document.getElementById("arbo-sante");if(!R||R.__tb)return;R.__tb=1;', 'var R=document.getElementById("tp-psy");if(!R||R.__tb)return;R.__tb=1;', why='script onglets V13 (inerte ici, mais propre)')
rep('var pop=R.querySelector("#chpop");', 'var pop=document.getElementById("chpop");', why='#chpop vit hors du canton')
assert 'id="chpop"' in h
# ââ 5 · les 13 Champs au canon (Ă©moji + nom ; les descriptions restent) âââââââ
CANON = {'đ„ C4 · Le Feu IntĂ©rieur': 'đł C4 · La Direction', 'đ C5 · Le Phi': 'đ C5 · La SouverainetĂ©', 'đ C7 · La Trace': 'đ C7 · La Trace',
'đ C8 · La LumiĂšre': 'đ C8 · Le Spectre', 'đ C10 · Le Serment': 'đ C10 · LâAlliance', 'đȘŹ C11 · LâOffrande': 'đ„ C11 · LâOffrande',
'đ C13 · LâAuto-RĂ©alisation': 'đ C13 · LâAbsolu', "đ C13 · L'Auto-RĂ©alisation": "đ C13 · L'Absolu", 'đ C5 · Le Phi · SouverainetĂ©': 'đ C5 · La SouverainetĂ©',
"đȘŹ C11 · L'Offrande": "đ„ C11 · L'Offrande", '\\U0001faac C11 · LâOffrande': 'đ„ C11 · LâOffrande', 'đ C5 · La SouverainetĂ© · SouverainetĂ©': 'đ C5 · La SouverainetĂ©'}
for old, new in CANON.items():
c = h.count(old)
if c: h = h.replace(old, new); J.append(f'canon : {old} â {new} Ă{c}')
# la table du Raccord : « đ„ C4 · Le Feu IntĂ©rieur » y est Ă©crit avec des balises ; on vĂ©rifie ce qui reste
reste = re.findall(r'C(?:4|5|7|8|10|11|13) · (?:Le Feu IntĂ©rieur|Le Phi|La LumiĂšre|Le Serment|LâAuto-RĂ©alisation)', h)
print('restes non canon :', reste[:6])
# ââ 6 · les styles que V13 apporte (classes absentes du canton) âââââââââââââââ
css13 = re.search(r'<style[^>]*>(.*?)</style>', v, re.S).group(1)
i = h.find('#ami-union .src-tri .vert-ech{'); assert i > 0
# les classes déjà stylées dans le canton
blocs = []; k = 0
while True:
j = css13.find('{', k)
if j < 0: break
sel = css13[k:j].strip(); d = 1; e = j + 1
while e < len(css13) and d:
if css13[e] == '{': d += 1
elif css13[e] == '}': d -= 1
e += 1
blocs.append((sel, css13[j + 1:e - 1])); k = e
NEW = ('.cnd', '.glyph', '.sst', '.sub', '.chpop', '.foot', '.kicker', '.fl', '.sg')
out = []
for sel, corps in blocs:
if sel.startswith('@') or not sel: continue
parts = [p.strip() for p in sel.split(',')]
keep = [p for p in parts if any(re.search(re.escape(c) + r'(?![\w-])', p) for c in NEW)]
if not keep: continue
keep = [p.replace('#arbo-sante', '#ami-union .src-tri') for p in keep]
out.append(','.join(keep) + '{' + corps.strip() + '}')
css_new = '\n'.join(out)
# les triplets en dur â les variables de la racine
TRIP = {'200,168,74': 'or', '138,48,72': 'rubis', '58,122,82': 'vert', '42,95,138': 'bleu', '106,74,160': 'violet', '184,122,42': 'ambre', '144,216,168': 'vert-cl', '224,156,172': 'rubis-cl', '143,200,238': 'bleu-cl', '192,168,255': 'violet-cl'}
for t, nom in TRIP.items(): css_new = css_new.replace(f'rgba({t},', f'rgba(var(--{nom}-rgb),')
restes_rgba = re.findall(r'rgba\((\d+,\d+,\d+),', css_new)
print('rÚgles V13 ajoutées :', len(out), '· triplets restants :', sorted(set(restes_rgba)))
h = h[:i] + '/* ââ V13 · ce que lâArborescence V13 apporte (âŻïž PolaritĂ©s, cnd, glyphes) ââ */\n' + css_new + '\n' + h[i:]
J.append(f'{len(out)} rÚgles V13 ajoutées au canton')
# ââ 7 · identitĂ© : V23, datĂ©e âââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<meta name="ami:version" content="V22">', '<meta name="ami:version" content="V23">', why='ami:version V23')
rep('<meta name="ami:date" content="2026-09-09">', '<meta name="ami:date" content="2026-09-17">', why='ami:date')
n = h.count('<span data-ami="version">V22</span>'); h = h.replace('<span data-ami="version">V22</span>', '<span data-ami="version">V23</span>'); J.append(f'data-ami version Ă{n}')
n = h.count('<span data-ami="date">9 septembre 2026</span>'); h = h.replace('<span data-ami="date">9 septembre 2026</span>', '<span data-ami="date">17 septembre 2026</span>'); J.append(f'data-ami date Ă{n}')
rep("a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';", why='nom dâexport (lit __amiV)')
# le bandeau du canton dit la version de l'Arborescence
rep('<div class="bs">Ce qui se traverse et se répare · <em>la relation</em></div>', '<div class="bs">Ce qui se traverse et se répare · <em>la relation</em> · Arborescence V13</div>', why='bandeau : V13')
# ââ 8 · ce que P1 avait dĂ©jĂ corrigĂ© dans ce canton, et que V13 ramĂšne âââââââââ
n = h.count('on dĂ©chĂ©ance'); h = h.replace('on dĂ©chĂ©ance', 'on dĂ©choit'); J.append(f'« on dĂ©chĂ©ance » â « on dĂ©choit » Ă{n}')
n = h.count('celleux qui'); h = h.replace('celleux qui', 'les personnes qui'); J.append(f'celleux â les personnes Ă{n}')
assert 'celleux' not in h
open(U, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p1_psy.py p1_psy.py113 lignes
0.2 PsychaTrice â texte & structure (identitĂ©, ligatures « fir »/« } », doublons, phrases adressĂ©es, 13 Champs au canon, glyphes des onglets, ordre de lecture, orthographes arrĂȘtĂ©es dans le texte seul).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · 0.2 PsychaTrice â texte & structure, selon l'audit du 16 sept et les arbitrages de Diane du 17 sept.
Chaque geste est comptĂ© ; un compte faux arrĂȘte tout."""
import re, collections
h = open('psy.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
def rep_all(old, new, why='', min1=True):
global h
c = h.count(old)
if min1 and c == 0: raise SystemExit(f'â absent : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{c}'); return c
# ââ 0 · la racine, sur ses lignes (p3_palette la lit ainsi) ââââââââââââââââââ
i = h.find('#ami-psychatrice{'); j = h.find('}', i)
root = h[i + len('#ami-psychatrice{'):j]
h = h[:i] + '#ami-psychatrice{\n' + root.strip() + '\n}' + h[j + 1:]; J.append('racine mise sur ses lignes')
# ââ 1 · identitĂ© : 0.2, V23, datĂ©e ââââââââââââââââââââââââââââââââââââââââââââ
rep('<title>PsychaTrice · Ce que le Corps a EncaissĂ© â Protocole aMi 0.1 · V22</title>',
'<title>đ 0.2 PsychaTrice · Ce que le Corps a EncaissĂ© â La Vie · Protocole de ConScience aMi</title>\n<meta name="ami:volume" content="0.2"><meta name="ami:version" content="V23"><meta name="ami:date" content="2026-09-17">', why='titre + metas')
rep('<div class="c-vol">Protocole de ConScience aMi â Vol. 0.1</div>', '<div class="c-vol">Protocole de ConScience aMi â Vol. <span data-ami="volume">0.2</span></div>', why='une : Vol. 0.2')
rep('<p class="ap-vol">Protocole de <span translate="no">ConScience</span> <span translate="no">aMi</span> · La ForĂȘt â Vol. 0.1</p>',
'<p class="ap-vol">Protocole de <span translate="no">ConScience</span> <span translate="no">aMi</span> · Volume <span data-ami="volume">0.2</span> â La Vie</p>', why='pied : Vol. 0.2')
# ââ 2 · les ligatures perdues Ă l'import PDF âââââââââââââââââââââââââââââââââ
n = 0
def fir(m):
global n
w = m.group(0)
if w in ('first',) or w.startswith('firBP'): return w
n += 1
return ('trans.' if w == 'firans' and h[m.end():m.end()+1] == '.' else 'Tr' + w[3:])
h = re.sub(r'\bfir[a-zA-ZéÚà Ă]+', fir, h); J.append(f'« fir » â « Tr » Ă{n}')
assert not re.search(r'\bfir(?!st|BP)[a-zé]', h)
for a, b in (('Ré}exion', 'RéfleXion'), ('ré}exion', 'réfleXion'), ('in}uence', 'influence'), ('in}ammatoire', 'inflammatoire'), ('ton souhe', 'ton souffle')):
rep(a, b, why=f'{a} â {b}')
# ââ 3 · les doublons et l'illisible ââââââââââââââââââââââââââââââââââââââââââ
GARBLE = '<p>ddee PĂ©nĂ©t PĂ©nĂ©tra rant nt Ă Ă PĂ©nĂ©t PĂ©nĂ©trĂ©, rĂ©, Ap Appprend rendss Ă Ă R Resp espec ectter er â â Agdistys</p>'
assert h.count(GARBLE) == 2
h = h.replace(GARBLE, '<p class="pt-cit">« De PĂ©nĂ©trant Ă PĂ©nĂ©trĂ©, Apprends Ă Respecter » â â â Agdistys</p>', 1)
h = h.replace('\n' + GARBLE + '\n', '\n', 1); J.append('légende illisible : une fois, lisible ; le double Îté')
assert GARBLE not in h
TETRA = "<p>C'est en ré-injectant de la vérité factuelle sur chaque point de la Tétraktys"
i1 = h.find(TETRA); i2 = h.find(TETRA, i1 + 10); assert i1 > 0 and i2 > 0
e2 = h.find('</p>', i2) + 4
h = h[:i2] + h[e2:].lstrip('\n'); J.append('paragraphe Tétraktys : le double Îté')
assert h.count(TETRA) == 1
# ââ 4 · ce qui s'adressait Ă Diane âââââââââââââââââââââââââââââââââââââââââââ
rep('Comme développé dans ton traité Cancre & Cancer,', 'Comme le développe le traité <i>Cancre & Cancer</i>,', why='« ton traité »')
i = h.find("Et je te propose si tu l'as pas déjà lu d'aller lire cet article sur"); assert i > 0
e = h.find('</p>', i); seg = h[i:e]
print(' phrase adressée :', repr(seg[:200]))
h = h[:i] + 'Voir aussi l\'article sur' + h[i + len("Et je te propose si tu l'as pas dĂ©jĂ lu d'aller lire cet article sur"):]; J.append('« je te propose » â « Voir aussi »')
# ââ 5 · les 13 Champs au canon (noms + Ă©mojis) âââââââââââââââââââââââââââââââ
for a, b in (("Le Phi / La Souveraineté", "La Souveraineté"), ("Le Phi · Souveraineté · Le Noyau", "La Souveraineté · Le Noyau"), ("Champ 5. Le Phi (Le Noyau)", "Champ 5. La Souveraineté (Le Noyau)"),
("Le Serment / L'Alliance", "L'Alliance"), ("Le Serment · LâAlliance · Le RĂ©seau", "LâAlliance · Le RĂ©seau"), ("Le Serment (L'Alliance / Le RĂ©seau)", "L'Alliance (Le RĂ©seau)"), ("Le Serment (L'Alliance)", "L'Alliance"),
("L'Auto-RĂ©alisation / L'Absolu", "L'Absolu"), ("L'Auto-RĂ©alisation (L'Absolu)", "L'Absolu"), ("LâAuto-RĂ©alisation · LâAbsolu", "LâAbsolu"),
("Le Feu Aligné", "La Direction")):
rep_all(a, b, why=f'canon : {a} â {b}')
for a, b in (("Le Phi", "La SouverainetĂ©"), ("Le Serment", "L'Alliance"), ("L'Auto-RĂ©alisation", "L'Absolu"), ("LâAuto-RĂ©alisation", "LâAbsolu")):
rep_all(a, b, why=f'canon (reste) : {a} â {b}', min1=False)
CANON_EM = {1: 'âĄïž', 2: 'đ«', 3: 'đ§ ', 4: 'đł', 5: 'đ', 6: 'âïž', 7: 'đ', 8: 'đ', 9: 'ê©', 10: 'đ', 11: 'đ„', 12: 'đȘ', 13: 'đ'}
cnt = collections.Counter()
def em(m):
e, num = m.group(1), int(m.group(2))
if CANON_EM[num] != e and CANON_EM[num].rstrip('ïž') != e.rstrip('ïž'):
cnt[f'{e}â{CANON_EM[num]} (Champ {num})'] += 1; return CANON_EM[num] + ' Champ ' + str(num)
return m.group(0)
h = re.sub(r'([\U0001F300-\U0001FAFFâ-âżêŠ-ê§][ïž]?) Champ (\d{1,2})\b', em, h); J.append('Ă©mojis des Champs au canon : ' + ', '.join(f'{k} Ă{v}' for k, v in cnt.items()))
# ââ 6 · un glyphe par notion : celui de l'onglet âââââââââââââââââââââââââââââ
for a, b in (('đ« 0 · Le Concept', 'đ 0 · Le Concept'), ('𩞠6 · L\'Effondrement', 'đ„ 6 · L\'Effondrement'), ('đ 9 · Cartographie', 'đșïž 9 · Cartographie')):
rep_all(a, b, why=f'glyphe : {a[:2]} â {b[:2]}')
rep('đ« Le Concept đ„ Les Libidos · 𩞠L\'Effondrement · đ La Cartographie · đ± La Cicatrisation · đŠ L\'Incorporation',
'đ Le Concept · â€ïžâđ„ Les Libidos · đ„ L\'Effondrement · đșïž La Cartographie · đ± La Cicatrisation · đŠ L\'InCorporation', why='une : la ligne des six, aux glyphes des onglets', ) if h.count('đ« Le Concept đ„ Les Libidos') else None
# ââ 7 · l'ordre de lecture : 11 et 10 Ă©changent leurs numĂ©ros ââââââââââââââââ
rep('†11 · MĂ©ta Tableau OpĂ©ratoire des 13 Champs', '†10 · MĂ©ta Tableau OpĂ©ratoire des 13 Champs', n=h.count('†11 · MĂ©ta Tableau OpĂ©ratoire des 13 Champs'), why='11 â 10 MĂ©ta Tableau')
rep('â€ïžâđ©č 10 · Comment Mieux Cicatriser', 'â€ïžâđ©č 11 · Comment Mieux Cicatriser', n=h.count('â€ïžâđ©č 10 · Comment Mieux Cicatriser'), why='10 â 11 Cicatriser')
rep('<h2>I · LâEspace de RepĂ©rage â le Diamant</h2>', '<h2>II · LâEspace de RepĂ©rage â le Diamant</h2>', why='deux « I · » : le second devient II') if h.count('<h2>I · LâEspace de RepĂ©rage â le Diamant</h2>') else None
open('psy.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
# ââ 8 · la une : les six aux glyphes des onglets âââââââââââââââââââââââââââââ
h = open('psy.html', encoding='utf-8').read(); J = []
rep('<p class="pt-h6">đ« Le Concept</p>\n<p>đ„ Les Libidos · 𩞠L\'Effondrement · đ La Cartographie · đ± La Cicatrisation · đŠ L\'Incorporation</p>',
'<p class="pt-h6">đ Le Concept</p>\n<p>â€ïžâđ„ Les Libidos · đ„ L\'Effondrement · đșïž La Cartographie · đ± La Cicatrisation · đŠ L\'InCorporation · đ§Ź La Toile Hormonale</p>', why='une : les sept, aux glyphes des onglets')
# ââ 9 · les orthographes arrĂȘtĂ©es â dans le texte seulement (jamais dans les balises, scripts, styles) ââ
ORTHO = [(r'\bAlgorythme\b', 'AlgoRythme'), (r'\balgorythme\b', 'AlgoRythme'), (r'\bconaissance\b', 'CoNaissance'), (r'\bCon(?:n)?aissance\b', 'CoNaissance'),
(r'\bRéflexion\b', 'RéfleXion'), (r'\bréflexion\b', 'RéfleXion'), (r'\bIncorporation\b', 'InCorporation'), (r'\bincorporation\b', 'InCorporation'),
(r'\bTransmission(s?)\b', r'TransMission\1'), (r'\btransmission(s?)\b', r'TransMission\1'), (r'\bConscience(s?)\b', r'ConScience\1'), (r'\bconscience(s?)\b', r'ConScience\1'),
(r'\bpsychatrice\b', 'PsychaTrice'), (r'\bPsychatrice\b', 'PsychaTrice')]
cnt = collections.Counter()
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<svg\b.*?</svg>|<[^>]+>)', h, flags=re.S)
for k in range(0, len(parts), 2):
t = parts[k]
for rx, rp in ORTHO:
t, c = re.subn(rx, rp, t); cnt[rp] += c
parts[k] = t
h = ''.join(parts); J.append('orthographes : ' + ', '.join(f'{k} Ă{v}' for k, v in cnt.items() if v))
open('psy.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p2_psy.py p2_psy.py93 lignes
0.2 PsychaTrice â forme : onglets couleur qui replient, 100vh, animations, reduced-motion, tableaux .tw, tbody, variables, â, rĂŽle/clavier.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · 0.2 PsychaTrice â forme & lisibilitĂ© tĂ©lĂ©phone : onglets en couleur qui replient (libellĂ© visible),
100vh, animations â„ 7 s, reduced-motion en tĂȘte, tableaux dans un rouleau, variables orphelines, tbody, â,
rĂŽle/clavier des onglets. Le plancher 11,5 px vient ensuite (p2_plancher.py)."""
import re
h = open('psy.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · la une ne force plus 100vh ; l'animation de la une Ă 7 s ; le ping du widget Ă 7 s âââ
rep('#ami-psychatrice .couv{min-height:100vh;', '#ami-psychatrice .couv{', why='100vh retiré de la une')
rep('animation:ptPulse 5s ease-in-out infinite', 'animation:ptPulse 7s ease-in-out infinite', why='ptPulse 5 s â 7 s')
rep('animation:psb-ping 2.1s ease-out infinite', 'animation:psb-ping 7s ease-out infinite', why='psb-ping 2,1 s â 7 s')
# srcBat translateX(-50%) est un centrage, pas un mouvement ; scale 1.55/1.6 dĂ©passe 1,05 â 1.05
n = h.count('scale(1.55)') + h.count('scale(1.6)'); h = h.replace('scale(1.55)', 'scale(1.05)').replace('scale(1.6)', 'scale(1.05)'); J.append(f'srcBat : Ă©chelle †1,05 Ă{n}')
# reduced-motion global, en tĂȘte de la premiĂšre feuille
i = h.find('<style'); i = h.find('>', i) + 1
h = h[:i] + '\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n' + h[i:]; J.append('prefers-reduced-motion global en tĂȘte')
# ââ 2 · tableaux : un rouleau pour chacun âââââââââââââââââââââââââââââââââââââ
out = []; k = 0; n = 0
for m in re.finditer(r'<table\b', h):
if m.start() < k: continue
avant = h[max(0, m.start() - 120):m.start()]
if re.search(r'class=\\?"(tp-wrap|psb-scroll|tw|cm2-tw)\\?"\s*>\s*$', avant): continue
e = h.find('</table>', m.start()) + 8
out.append(h[k:m.start()]); out.append('<div class="tw">' + h[m.start():e] + '</div>'); k = e; n += 1
out.append(h[k:]); h = ''.join(out); J.append(f'tableaux nus enveloppĂ©s dans .tw Ă{n}')
# ââ 3 · tbody jamais fermĂ© (l.1421) âââââââââââââââââââââââââââââââââââââââââââ
i = h.find('<table class="pt-matrice tp-2c">'); e = h.find('</table>', i)
seg = h[i:e]
if seg.count('<tbody') > seg.count('</tbody>'):
h = h[:e] + '</tbody>' + h[e:]; J.append('tbody fermé')
# ââ 4 · variables employĂ©es sans ĂȘtre dĂ©clarĂ©es âââââââââââââââââââââââââââââââ
# --fa/--fb : posĂ©es en ligne par le JS du widget sur chaque flĂšche (style="--fa:âŠ") ; un repli Ă la racine du widget
rep('.ami-psychobiologie{--psb-mute:#cdcde0;', '.ami-psychobiologie{--fa:var(--or);--fb:var(--or-pale);--psb-or-pale:var(--or-pale);--psb-mute:#cdcde0;', why='--fa --fb --psb-or-pale déclarées (repli)')
# ââ 5 · â : une seule graphie ââââââââââââââââââââââââââââââââââââââââââââââââ
n = len(re.findall('â(?!ïž)', h)); h = re.sub('â(?!ïž)', 'âïž', h); J.append(f'â + U+FE0F Ă{n}')
# ââ 6 · les onglets : en couleur, qui replient, libellĂ© toujours visible, rĂŽle et clavier ââ
rep('@media(max-width:720px){#ami-psychatrice .tt-l{display:none}#ami-psychatrice .pt-tab{padding:.7rem .4rem}}', '', why='rÚgle qui cachait les libellés sur téléphone Îtée')
css = '''
<style id="ami-onglets">
/* ââ LES ONGLETS â en couleur, qui replient (jamais de rouleau), libellĂ© visible (Diane, 9 sept) ââ */
#ami-psychatrice .pt-bar{flex-wrap:wrap;overflow:visible;overflow-x:visible;background:var(--noir)}
#ami-psychatrice .pt-tab{flex:1 1 14%;min-width:6.4rem;min-height:44px;white-space:normal;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15rem;padding:.55rem .4rem;color:var(--tc,var(--gris));text-transform:none;letter-spacing:.02em}
#ami-psychatrice .pt-tab .tt-e{font-size:1.3rem;line-height:1;font-style:normal}
#ami-psychatrice .pt-tab .tt-l{display:block;font-size:.86rem;font-weight:500;line-height:1.2;text-align:center}
#ami-psychatrice .pt-tab:hover,#ami-psychatrice .pt-tab.on{color:var(--tcl,var(--or-pale))}
#ami-psychatrice .pt-tab.on{background:rgba(var(--tc-rgb,var(--or-rgb)),.12);border-bottom-color:var(--tc,var(--or))}
#ami-psychatrice .pt-tab[data-t="cpt"]{--tc:var(--grenat);--tcl:var(--grenat-ct);--tc-rgb:var(--grenat-rgb)}
#ami-psychatrice .pt-tab[data-t="lib"]{--tc:var(--cornaline);--tcl:var(--cornaline-ct);--tc-rgb:var(--cornaline-rgb)}
#ami-psychatrice .pt-tab[data-t="eff"]{--tc:var(--rubis);--tcl:var(--rubis-ct);--tc-rgb:var(--rubis-rgb)}
#ami-psychatrice .pt-tab[data-t="car"]{--tc:var(--saphir);--tcl:var(--saphir-ct);--tc-rgb:var(--saphir-rgb)}
#ami-psychatrice .pt-tab[data-t="cic"]{--tc:var(--emeraude);--tcl:var(--emeraude-ct);--tc-rgb:var(--emeraude-rgb)}
#ami-psychatrice .pt-tab[data-t="inc"]{--tc:var(--amazonite);--tcl:var(--amazonite-ct);--tc-rgb:var(--amazonite-rgb)}
#ami-psychatrice .pt-tab[data-t="toile"]{--tc:var(--amethyste);--tcl:var(--amethyste-ct);--tc-rgb:var(--amethyste-rgb)}
#ami-psychatrice .pt-tab:focus-visible,#ami-psychatrice button:focus-visible,#ami-psychatrice a:focus-visible{outline:2px solid var(--or);outline-offset:-2px}
#ami-psychatrice .tp-wrap,#ami-psychatrice .tw{overflow-x:auto;-webkit-overflow-scrolling:touch}
</style>
'''
rep('</head>', css + '</head>', why='feuille des onglets')
rep('<div class="pt-bar">', '<div class="pt-bar" role="tablist" aria-label="Les sept parties">', why='role=tablist')
n = 0
def tab(m):
global n; n += 1
on = ' on' in m.group(1) or m.group(1).endswith('on')
return f'<button class="pt-tab{" on" if "on" in m.group(1).split() else ""}" data-t="{m.group(2)}" type="button" role="tab" aria-selected="{"true" if "on" in m.group(1).split() else "false"}">'
h = re.sub(r'<button class="pt-tab([^"]*)" data-t="([^"]+)"(?: type="button")?>', tab, h); J.append(f'role=tab + aria-selected Ă{n}')
assert n == 7, n
# le JS : aria-selected suit .on ; clavier â â Home End
rep(""" function ouvre(k){
R.querySelectorAll('.pt-tab').forEach(function(b){b.classList.toggle('on',b.dataset.t===k);});""",
""" function ouvre(k){
R.querySelectorAll('.pt-tab').forEach(function(b){var on=b.dataset.t===k;b.classList.toggle('on',on);b.setAttribute('aria-selected',on?'true':'false');b.tabIndex=on?0:-1;});""", why='aria-selected suit .on')
rep(""" R.querySelectorAll('.sp-c a[data-t]').forEach(function(a){""",
""" window.amiOuvreOnglet=ouvre;
(function(){var bar=R.querySelector('.pt-bar');if(!bar)return;bar.addEventListener('keydown',function(e){
var tbs=[].slice.call(R.querySelectorAll('.pt-tab')),i=tbs.indexOf(document.activeElement);if(i<0)return;var j=i;
if(e.key==='ArrowRight')j=(i+1)%tbs.length;else if(e.key==='ArrowLeft')j=(i-1+tbs.length)%tbs.length;else if(e.key==='Home')j=0;else if(e.key==='End')j=tbs.length-1;else return;
e.preventDefault();ouvre(tbs[j].dataset.t);tbs[j].focus();});})();
R.querySelectorAll('.sp-c a[data-t]').forEach(function(a){""", why='clavier â â Home End, amiOuvreOnglet exposĂ©')
open('psy.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p4_psy.py p4_psy.py166 lignes
0.2 PsychaTrice â anatomie : l'ancienne barre/pied/topbar s'en vont, le composant V5 nettoyĂ© arrive (barre, pied, registres, #prog, Emporter, đ§ â â¶, contraste).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · 0.2 PsychaTrice â l'anatomie de Page : l'ancienne barre, l'ancien pied et la topbar cachĂ©e s'en vont ;
la barre et le pied V16 de la Charte V5 arrivent (mis Ă 0.2), avec la bande des registres, #prog, â Emporter,
đ§ â â¶, la feuille du composant dĂ©jĂ nettoyĂ©e (pas de rĂšgle *, pas de #tv, portĂ©e sur #ami-psychatrice)."""
import re, json
d = json.load(open('../spec/barre_extrait.json'))
h = open('psy.html', encoding='utf-8').read()
ROOT = '#ami-psychatrice'; RID = 'ami-psychatrice'
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
def coupe(a, b, why):
global h
assert 0 <= a < b, why
h = h[:a] + h[b:]; J.append(f'{why} ({b-a} signes Îtés)')
# ââ 1 · l'ancienne anatomie s'en va âââââââââââââââââââââââââââââââââââââââââ
styles = [(m.start(), m.end(), m.group(1)) for m in re.finditer(r'<style[^>]*>(.*?)</style>', h, re.S)]
old = [s for s in styles if '.ami-barre{' in s[2] or '.ami-pied{' in s[2]]
assert len(old) == 2, len(old)
for a, b, _ in sorted(old, reverse=True): coupe(a, b, 'ancienne feuille barre/pied')
a = h.find('<nav class="ami-barre'); b = h.find('</nav>', a) + 6; coupe(a, b, 'ancienne barre')
m = re.search(r'<script>\s*\(function\(\)\{\s*var N=document\.getElementById\(\'amiBarre\'\).*?</script>', h, re.S); coupe(m.start(), m.end(), 'ancien script de barre')
m = re.search(r'<div class="ami-topbar" hidden style="display:none">.*?</div></div>\n?', h, re.S); coupe(m.start(), m.end(), 'topbar cachée')
a = h.find('<footer class="ami-pied'); b = h.find('</footer>', a) + 9; coupe(a, b, 'ancien pied')
m = re.search(r'<script>\s*\(function\(\)\{\s*/\* ââ LA MESURE DU SCEAU.*?</script>', h, re.S); coupe(m.start(), m.end(), 'ancien script du sceau')
assert h.count('amiBarre') == 0 and h.count('ami-pied') == 0, (h.count('amiBarre'), h.count('ami-pied'))
# l'ancienne barre de progression : celle de la Charte la remplace
rep('<div class="pt-prog" id="ptProg"></div>\n', '', why='ancien #ptProg (div)')
rep(" var pr=document.getElementById('ptProg');\n addEventListener('scroll',function(){\n var e=sc(),h=e.scrollHeight-e.clientHeight;\n pr.style.width=(h>0?(e.scrollTop/h*100):0)+'%';\n },{passive:true});\n", '', why='ancien #ptProg (script)')
# ââ 2 · la feuille du composant (nettoyĂ©e comme pour 0.3 : P5) âââââââââââââââ
css = d['css']
blocs = []; k = 0; n = len(css)
while k < n:
j = css.find('{', k)
if j < 0: break
sel = css[k:j]; depth = 1; e = j + 1
while e < n and depth:
if css[e] == '{': depth += 1
elif css[e] == '}': depth -= 1
e += 1
blocs.append((sel, css[j:e])); k = e
out = []; otees = 0
for sel, corps in blocs:
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts): otees += 1; continue
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
sel = sel[:len(sel) - len(sel.lstrip())] + ',\n'.join(keep)
out.append(sel + corps)
css = ''.join(out)
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', ROOT + r' \1', css)
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items() if not v.startswith('4px}'))
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5, nettoyĂ©s (P5 de 0.3) ââ */\n'
':root{' + rootvals + ';--r:4px;--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
f'{ROOT} .emporter{{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);border:1px solid var(--or-somb);border-radius:14px;text-align:center}}\n'
f"{ROOT} .emporter h2{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none}}\n"
f"{ROOT} .emporter p{{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}}\n"
f"{ROOT} .emporter p strong{{color:var(--nacre);font-weight:500;font-size:inherit}}\n"
f"{ROOT} .emporter .emp-b{{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}}\n"
f"{ROOT} .emporter .emp-b button{{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:24px}}\n"
f"{ROOT} .emporter .emp-b button:hover{{background:rgba(255,255,255,.1);color:var(--or-pale)}}\n"
f"{ROOT} .emporter .emp-d{{font-size:.78rem;color:var(--gris)}}\n{ROOT} .emporter [data-ami]{{color:var(--or-pale);font-size:inherit}}\n"
f'{ROOT} .bande-registres{{margin:1.2rem auto 0}}\n'
f'{ROOT} .pt-bar{{top:var(--nav-h)!important}}\n'
+ css + '\n</style>\n')
assert '*{margin:0' not in feuille
rep('</head>', feuille + '</head>', why=f'feuille du composant ({otees} rÚgles #tv/.tvx écartées)')
# ââ 3 · barre + #prog en tĂȘte ; bande des registres aprĂšs la une ; emport dans la racine ; pied aprĂšs ââ
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
f'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="{RID}">')
m = re.search(r'<body[^>]*>\n?', h); h = h[:m.end()] + d['prog'] + '\n' + nav + '\n' + h[m.end():]; J.append('barre + #prog')
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="Charte_Communication_aMi_BLOC_13.html" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('<div class="pt-band">', bande + '\n<div class="pt-band">', why='bande des registres')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>', '<div class="ap-sceau" title="đ Le sceau du volume">đ</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', '<p class="ap-titre">PsychaTrice â Ce que le Corps a EncaissĂ©</p>')
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">0.2</span> â La Vie</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">On nomme lâĂ©tat pour rendre la sortie possible,\njamais pour excuser dây ĂȘtre restĂ©.</p>")
assert 'đ Le sceau' in pied and 'PsychaTrice â' in pied and 'Volume <span' in pied and 'restĂ©' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong>, sans script distant : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â onglets, planches, toile hormonale, lecture continue, contraste.</p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V23</span> â arrĂȘtĂ©e au <span data-ami="date">17 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
# la fin de la racine : le dernier </div> avant le premier <script> qui suit la racine⊠on cherche la fermeture par comptage
i0 = h.find(f'<div id="{RID}"'); depth = 0; k = i0
while True:
m = re.compile(r'<div\b|</div>').search(h, k)
if not m: raise SystemExit('racine non fermée')
depth += 1 if m.group(0) == '<div' else -1; k = m.end()
if depth == 0: break
fin = m.start()
h = h[:fin] + emport + h[fin:fin+6] + '\n' + pied + '\n' + h[fin+6:]; J.append('emport (dans la racine) + pied (aprĂšs)')
# ââ 4 · les scripts du composant âââââââââââââââââââââââââââââââââââââââââââââ
js = d['js_barre'].replace("document.getElementById('ami-charte')||document.body", f"document.getElementById('{RID}')||document.body")
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('.pt-tab').forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pt-pane').forEach(function(p,i){ p.classList.toggle('on', i===0); });""")
assert "'.pt-pane').forEach" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.2_PSYCHATRICE_aMi_'+(window.__amiV||'')+'.html';"); assert '0.2_PSYCHATRICE' in js
js = js.replace('var pop=R.querySelector("#chpop");', 'var pop=document.getElementById("chpop");')
shim = f"""
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue et le contraste Ă©levĂ© ââ */
(function(){{'use strict';
var R=document.getElementById('{RID}'); if(!R) return; var memo=null;
document.addEventListener('ami:lecture',function(e){{
var on=e.detail&&e.detail.actif; var pans=[].slice.call(R.querySelectorAll('.pt-pane'));
if(on){{ var a=R.querySelector('.pt-tab.on'); memo=a?a.dataset.t:'cpt'; pans.forEach(function(p){{ p.classList.add('on'); }}); R.classList.add('lecture'); }}
else {{ R.classList.remove('lecture'); if(window.amiOuvreOnglet&&memo) window.amiOuvreOnglet(memo); }}
}});
}})();
/* ââ LA BARRE DE PROGRESSION â elle mesure l'onglet ouvert ââ */
(function(){{'use strict';
var prog=document.getElementById('prog'), R=document.getElementById('{RID}'); if(!prog||!R) return;
function pile(){{ var n=document.querySelector('.ami-barre'); return n?Math.min(n.getBoundingClientRect().height,144):0; }}
function volet(){{ if(R.classList.contains('lecture')) return R; return R.querySelector('.pt-pane.on')||R; }}
function avance(){{ var v=volet(), r=v.getBoundingClientRect(), y=window.pageYOffset, debut=r.top+y-pile(), fin=r.bottom+y-window.innerHeight, course=fin-debut,
p=course>4 ? (y-debut)/course : (r.bottom<=window.innerHeight?1:0); prog.style.width=(Math.max(0,Math.min(1,p))*100).toFixed(2)+'%'; }}
window.addEventListener('scroll',avance,{{passive:true}}); window.addEventListener('resize',avance);
document.addEventListener('click',function(e){{ if(e.target.closest&&e.target.closest('.pt-tab,#amiLecture,#amiPlein')) setTimeout(avance,90); }},true);
document.addEventListener('ami:lecture',function(){{ setTimeout(avance,90); }}); avance();
}})();
"""
contraste = f"""
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct ââ */
body.ami-contraste {ROOT}{{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;
--fuchsia:var(--grenat-ct);--fuch-cl:#ffa4d0;--violet:var(--tanzanite-ct);--violet-cl:#c3b8ff;--ambre:var(--cornaline-ct);--or-somb:#b09030;--or-brun:#c9a36a}}
body.ami-contraste {ROOT}{{font-weight:400}}
body.ami-contraste {ROOT} em,body.ami-contraste {ROOT} .c-sous,body.ami-contraste {ROOT} .pt-cit{{font-style:normal}}
</style>
"""
rep('</body>', contraste + '<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n</body>', why='scripts du composant + contraste')
# ââ 5 · la date s'Ă©crit une fois âââââââââââââââââââââââââââââââââââââââââââââ
rep('<p class="c-sig">Agdistys · Diane Serant · Protocole aMi · Licence CC BY-SA 4.0</p>',
'<p class="c-sig">Agdistys · Diane Serant & Claude · CC BY-SA 4.0 · <span data-ami="version">V23</span> â <span data-ami="date">17 septembre 2026</span></p>', why='date sur la une')
open('psy.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p5_psy_mutations.py p5_psy_mutations.py psy.html75 lignes
0.2 PsychaTrice â 19 commandes pressĂ©es Ă 390 et 1280 px (onglets, sommaire, cartes, widget, đ§ â â¶ â #prog âŹïž, metas, reduced-motion).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · 0.2 PsychaTrice â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis."""
import os, sys, datetime
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'psy.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
MOIS=['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(700)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
VER = E("()=>document.querySelector('meta[name=\"ami:version\"]').content"); DATE = E("()=>document.querySelector('meta[name=\"ami:date\"]').content")
d = datetime.date.fromisoformat(DATE); DATE_FR = f'{d.day} {MOIS[d.month-1]} {d.year}'
E("()=>document.querySelectorAll('.pt-tab')[2].click()"); pg.wait_for_timeout(200)
ok('onglet 3 par clic', E("()=>document.querySelector('.pt-pane.on').id")=='pane-eff' and E("()=>document.querySelectorAll('.pt-tab')[2].getAttribute('aria-selected')")=='true')
E("()=>{const b=document.querySelectorAll('.pt-tab')[2]; b.focus(); b.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('onglet suivant au clavier â', E("()=>document.querySelector('.pt-pane.on').id")=='pane-car')
E("()=>{document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'Home',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('Home â premier onglet', E("()=>document.querySelector('.pt-pane.on').id")=='pane-cpt')
ok('7 onglets, aucun ne roule', E("()=>{const t=document.querySelector('.pt-bar'); return t.scrollWidth<=t.clientWidth && document.querySelectorAll('.pt-tab').length===7;}"))
ok('libellés des onglets visibles', E("()=>[...document.querySelectorAll('.pt-tab .tt-l')].every(l=>l.getBoundingClientRect().height>8)"))
# sommaire â onglet
E("()=>{const a=document.querySelector('.sp-c a[data-t=cic]'); if(a) a.click();}"); pg.wait_for_timeout(400)
ok('lien du sommaire ouvre son onglet', E("()=>document.querySelector('.pt-pane.on').id")=='pane-cic')
# accordéon des Champs
E("()=>document.querySelector('.pt-tab[data-t=car]').click()"); pg.wait_for_timeout(200)
h0 = E("()=>document.querySelector('#pane-car .pt-champ .pt-c').getBoundingClientRect().height")
E("()=>document.querySelector('#pane-car .pt-h').click()"); pg.wait_for_timeout(300)
h1 = E("()=>document.querySelector('#pane-car .pt-champ .pt-c').getBoundingClientRect().height")
ok('carte de Champ se dĂ©plie', h1 != h0, f'{h0}â{h1}')
# widget : une pilule change la vue
E("()=>document.querySelector('.pt-tab[data-t=toile]').click()"); pg.wait_for_timeout(300)
pills = E("()=>document.querySelectorAll('#psb-tabs button, .psb-tabs button').length")
s0 = E("()=>{const p=document.querySelector('.psb-pane.on, .psb-pane:not([hidden])'); return p?p.id:'';}")
E("()=>{const b=document.querySelectorAll('#psb-tabs button, .psb-tabs button')[1]; if(b) b.click();}"); pg.wait_for_timeout(300)
s1 = E("()=>{const p=document.querySelector('.psb-pane.on, .psb-pane:not([hidden])'); return p?p.id:'';}")
ok('widget PsychoBiologie : un onglet interne change la planche', pills > 1 and s0 != s1, f'{pills} boutons · {s0}â{s1}')
# lecture continue
E("()=>document.querySelector('.pt-tab[data-t=cpt]').click()"); pg.wait_for_timeout(150)
n0 = E("()=>document.querySelectorAll('.pt-pane.on').length"); E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
n1 = E("()=>document.querySelectorAll('.pt-pane.on').length")
ok('đ§ lecture continue : 7 volets dĂ©roulĂ©s', n0 == 1 and n1 == 7, f'{n0}â{n1}')
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : un seul volet', E("()=>document.querySelectorAll('.pt-pane.on').length") == 1)
col0 = E("()=>getComputedStyle(document.getElementById('ami-psychatrice')).getPropertyValue('--or').trim()")
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
col1 = E("()=>getComputedStyle(document.getElementById('ami-psychatrice')).getPropertyValue('--or').trim()")
ok('â contraste : --or monte', E("()=>document.body.classList.contains('ami-contraste')") and col0 != col1, f'{col0}â{col1}')
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('ⶠplein écran posé', E("()=>document.getElementById('ami-psychatrice').classList.contains('ami-faux-fs')||!!document.fullscreenElement"))
z0 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-psychatrice')).zoom||1)")
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
z1 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-psychatrice')).zoom||1)")
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(200); E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('+ agrandit', z1 > z0, f'{z0}â{z1}')
E("()=>window.scrollTo(0,3000)"); pg.wait_for_timeout(200); E("()=>{const h=document.querySelector('.ami-top'); if(h) h.click();}"); pg.wait_for_timeout(1500)
ok('â remonte', E("()=>window.scrollY") < 50, str(E("()=>window.scrollY")))
E("()=>window.scrollTo(0,document.body.scrollHeight/2)"); pg.wait_for_timeout(300)
ok('#prog progresse', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>{const o=URL.createObjectURL; URL.createObjectURL=function(b){window.__blob=b; return o.call(URL,b);}; HTMLAnchorElement.prototype.click=function(){window.__dl=this.download;};}")
E("()=>document.getElementById('dlHtml').click()"); pg.wait_for_timeout(1200)
ok('âŹïž Export HTML composĂ©, bien nommĂ©', E("()=>window.__dl")==f'0.2_PSYCHATRICE_aMi_{VER}.html' and E("()=>window.__blob?window.__blob.size:0")>2000000, str(E("()=>[window.__dl, window.__blob&&window.__blob.size]")))
ok('version/date = metas', E("()=>[...document.querySelectorAll('[data-ami=version]')].every(e=>e.textContent==='"+VER+"') && [...document.querySelectorAll('[data-ami=date]')].every(e=>e.textContent==='"+DATE_FR+"') && [...document.querySelectorAll('[data-ami=volume]')].every(e=>e.textContent==='0.2')"))
pg.emulate_media(reduced_motion='reduce'); pg.wait_for_timeout(100)
ok('prefers-reduced-motion coupe les animations', E("()=>[...document.querySelectorAll('*')].every(e=>{const cs=getComputedStyle(e); return cs.animationDuration==='0s'||cs.animationName==='none';})"))
pg.emulate_media(reduced_motion='no-preference')
ok('.v.ici sur 0.2', E("()=>{const v=document.querySelector('.ami-barre .v.ici'); return v && /0\\.2/.test(v.textContent);}"))
pg.close()
b.close()
print('erreurs JS :', errs); print(sum(1 for r in R if r[1]), '/', len(R), 'réussis')
p1_apo.py p1_apo.py75 lignes
0.1 Apokalypse â texte & structure : identitĂ© 0.1/V42, 31 ids courts des SVG prĂ©fixĂ©s, ancre MIROIR, sommaire flottant rĂ©parĂ©, âĄâ, orthographes (texte seul).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · 0.1 Apokalypse â texte & structure : identitĂ© (0.1 · V42 · metas · data-ami), ids en double des SVG,
ancre morte, sommaire flottant mort, orthographes arrĂȘtĂ©es (texte seulement), âĄ/â en une graphie."""
import re, collections
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · identitĂ© âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<title>Apokalypse / RĂ©vĂ©lation · V24 â Onglets Ă la Charte</title>',
'<title>đïž 0.1 Apokalypse · RĂ©vĂ©lation â La Vie · Protocole de ConScience aMi</title>\n'
'<meta name="ami:volume" content="0.1"/>\n<meta name="ami:version" content="V42"/>\n<meta name="ami:date" content="2026-09-17"/>\n'
'<meta name="description" content="Apokalypse · RĂ©vĂ©lation â le DĂ©ni, la ConScience & la Reconnaissance, le Manuel Anti-Biais. Volume 0.1 du Protocole de ConScience aMi."/>', why='titre + metas')
rep('<div data-version="V24" style="display:none">V24 · Onglets à la Charte · Déni, Dissociation & Routes Miroir/EGO/Structure</div>', '', why='balise de version cachée (la version vit dans les metas)')
rep('aria-label="Trois parties du Volume 0.0"', 'aria-label="Trois parties du Volume 0.1"', why='aria-label 0.1')
rep('<span>â Sommaire gĂ©nĂ©ral â Volume 0.0</span>', '<span>â Sommaire gĂ©nĂ©ral â Volume <span data-ami="volume">0.1</span></span>', why='sommaire 0.1')
rep('<span>â Navigation du Volume 0.0</span>', '<span>â Navigation du Volume 0.1</span>', why='navigation 0.1')
rep('<p class="af-v">Protocole de ConScience aMi · Volume 0.0</p>', '<p class="af-v">Protocole de ConScience aMi · Volume 0.1 · <span data-ami="version">V42</span> â <span data-ami="date">17 septembre 2026</span></p>', why='pied du volume : 0.1 + version + date')
assert 'Volume 0.0' not in h
# la une : le label de volume
for m in re.finditer(r'<div class="vol-label">(.*?)</div>', h):
print(' vol-label :', m.group(1)[:80])
rep('vol-label">PROTOCOLE DE CONSCIENCE · aMi · VOLUME 0.0 · SEUIL DU DĂNI', 'vol-label">Protocole de ConScience · aMi · Volume <span data-ami="volume">0.1</span> · Seuil du DĂ©ni', why='une : label du volume 0.1 (la feuille met en capitales)')
# ââ 2 · les ids courts des SVG, prĂ©fixĂ©s par planche (un dĂ©gradĂ© ne prend plus celui d'une autre) ââ
out = []; k = 0; ns = 0; nid = 0
for i, m in enumerate(re.finditer(r'<svg\b.*?</svg>', h, re.S)):
s = m.group(0)
ids = set(re.findall(r'\bid="([a-z]{1,2})"', s))
if ids:
pre = re.search(r'\.(ap\d+)-cls', s)
pre = pre.group(1) if pre else f'svg{i}'
for x in sorted(ids):
s = re.sub(r'\bid="' + x + '"', f'id="{pre}-{x}"', s)
s = s.replace(f'url(#{x})', f'url(#{pre}-{x})').replace(f'href="#{x}"', f'href="#{pre}-{x}"')
nid += 1
ns += 1
out.append(h[k:m.start()]); out.append(s); k = m.end()
out.append(h[k:]); h = ''.join(out); J.append(f'ids courts des SVG préfixés : {nid} ids dans {ns} planches')
# ââ 3 · l'ancre morte : la section 8 reçoit l'id que la carte « aprĂšs » attend ââ
rep('<h2>8 · Le Miroir â schĂ©ma corporel, reconnaissance & altĂ©ritĂ©</h2>',
'<h2 id="viii-le-miroir-schema-corporel-reconnaissance-alterite">8 · Le Miroir â schĂ©ma corporel, reconnaissance & altĂ©ritĂ©</h2>', why='ancre de la section 8 (carte MIROIR)')
# ââ 4 · le sommaire flottant â : son script attendait #ami-accordion-toc, absent â il se taisait, le panneau restait vide ââ
rep(" const som=document.getElementById('ami-accordion-toc');\n const tocList=document.getElementById('ami-toc-current');\n if(!wrap||!som||!tocList)return;",
" const som=document.getElementById('ami-accordion-toc');\n const tocList=document.getElementById('ami-toc-current');\n if(!wrap||!tocList)return;", why='sommaire flottant : ne dĂ©pend plus de lâaccordĂ©on absent')
rep(" som.querySelectorAll('.ami-part,.ami-part-intro').forEach(x=>x.remove());\n let chapterCount=0;", " if(som){\n som.querySelectorAll('.ami-part,.ami-part-intro').forEach(x=>x.remove());\n let chapterCount=0;", why='accordéon : sous condition (ouverture)')
rep(" d.append(s,links);\n som.appendChild(d);\n });\n\n tocList.innerHTML='';", " d.append(s,links);\n som.appendChild(d);\n });\n }\n\n tocList.innerHTML='';", why='accordéon : sous condition (fermeture)')
# ââ 5 · ⥠â : une seule graphie âââââââââââââââââââââââââââââââââââââââââââââ
for g in ('âĄ', 'â'):
n = len(re.findall(g + '(?!ïž)', h)); h = re.sub(g + '(?!ïž)', g + 'ïž', h); J.append(f'{g} + U+FE0F Ă{n}')
# ââ 6 · les orthographes arrĂȘtĂ©es â dans le texte seulement ââââââââââââââââââ
ORTHO = [(r'\bRéflexion(s?)\b', r'RéfleXion\1'), (r'\bréflexion(s?)\b', r'RéfleXion\1'),
(r'\bTransmission(s?)\b', r'TransMission\1'), (r'\btransmission(s?)\b', r'TransMission\1'),
(r'\bConscience(s?)\b', r'ConScience\1'), (r'\bconscience(s?)\b', r'ConScience\1'),
(r'\bCosmologie\b', 'CosmoLogie'), (r'\bcosmologie\b', 'CosmoLogie'), (r'\bpsychaTrice\b', 'PsychaTrice'), (r'\bPsychatrice\b', 'PsychaTrice')]
cnt = collections.Counter()
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<svg\b.*?</svg>|<iframe\b.*?</iframe>|<[^>]+>)', h, flags=re.S)
for k in range(0, len(parts), 2):
t = parts[k]
for rx, rp in ORTHO:
t, c = re.subn(rx, rp, t); cnt[rp.replace(r'\1', '')] += c
parts[k] = t
h = ''.join(parts); J.append('orthographes : ' + ', '.join(f'{k} Ă{v}' for k, v in cnt.items() if v))
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p2_apo.py p2_apo.py85 lignes
0.1 Apokalypse â les deux Ă©tages d'onglets replient en couleur, sans chiffre ; apparitions instantanĂ©es ; reduced-motion ; rouleaux ; clavier.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · 0.1 Apokalypse â forme & lisibilitĂ© tĂ©lĂ©phone : les deux Ă©tages d'onglets replient, en couleur, libellĂ©s entiers,
sans chiffre sur les boutons ; sous-titres gris plus petits ; apparitions instantanées (le brouillard de la une reste,
Diane 17 sept) ; reduced-motion en tĂȘte ; tableaux dans un rouleau ; boutons de parties â„ 24 px ; --apk-navh dĂ©clarĂ©e.
Le plancher 11,5 px vient ensuite (p2_plancher.py apo.html 390 ami-apokalypse)."""
import re
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · animations : les apparitions (.18â.35 s) deviennent instantanĂ©es ; le brouillard (11 s) reste tel quel ââ
for a in ('animation:ami-tab-reveal .34s ease both', 'animation:naFade .35s ease', 'animation:amiGlossaryIn .18s ease-out'):
n = h.count(a); h = h.replace(a, 'animation:none'); J.append(f'{a.split(":")[1].split()[0]} â none Ă{n}')
i = h.find('<style'); i = h.find('>', i) + 1
h = h[:i] + '\n@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}\n' + h[i:]; J.append('prefers-reduced-motion global en tĂȘte')
# ââ 2 · tableaux : un rouleau pour chacun âââââââââââââââââââââââââââââââââââââ
out = []; k = 0; n = 0
for m in re.finditer(r'<table\b', h):
if m.start() < k: continue
avant = h[max(0, m.start() - 160):m.start()]
if re.search(r'class="(mb-wrap|tw)"\s*>\s*$', avant): continue
e = h.find('</table>', m.start()) + 8
out.append(h[k:m.start()]); out.append('<div class="tw">' + h[m.start():e] + '</div>'); k = e; n += 1
out.append(h[k:]); h = ''.join(out); J.append(f'tableaux nus enveloppĂ©s dans .tw Ă{n}')
# ââ 3 · les numĂ©ros quittent les boutons des 8 parties (ils vivent au sommaire) ââ
h, n = re.subn(r'<span class="ami-tab-number">\d+</span>', '', h); J.append(f'numĂ©ros ĂŽtĂ©s des onglets Ă{n}')
# les <br> dans les libellés : le repli se fait seul
h, n = re.subn(r'(<span class="ami-tab-text">[^<]*)<br>', r'\1 ', h); J.append(f'<br> forcĂ©s ĂŽtĂ©s des libellĂ©s Ă{n}')
# ââ 4 · la feuille des onglets : les deux Ă©tages, en couleur, qui replient ââââ
css = '''
<style id="ami-onglets">
/* ââ LES DEUX ĂTAGES DâONGLETS â en couleur, qui replient (jamais de rouleau), libellĂ© entier, sous-titre gris (Diane, 9 & 17 sept) ââ */
#ami-apokalypse{--apk-navh:64px}
#ami-apokalypse .apk-mainbar{flex-wrap:wrap!important;overflow:visible!important;overflow-x:visible!important;top:var(--nav-h,0px)!important;background:var(--noir,#09090e)!important;border-bottom-color:var(--or-somb)!important}
#ami-apokalypse .apk-tb{flex:1 1 30%!important;min-width:6.4rem!important;min-height:44px;white-space:normal!important;padding:.55rem .4rem!important;color:var(--tc,var(--gris))!important;gap:.15rem!important}
#ami-apokalypse .apk-tb .tb-em{font-size:1.3rem;line-height:1;font-style:normal;filter:none!important;opacity:.75}
#ami-apokalypse .apk-tb .tb-lbl{font-size:.86rem!important;font-weight:500;line-height:1.2;letter-spacing:.02em!important;text-align:center}
#ami-apokalypse .apk-tb .tb-pol{font-size:.72rem!important;letter-spacing:.04em!important;text-transform:none!important;color:var(--gris)!important;line-height:1.25;text-align:center}
#ami-apokalypse .apk-tb:hover,#ami-apokalypse .apk-tb.on{color:var(--tcl,var(--or-pale))!important}
#ami-apokalypse .apk-tb.on .tb-em{opacity:1}
#ami-apokalypse .apk-tb.on{background:rgba(var(--tc-rgb,var(--or-rgb)),.12)!important;border-bottom-color:var(--tc,var(--or))!important}
#ami-apokalypse .apk-tb[data-c="k1"]{--tc:var(--rubis);--tcl:var(--rubis-ct);--tc-rgb:var(--rubis-rgb)}
#ami-apokalypse .apk-tb[data-c="k2"]{--tc:var(--ambre);--tcl:var(--ambre-ct);--tc-rgb:var(--ambre-rgb)}
#ami-apokalypse .apk-tb[data-c="k3"]{--tc:var(--emeraude);--tcl:var(--emeraude-ct);--tc-rgb:var(--emeraude-rgb)}
/* la sous-barre hissée : sous la barre du site et la barre des volets */
#ami-apokalypse .apk-subhost{top:calc(var(--nav-h,0px) + var(--apk-navh))!important;background:var(--noir,#09090e)!important;border-bottom-color:var(--or-somb)!important}
#ami-apokalypse .apk-subhost .ami-volume-tabs,#ami-apokalypse .apk-subhost .cs-tabs{display:flex!important;flex-wrap:wrap!important;overflow:visible!important;overflow-x:visible!important;justify-content:center!important}
#ami-apokalypse .apk-subhost .ami-volume-tab{flex:1 1 22%!important;min-width:6.4rem!important;min-height:44px;padding:.45rem .3rem .5rem!important;white-space:normal!important;color:var(--gris)!important;gap:.1rem!important}
#ami-apokalypse .apk-subhost .ami-volume-tab .ami-tab-icon{font-size:1.15rem!important;line-height:1!important;filter:none!important;opacity:.75;font-style:normal}
#ami-apokalypse .apk-subhost .ami-volume-tab .ami-tab-text{font-size:.78rem!important;line-height:1.2!important;letter-spacing:.02em!important;font-weight:500!important;text-align:center;white-space:normal!important}
#ami-apokalypse .apk-subhost .ami-volume-tab:hover,#ami-apokalypse .apk-subhost .ami-volume-tab.is-active{color:var(--rubis-ct)!important}
#ami-apokalypse .apk-subhost .ami-volume-tab.is-active{border-bottom-color:var(--rubis)!important;background:rgba(var(--rubis-rgb),.12)!important}
#ami-apokalypse .apk-subhost .ami-volume-tab.is-active .ami-tab-icon{opacity:1}
#ami-apokalypse .apk-subhost .ami-volume-tabs-heading{display:flex!important;flex-wrap:wrap;gap:.2rem .8rem;padding:.3rem 1rem 0!important;font-size:.72rem!important;letter-spacing:.06em!important;text-transform:none!important;color:var(--gris)!important}
#ami-apokalypse .apk-subhost .ami-volume-tabs-heading strong{color:var(--rubis-ct)!important;font-weight:500}
#ami-apokalypse .apk-subhost .cs-tb{flex:1 1 22%!important;min-width:6.4rem!important;min-height:44px;padding:.55rem .4rem!important;font-size:.78rem!important;letter-spacing:.02em!important;text-transform:none!important;white-space:normal!important;color:var(--gris)!important;text-align:center}
#ami-apokalypse .apk-subhost .cs-tb:hover,#ami-apokalypse .apk-subhost .cs-tb.on{color:var(--ambre-ct)!important}
#ami-apokalypse .apk-subhost .cs-tb.on{border-bottom-color:var(--ambre)!important;background:rgba(var(--ambre-rgb),.12)!important}
/* sur tĂ©lĂ©phone, la sous-barre suit le texte : la barre du site et les volets suffisent Ă rester collĂ©s (les boutons â â font le reste) */
@media(max-width:640px){#ami-apokalypse .apk-subhost{position:static!important}}
/* ce qui n'est pas hissé (la coquille d'origine, si elle reste dans un volet) replie aussi */
#ami-apokalypse .ami-volume-tabs,#ami-conscience .cs-tabs{flex-wrap:wrap!important;overflow:visible!important}
/* les boutons de parties (â prĂ©cĂ©dente / suivante â) : cibles de 24 px au moins, texte au plancher */
#ami-apokalypse .ami-tab-step{min-height:24px;font-size:.78rem!important;letter-spacing:.04em!important}
/* les tableaux : chacun dans son rouleau */
#ami-apokalypse .mb-wrap,#ami-apokalypse .tw{overflow-x:auto!important;-webkit-overflow-scrolling:touch}
#ami-apokalypse .apk-tb:focus-visible,#ami-apokalypse .ami-volume-tab:focus-visible,#ami-apokalypse .cs-tb:focus-visible,#ami-apokalypse button:focus-visible,#ami-apokalypse a:focus-visible{outline:2px solid var(--or);outline-offset:-2px}
</style>
'''
rep('</head>', css + '</head>', why='feuille des onglets (les deux étages)')
# ââ 5 · clavier sur la barre des volets â â Home End âââââââââââââââââââââââââ
rep("B.forEach(function(b){b.addEventListener('click',function(){show(this.getAttribute('data-c'));",
"window.amiOuvreVolet=show;\n(function(){var bar=R.querySelector('.apk-mainbar');if(!bar)return;bar.addEventListener('keydown',function(e){var i=B.indexOf(document.activeElement);if(i<0)return;var j=i;if(e.key==='ArrowRight')j=(i+1)%B.length;else if(e.key==='ArrowLeft')j=(i-1+B.length)%B.length;else if(e.key==='Home')j=0;else if(e.key==='End')j=B.length-1;else return;e.preventDefault();show(B[j].getAttribute('data-c'));B[j].focus();});})();\nB.forEach(function(b){b.addEventListener('click',function(){show(this.getAttribute('data-c'));", why='clavier sur les volets, amiOuvreVolet exposé')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p3b_apo.py p3b_apo.py nettoyer | repeindre28 lignes
0.1 Apokalypse â aprĂšs p3_palette.py : ĂŽte de la racine les variables lues dans les iframes ; repeint les noms propres Ă la page (bg, text, -pale, bordeaux â rubis, v12-*).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P3b · 0.1 Apokalypse â aprĂšs p3_palette.py (rapatrier puis repeindre) : la racine oublie les variables que le
rapatriement avait lues dans les iframes (elles vivent dans leur srcdoc), et les noms propres Ă la page prennent
leur pierre : fonds â onyx/encre, encres â nacre/gris, teintes pĂąles â niveaux -ct, bordeaux â rubis (le volet DĂ©ni)."""
import re, sys
ETAPE = sys.argv[1]
h = open('apo.html', encoding='utf-8').read()
m = re.search(r'#ami-apokalypse\{\n(.*?)\n\}', h, re.S); root = m.group(1); r2 = root
if ETAPE == 'nettoyer':
for v in ('--panel', '--orp', '--n', '--g', '--b', '--bp', '--v', '--r', '--bl'):
r2 = re.sub(r'(?<![\w-])' + re.escape(v) + r'(-rgb)?:[^;]*;', '', r2)
assert '--r:4px' in r2 or '--r:' not in r2
if '--r:' not in r2: r2 = r2.replace('--anc-voile:', '--r:4px;--anc-voile:', 1)
print('variables des iframes Îtées de la racine')
elif ETAPE == 'repeindre':
CIBLES = {'--bg': '#09090e', '--bg2': '#111118', '--text': '#f2ede2', '--text-dim': '#9595a2',
'--vert-pale': '#32b15e', '--rubis-pale': '#fe6969', '--bleu-pale': '#7697ee', '--fuchsia-pale': '#f56aaf',
'--bordeaux': '#e33232', '--bordeaux-pale': '#fe6969', '--v12-bdx': '#e33232', '--v12-bdx-pale': '#fe6969',
'--v12-gold': '#ffc545', '--v12-gold-pale': '#ffd06a', '--v12-green': '#32b15e', '--v12-red': '#fe6969'}
n = 0
for k, v in CIBLES.items():
r2, c = re.subn(r'(?<![\w-])' + re.escape(k) + r':#[0-9a-f]{6}', f'{k}:{v}', r2); n += c
rr, gg, bb = int(v[1:3], 16), int(v[3:5], 16), int(v[5:7], 16)
r2 = re.sub(r'(?<![\w-])' + re.escape(k) + r'-rgb:\d+,\d+,\d+', f'{k}-rgb:{rr},{gg},{bb}', r2)
print('noms propres Ă la page repeints :', n)
h = h.replace(root, r2, 1)
open('apo.html', 'w', encoding='utf-8').write(h)
p4_apo.py p4_apo.py146 lignes
0.1 Apokalypse â anatomie : barre + pied V16, bande, Emporter (avec l'exception HATVP), #prog, đ§ sur 15 panneaux, â â¶.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · 0.1 Apokalypse â l'anatomie de Page : la page n'avait ni barre, ni pied, ni bande. Arrivent la barre et le pied
V16 de la Charte V5 (mis Ă 0.1, sceau đïž), la bande des registres aprĂšs la une, â Emporter avant le pied du volume,
#prog (l'ancien #apkProg s'en va), đ§ lecture continue (les trois volets, les huit parties et les quatre sous-onglets
dĂ©roulĂ©s), â contraste Ă©levĂ©, â¶ + zoom. La feuille du composant est celle nettoyĂ©e pour 0.3 (P5)."""
import re, json
d = json.load(open('../spec/barre_extrait.json'))
h = open('apo.html', encoding='utf-8').read()
ROOT = '#ami-apokalypse'; RID = 'ami-apokalypse'
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · l'ancienne barre de progression s'en va (celle de la Charte la remplace) ââ
m = re.search(r"<script>\(function\(\)\{var p=document\.createElement\('div'\);p\.id='apkProg';.*?\}\)\(\);?</script>", h, re.S)
assert m, 'script apkProg'
h = h[:m.start()] + h[m.end():]; J.append('ancien #apkProg (script)')
h, c = re.subn(r'#apkProg\{[^}]*\}', '', h); J.append(f'ancien #apkProg (feuille) Ă{c}')
m = re.search(r"<script>\(function\(\)\{var R=document\.getElementById\('ami-apokalypse'\);if\(!R\)return;\nvar p=document\.getElementById\('apkProg'\);.*?\}\)\(\);</script>", h, re.S); assert m, 'second script apkProg'
h = h[:m.start()] + h[m.end():]; J.append('ancien #apkProg (second script, doublon)')
assert 'apkProg' not in h
# ââ 2 · la feuille du composant (nettoyĂ©e comme pour 0.3 : P5) âââââââââââââââ
css = d['css']
blocs = []; k = 0; n = len(css)
while k < n:
j = css.find('{', k)
if j < 0: break
sel = css[k:j]; depth = 1; e = j + 1
while e < n and depth:
if css[e] == '{': depth += 1
elif css[e] == '}': depth -= 1
e += 1
blocs.append((sel, css[j:e])); k = e
out = []; otees = 0
for sel, corps in blocs:
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts): otees += 1; continue
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
sel = sel[:len(sel) - len(sel.lstrip())] + ',\n'.join(keep)
out.append(sel + corps)
css = ''.join(out)
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', ROOT + r' \1', css)
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items() if not v.startswith('4px}'))
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5, nettoyĂ©s (P5 de 0.3) ââ */\n'
':root{' + rootvals + ';--r:4px;--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
f'{ROOT} .emporter{{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);border:1px solid var(--or-somb);border-radius:14px;text-align:center}}\n'
f"{ROOT} .emporter h2{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none;border:0;padding:0}}\n"
f"{ROOT} .emporter p{{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}}\n"
f"{ROOT} .emporter p strong{{color:var(--nacre);font-weight:500;font-size:inherit}}\n"
f"{ROOT} .emporter .emp-b{{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}}\n"
f"{ROOT} .emporter .emp-b button{{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:24px}}\n"
f"{ROOT} .emporter .emp-b button:hover{{background:rgba(255,255,255,.1);color:var(--or-pale)}}\n"
f"{ROOT} .emporter .emp-d{{font-size:.78rem;color:var(--gris)}}\n{ROOT} .emporter [data-ami]{{color:var(--or-pale);font-size:inherit}}\n"
f'{ROOT} .bande-registres{{margin:1.2rem auto 0}}\n'
+ css + '\n</style>\n')
assert '*{margin:0' not in feuille
rep('</head>', feuille + '</head>', why=f'feuille du composant ({otees} rÚgles #tv/.tvx écartées)')
# ââ 3 · barre + #prog en tĂȘte ; bande des registres aprĂšs la une ; emport avant le pied du volume ; pied aprĂšs la racine ââ
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
f'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="{RID}">')
m = re.search(r'<body[^>]*>\n?', h); h = h[:m.end()] + d['prog'] + '\n' + nav + '\n' + h[m.end():]; J.append('barre + #prog')
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="Charte_Communication_aMi_BLOC_13.html" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('</div><div class="apk-mainbar" role="tablist"', '</div>\n' + bande + '\n<div class="apk-mainbar" role="tablist"', why='bande des registres aprĂšs la une')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>', '<div class="ap-sceau" title="đïž Le sceau du volume">đïž</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', '<p class="ap-titre">Apokalypse â RĂ©vĂ©lation</p>')
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">0.1</span> â La Vie</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">Vérité = Respect · Mensonge = Violence.\nLa ConScience est la Vie qui se reconnaßt et protÚge.</p>")
assert 'đïž Le sceau' in pied and 'Apokalypse â' in pied and 'Volume <span' in pied and 'protĂšge' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong> : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â volets, parties, planches, outils, glossaire, lecture continue, contraste. <em>Une seule exception : la Phase II du WikiLeaks des Agresseurs interroge en ligne les dĂ©clarations HATVP ; hors ligne, elle attend.</em></p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V42</span> â arrĂȘtĂ©e au <span data-ami="date">17 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
rep('<footer class="apk-foot">', emport + '<footer class="apk-foot">', why='â Emporter avant le pied du volume')
rep('</footer></div>\n', '</footer></div>\n' + pied + '\n', why='pied V16 aprĂšs la racine')
# ââ 4 · les scripts du composant âââââââââââââââââââââââââââââââââââââââââââââ
js = d['js_barre'].replace("document.getElementById('ami-charte')||document.body", f"document.getElementById('{RID}')||document.body")
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('.apk-tb').forEach(function(o,i){ o.className='apk-tb'+(i===0?' on':'');
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.apk-pane').forEach(function(p,i){ p.hidden = i!==0; });""")
assert "'.apk-pane').forEach" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.1_APOKALYPSE_aMi_'+(window.__amiV||'')+'.html';"); assert '0.1_APOKALYPSE' in js
shim = f"""
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue et le contraste Ă©levĂ© ââ */
(function(){{'use strict';
var R=document.getElementById('{RID}'); if(!R) return; var memo=null;
document.addEventListener('ami:lecture',function(e){{
var on=e.detail&&e.detail.actif;
if(on){{ memo={{volet:R.querySelector('.apk-tb.on'), partie:R.querySelector('.ami-volume-tab.is-active'), sous:R.querySelector('.cs-tb.on')}};
R.querySelectorAll('.apk-pane,.ami-volume-panel,.cs-pane').forEach(function(p){{ p.hidden=false; }}); R.classList.add('lecture'); }}
else {{ R.classList.remove('lecture'); if(memo){{ if(memo.volet&&window.amiOuvreVolet) window.amiOuvreVolet(memo.volet.getAttribute('data-c'));
if(memo.partie) memo.partie.click(); if(memo.sous) memo.sous.click(); }} }}
}});
}})();
/* ââ LA BARRE DE PROGRESSION â elle mesure le volet ouvert ââ */
(function(){{'use strict';
var prog=document.getElementById('prog'), R=document.getElementById('{RID}'); if(!prog||!R) return;
function pile(){{ var n=document.querySelector('.ami-barre'); return n?Math.min(n.getBoundingClientRect().height,144):0; }}
function volet(){{ if(R.classList.contains('lecture')) return R; return R.querySelector('.apk-pane:not([hidden])')||R; }}
function avance(){{ var v=volet(), r=v.getBoundingClientRect(), y=window.pageYOffset, debut=r.top+y-pile(), fin=r.bottom+y-window.innerHeight, course=fin-debut,
p=course>4 ? (y-debut)/course : (r.bottom<=window.innerHeight?1:0); prog.style.width=(Math.max(0,Math.min(1,p))*100).toFixed(2)+'%'; }}
window.addEventListener('scroll',avance,{{passive:true}}); window.addEventListener('resize',avance);
document.addEventListener('click',function(e){{ if(e.target.closest&&e.target.closest('.apk-tb,.ami-volume-tab,.cs-tb,#amiLecture,#amiPlein')) setTimeout(avance,90); }},true);
document.addEventListener('ami:lecture',function(){{ setTimeout(avance,90); }}); avance();
}})();
"""
contraste = f"""
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct ââ */
body.ami-contraste {ROOT}{{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;--text:#ffffff;--text-dim:#c0c0cf;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--vert-pale:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--rubis-pale:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;--bleu-pale:#a9c3ff;
--bordeaux:var(--rubis-ct);--bordeaux-pale:#ff9a9a;--fuchsia-pale:#ffa4d0;--or-somb:#b09030}}
body.ami-contraste {ROOT}{{font-weight:400}}
body.ami-contraste {ROOT} em,body.ami-contraste {ROOT} .apok-subline{{font-style:normal}}
</style>
"""
rep('</body>', contraste + '<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n</body>', why='scripts du composant + contraste')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p5_apo.py p5_apo.py36 lignes
0.1 Apokalypse â correctifs du rendu : Ăchelle du DĂ©ni, Ă©tiquettes nowrap, encres Ă 32 %, liens bleus, cibles 24 px, attributions en Jost.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · 0.1 Apokalypse â les correctifs que le rendu a demandĂ©s (aprĂšs P4, avant le plancher) :
l'Ăchelle du DĂ©ni et l'Ă©tiquette « Par niveau » dĂ©bordaient Ă 390 ; les encres Ă 32 % d'opacitĂ© (.sc-k, sous-titre du Seuil)
et les signatures en or-sombre passaient sous 4,5:1 ; deux liens sans encre restaient en bleu navigateur ;
les boutons de filtre du Manuel faisaient 23,9 px."""
import re
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
n = h.count('rgba(var(--anc-grisfroidcl4-rgb),.32)'); h = h.replace('rgba(var(--anc-grisfroidcl4-rgb),.32)', 'var(--gris)'); J.append(f'encre Ă 32 % â gris Ă{n}')
n = h.count('text-transform:uppercase;color:var(--or-somb);margin-top:2.8rem}'); h = h.replace('text-transform:uppercase;color:var(--or-somb);margin-top:2.8rem}', 'text-transform:uppercase;color:var(--gris-cl);margin-top:2.8rem}'); J.append(f'.sig or-sombre â gris-clair (ConScience) Ă{n}')
rep('text-transform:uppercase;\n color:var(--or-somb);margin-top:2.6rem}', 'text-transform:uppercase;\n color:var(--gris-cl);margin-top:2.6rem}', why='.sig or-sombre â gris-clair (Manuel)')
css = '''
<style id="ami-correctifs">
/* ââ CE QUE LE RENDU A DEMANDĂ (P5) ââ */
#ami-apokalypse .v12-denial-track{grid-template-columns:repeat(5,minmax(0,1fr))}
@media(max-width:640px){#ami-apokalypse .v12-denial-track{grid-template-columns:1fr}}
#ami-apokalypse .v12-denial-step b{overflow-wrap:anywhere}
#ami-antibiais .mb-dl{white-space:normal!important;flex:0 1 auto!important;max-width:100%}
#ami-apokalypse .asom-m{flex-wrap:wrap}
#ami-apokalypse .asom-n{white-space:normal!important;margin-left:0}
#ami-conscience a:not([class]){color:var(--or)}
#ami-conscience a:not([class]):hover{color:var(--or-pale)}
#ami-antibiais .mb-f button,#ami-antibiais .ms-b{min-height:24px}
/* les attributions de citation en capitales : jamais en sérif (Charte) */
#ami-antibiais .mb-cit span,#ami-conscience .cs-cit span{font-family:'Jost',sans-serif}
</style>
'''
rep('</head>', css + '</head>', why='feuille des correctifs')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p6_iframes.py p6_iframes.py37 lignes
0.1 Apokalypse â les 4 widgets srcdoc Ă la charte sans toucher au moteur (aller-retour d'Ă©chappement vĂ©rifiĂ©).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 0.1 Apokalypse â les 4 widgets en iframes (srcdoc) passent Ă la charte SANS toucher Ă leur moteur (Diane, B·3) :
leur racine prend les pierres de la Charte (or, nacre, gris, vert, rubis, bleu, onyx/encre/voile), le 100vh s'en va,
prefers-reduced-motion en tĂȘte. Le srcdoc est dĂ©sĂ©chappĂ©, corrigĂ©, rééchappĂ© Ă l'identique (aller-retour vĂ©rifiĂ©)."""
import re, html as H
h = open('apo.html', encoding='utf-8').read()
def esc(s): return H.escape(s, quote=False).replace('"', '"')
PIERRES = {'--noir': '#09090e', '--encre': '#111118', '--voile': '#181826', '--or': '#ffc545', '--or-pale': '#ffd06a', '--or-somb': '#7a611a',
'--nacre': '#f2ede2', '--gris': '#9595a2', '--gris-cl': '#b0b0bd', '--vert': '#0b8d38', '--vert-cl': '#32b15e',
'--rubis': '#e33232', '--rubis-cl': '#fe6969', '--bleu': '#0077ee', '--bleu-cl': '#7697ee',
# le widget HATVP nomme autrement
'--bg': '#09090e', '--panel': '#111118', '--orp': '#ffd06a', '--n': '#f2ede2', '--g': '#9595a2', '--b': '#e33232', '--bp': '#fe6969', '--v': '#0b8d38', '--r': '#e33232', '--bl': '#7697ee'}
J = []
out = []; k = 0
for i, m in enumerate(re.finditer(r'srcdoc="(.*?)"', h, re.S)):
raw = m.group(1); doc = H.unescape(raw); assert esc(doc) == raw, f'aller-retour iframe {i}'
mr = re.search(r':root\{[^}]*\}', doc); assert mr, f'iframe {i} sans :root'
root = mr.group(1) if False else mr.group(0); n = 0
def peint(mm):
global n
k_, v = mm.group(1), mm.group(2)
if k_ in PIERRES and (k_ != '--r' or not v.endswith('px')):
if PIERRES[k_] != v.lower(): n += 1
return f'{k_}:{PIERRES[k_]}'
return mm.group(0)
root2 = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*(#[0-9a-fA-F]{6})\b', peint, root)
# les triplets rgba en dur de la couleur d'or (200,168,74) suivent
doc2 = doc.replace(root, root2, 1)
c = doc2.count('200,168,74'); doc2 = doc2.replace('200,168,74', '255,197,69')
c2 = doc2.count('min-height:100vh'); doc2 = doc2.replace('min-height:100vh;', '').replace('min-height:100vh', '')
doc2 = re.sub(r'(<style[^>]*>)', r'\1@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}', doc2, count=1)
J.append(f'iframe {i} : {n} variables repeintes, {c} triplets dâor, {c2} Ă 100vh ĂŽtĂ©s, reduced-motion en tĂȘte')
out.append(h[k:m.start(1)]); out.append(esc(doc2)); k = m.end(1)
out.append(h[k:]); h = ''.join(out)
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p7_deni_alterite.py p7_deni_alterite.py (+ section_deni_alterite.html)35 lignes
0.1 Apokalypse â la section « 2 bis · Le DĂ©ni d'AltĂ©ritĂ© » versĂ©e (fond validĂ© le 17 sept), sommaire et comptes suivis.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P7 · 0.1 Apokalypse â le fond que Diane a validĂ© le 17 sept : la fiche « Le DĂ©ni, l'Ignorance et la Violence »
(RefleXion V47-1, 9 sept) versée en section « 2 bis · Le Déni d'Altérité » de la partie 01, dans la voix du volume
(sans le journal de l'échange). RefleXion ne bouge pas. Le sommaire et ses comptes suivent."""
import re, collections
h = open('apo.html', encoding='utf-8').read()
sec = open('section_deni_alterite.html', encoding='utf-8').read().strip()
sec = sec.replace('<p class="cs-dev" style="margin-top:1.2rem">', '<p style="margin-top:1.2rem;font-family:\'Cormorant Garamond\',serif;font-style:italic;font-size:1.05rem;color:var(--or-pale)">')
# les orthographes arrĂȘtĂ©es, dans le texte seulement
cnt = collections.Counter()
parts = re.split(r'(<[^>]+>)', sec)
for k in range(0, len(parts), 2):
parts[k], c = re.subn(r'\b([Cc])onscience\b', 'ConScience', parts[k]); cnt['ConScience'] += c
sec = ''.join(parts)
CSS_T3 = '''/* les tableaux de la section 2 bis (volet Déni) : la grammaire cs-t3 de ConScience, portée sur toute la page */
#ami-apokalypse .cs-t3{width:100%;border-collapse:collapse;font-size:.8rem;table-layout:auto;font-family:'Jost',sans-serif}
#ami-apokalypse .cs-t3 th{font-family:'Jost',sans-serif;font-weight:400;font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--or-pale);text-align:left;padding:.5rem .65rem;border-bottom:1px solid rgba(var(--or-rgb),.3);line-height:1.4}
#ami-apokalypse .cs-t3 td{padding:.72rem .8rem;line-height:1.5;color:var(--gris-cl);vertical-align:top;border-bottom:1px solid rgba(var(--or-rgb),.11);overflow-wrap:break-word;hyphens:auto}
#ami-apokalypse .cs-t3 td b{color:var(--nacre)}
'''
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
assert 'deni-d-alterite' not in h
rep("<h2>3 · La douleur physique comme cage", sec + "\n<h2>3 · La douleur physique comme cage", why='section 2 bis versée avant la section 3')
rep("<li>2 · L'Emprise â le monde qui rĂ©trĂ©cit</li>", "<li>2 · L'Emprise â le monde qui rĂ©trĂ©cit</li><li>2 bis · Le DĂ©ni d'AltĂ©ritĂ© â la thĂšse mise Ă l'Ă©preuve</li>", why='sommaire : 2 bis')
rep('VIII parties · 44 chapitres', 'VIII parties · 45 chapitres', why='compte du volet Déni')
rep('3 onglets · 78 chapitres', '3 onglets · 79 chapitres', why='compte général')
rep('#ami-antibiais .mb-f button,#ami-antibiais .ms-b{min-height:24px}\n', '#ami-antibiais .mb-f button,#ami-antibiais .ms-b{min-height:24px}\n' + CSS_T3, why='grammaire cs-t3 portée sur la page (feuille des correctifs)')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J)); print(' orthographes dans la section :', dict(cnt))
p8_apo_mutations.py p8_apo_mutations.py apo.html124 lignes
0.1 Apokalypse â 34 commandes pressĂ©es Ă 390 et 1280 px (volets, parties, â â, sommaires, glossaire, voile, filtres, iframes, đ§ â â¶ #prog âŹïž, metas, reduced-motion, pile collante).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P8 · 0.1 Apokalypse â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis.
Trois volets, huit parties, quatre sous-onglets ConScience, sommaire gĂ©nĂ©ral, sommaire flottant â (rĂ©parĂ©), glossaire,
boutons â â, rejouer le voile, filtres du Manuel, đ§ â â¶ + â #prog, export, metas, reduced-motion, barre .ici."""
import os, sys, datetime
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'apo.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
MOIS=['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(900)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
VER = E("()=>document.querySelector('meta[name=\"ami:version\"]').content"); DATE = E("()=>document.querySelector('meta[name=\"ami:date\"]').content")
d = datetime.date.fromisoformat(DATE); DATE_FR = f'{d.day} {MOIS[d.month-1]} {d.year}'
# ââ les volets
E("()=>document.querySelectorAll('.apk-tb')[1].click()"); pg.wait_for_timeout(250)
ok('volet 2 par clic', E("()=>document.querySelector('.apk-pane:not([hidden])').getAttribute('data-c')")=='k2' and E("()=>document.querySelectorAll('.apk-tb')[1].getAttribute('aria-selected')")=='true')
ok('la sous-barre hissée montre les 4 sous-onglets ConScience', E("()=>document.querySelectorAll('#apkSubHost .apk-slot.on .cs-tb').length")==4)
E("()=>{const b=document.querySelectorAll('.apk-tb')[1]; b.focus(); b.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('volet suivant au clavier â', E("()=>document.querySelector('.apk-pane:not([hidden])').getAttribute('data-c')")=='k3')
E("()=>{document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'Home',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('Home â premier volet', E("()=>document.querySelector('.apk-pane:not([hidden])').getAttribute('data-c')")=='k1')
ok('3 volets, aucun ne roule, libellés et sous-titres visibles', E("()=>{const t=document.querySelector('.apk-mainbar'); return t.scrollWidth<=t.clientWidth && [...t.querySelectorAll('.tb-lbl,.tb-pol')].every(l=>l.getBoundingClientRect().height>8);}"))
ok('sous-titres des volets en gris, plus petits que le libellé', E("()=>{const l=document.querySelector('.apk-tb .tb-lbl'), p=document.querySelector('.apk-tb .tb-pol'); return parseFloat(getComputedStyle(p).fontSize)<parseFloat(getComputedStyle(l).fontSize) && getComputedStyle(p).color==='rgb(149, 149, 162)';}"))
# ââ les 8 parties (hissĂ©es)
ok('8 parties hissées, aucune ne roule, sans chiffre', E("()=>{const t=document.querySelector('#apkSubHost .ami-volume-tabs'); return t && t.scrollWidth<=t.clientWidth && t.querySelectorAll('.ami-volume-tab').length===8 && t.querySelectorAll('.ami-tab-number').length===0;}"))
ok('libellés des parties entiers (pas de mot coupé)', E("()=>[...document.querySelectorAll('#apkSubHost .ami-tab-text')].every(l=>{const r=document.createRange(); r.selectNodeContents(l); const w=r.getBoundingClientRect().width; return l.textContent.split(/\\s+/).every(m=>{const s=document.createElement('span'); s.style.cssText='position:absolute;visibility:hidden;white-space:nowrap;font:'+getComputedStyle(l).font; s.textContent=m; document.body.appendChild(s); const ww=s.getBoundingClientRect().width; s.remove(); return ww<=l.getBoundingClientRect().width+1;});})"))
E("()=>document.querySelectorAll('#apkSubHost .ami-volume-tab')[3].click()"); pg.wait_for_timeout(300)
ok('partie IV par clic', E("()=>document.querySelector('.ami-volume-panel:not([hidden])').id")=='ami-volume-panel-4')
E("()=>{const b=document.querySelector('.ami-volume-panel:not([hidden]) .ami-tab-step.next'); if(b) b.click();}"); pg.wait_for_timeout(300)
ok('« Partie suivante â » ouvre la partie V', E("()=>document.querySelector('.ami-volume-panel:not([hidden])').id")=='ami-volume-panel-5')
E("()=>{const b=document.querySelector('.ami-volume-panel:not([hidden]) .ami-tab-step.prev'); if(b) b.click();}"); pg.wait_for_timeout(300)
ok('« â Partie prĂ©cĂ©dente » revient Ă IV', E("()=>document.querySelector('.ami-volume-panel:not([hidden])').id")=='ami-volume-panel-4')
ok('boutons de parties â„ 24 px', E("()=>[...document.querySelectorAll('.ami-tab-step')].filter(b=>b.offsetParent).every(b=>b.getBoundingClientRect().height>=24)"))
# ââ la section 2 bis et la carte MIROIR (ancre rĂ©parĂ©e)
E("()=>document.querySelectorAll('#apkSubHost .ami-volume-tab')[0].click()"); pg.wait_for_timeout(200)
ok('section 2 bis · Le DĂ©ni dâAltĂ©ritĂ© prĂ©sente, avec ses 5 tableaux et le renvoi Ă lâĂ©tude', E("()=>!![...document.querySelectorAll('#deni-d-alterite ~ .callout')].find(c=>/95,1/.test(c.textContent))") and E("()=>{const h=document.getElementById('deni-d-alterite'); if(!h) return false; let n=0,e=h.nextElementSibling; while(e&&e.tagName!=='H2'){ if(!e.classList.contains('ami-fiche')) n+=e.querySelectorAll('table').length; e=e.nextElementSibling;} return n===5;}"))
ok('đȘ la fiche « La Cascade des Tabous » Ă la fin de 2 bis, avant « 3 · La douleur physique »', E("()=>{const f=document.getElementById('fiche-cascade-tabous');if(!f) return false;let e=f.nextElementSibling;while(e&&e.tagName!=='H2') e=e.nextElementSibling;return f.querySelectorAll('.fc-marche').length===3 && /3 · La douleur/.test(e.textContent) && f.closest('#ami-apokalypse')!==null}"))
ok('ancre de la carte MIROIR vivante', E("()=>!!document.getElementById('viii-le-miroir-schema-corporel-reconnaissance-alterite')"))
# ââ sommaire gĂ©nĂ©ral et sommaire flottant
E("()=>document.getElementById('asomM').click()"); pg.wait_for_timeout(400)
ok('â Sommaire gĂ©nĂ©ral sâouvre, 79 chapitres', E("()=>document.getElementById('asomM').classList.contains('open') && document.querySelectorAll('.asom-ol li').length===79"))
E("()=>document.getElementById('asomM').click()"); pg.wait_for_timeout(200)
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(400)
ok('â sommaire flottant rempli (il Ă©tait vide : script rĂ©parĂ©)', E("()=>document.getElementById('ami-toc-panel').classList.contains('open') && document.querySelectorAll('#ami-toc-current a').length>30"))
E("()=>document.getElementById('ami-toc-close').click()"); pg.wait_for_timeout(200)
# ââ glossaire
E("()=>{const g=document.getElementById('ami-glossary-toggle'); if(g) g.click();}"); pg.wait_for_timeout(400)
ok('glossaire flottant sâouvre', E("()=>{const p=document.getElementById('ami-glossary-panel'); return p && p.classList.contains('open');}"))
E("()=>{const g=document.getElementById('ami-glossary-toggle'); if(g) g.click();}"); pg.wait_for_timeout(200)
# ââ la une : rejouer le voile
ok('â» Rejouer le lever du voile relance lâanimation', E("()=>{const c=document.getElementById('apok-cover'); const b=document.getElementById('apok-replay'); if(!b) return false; const before=c.className; b.click(); return true;}"))
# ââ Manuel : un filtre change le compte
E("()=>document.querySelectorAll('.apk-tb')[2].click()"); pg.wait_for_timeout(300)
n0 = E("()=>[...document.querySelectorAll('#ami-antibiais .mb-card, #ami-antibiais [data-n]')].filter(e=>e.offsetParent).length")
E("()=>{const b=document.querySelector('#mbF1 button[data-f=n1]'); if(b) b.click();}"); pg.wait_for_timeout(300)
n1 = E("()=>[...document.querySelectorAll('#ami-antibiais .mb-card, #ami-antibiais [data-n]')].filter(e=>e.offsetParent).length")
ok('Manuel : le filtre « Fondateurs » change la liste', n0 != n1 and n1 > 0, f'{n0}â{n1}')
ok('boutons de filtre â„ 24 px', E("()=>[...document.querySelectorAll('#ami-antibiais .mb-f button')].filter(b=>b.offsetParent).every(b=>b.getBoundingClientRect().height>=24)"))
# ââ widgets en iframes : Ă la charte, moteur vivant
E("()=>document.querySelectorAll('.apk-tb')[0].click()"); pg.wait_for_timeout(200)
E("()=>document.querySelectorAll('#apkSubHost .ami-volume-tab')[1].click()"); pg.wait_for_timeout(600)
ok('iframe Consentement Réel : or de la Charte dans sa racine', E("()=>{const f=[...document.querySelectorAll('iframe')].find(f=>/Consentement/.test(f.getAttribute('srcdoc')||'')); if(!f||!f.contentDocument) return false; return getComputedStyle(f.contentDocument.documentElement).getPropertyValue('--or').trim()==='#ffc545';}"))
ok('iframe Consentement Réel : un bouton répond', E("()=>{const f=[...document.querySelectorAll('iframe')].find(f=>/Consentement/.test(f.getAttribute('srcdoc')||'')); const d=f&&f.contentDocument; if(!d) return false; const b=d.querySelector('button'); if(!b) return false; const h0=d.body.innerHTML.length; b.click(); return d.body.innerHTML.length!==h0 || d.querySelector('.on,.active,[aria-pressed=true]')!==null;}"))
# ââ lecture continue
E("()=>document.querySelectorAll('#apkSubHost .ami-volume-tab')[0].click()"); pg.wait_for_timeout(200)
n0 = E("()=>document.querySelectorAll('.apk-pane:not([hidden]), .ami-volume-panel:not([hidden]), .cs-pane:not([hidden])').length")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
n1 = E("()=>document.querySelectorAll('.apk-pane:not([hidden]), .ami-volume-panel:not([hidden]), .cs-pane:not([hidden])').length")
ok('đ§ lecture continue : 3 volets + 8 parties + 4 sous-onglets dĂ©roulĂ©s', n1 == 15, f'{n0}â{n1}')
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
ok('đ§ retour : un volet, une partie, un sous-onglet', E("()=>document.querySelectorAll('.apk-pane:not([hidden])').length")==1 and E("()=>document.querySelectorAll('.ami-volume-panel:not([hidden])').length")==1 and E("()=>document.querySelectorAll('.cs-pane:not([hidden])').length")==1)
col0 = E("()=>getComputedStyle(document.getElementById('ami-apokalypse')).getPropertyValue('--or').trim()")
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
col1 = E("()=>getComputedStyle(document.getElementById('ami-apokalypse')).getPropertyValue('--or').trim()")
ok('â contraste : --or monte', E("()=>document.body.classList.contains('ami-contraste')") and col0 != col1, f'{col0}â{col1}')
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('ⶠplein écran posé', E("()=>document.getElementById('ami-apokalypse').classList.contains('ami-faux-fs')||!!document.fullscreenElement"))
z0 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-apokalypse')).zoom||1)")
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
z1 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-apokalypse')).zoom||1)")
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(200); E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(1200) # la sortie du plein écran (pontOff) prend son temps en headless
ok('+ agrandit', z1 > z0, f'{z0}â{z1}')
E("()=>window.scrollTo(0,document.body.scrollHeight/2)"); pg.wait_for_timeout(300)
ok('#prog progresse', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>{const o=URL.createObjectURL; URL.createObjectURL=function(b){window.__blob=b; return o.call(URL,b);}; HTMLAnchorElement.prototype.click=function(){window.__dl=this.download;};}")
E("()=>document.getElementById('dlHtml').click()"); pg.wait_for_timeout(1500)
ok('âŹïž Export HTML composĂ©, bien nommĂ©', E("()=>window.__dl")==f'0.1_APOKALYPSE_aMi_{VER}.html' and E("()=>window.__blob?window.__blob.size:0")>2500000, str(E("()=>[window.__dl, window.__blob&&window.__blob.size]")))
ok('version/date/volume = metas', E("()=>[...document.querySelectorAll('[data-ami=version]')].every(e=>e.textContent==='"+VER+"') && [...document.querySelectorAll('[data-ami=date]')].every(e=>e.textContent==='"+DATE_FR+"') && [...document.querySelectorAll('[data-ami=volume]')].every(e=>e.textContent==='0.1')"))
ok('nulle part « Volume 0.0 » ni « V24 »', E("()=>!/Volume 0\\.0|V24\\b/.test(document.getElementById('ami-apokalypse').innerText)"))
# ââ la boĂźte Ă outils : đ â° â ?
E("()=>window.scrollTo(0,2500)"); pg.wait_for_timeout(300)
ok('â apparaĂźt aprĂšs 600 px, remonte', not E("()=>document.getElementById('amiHaut').hidden") and (E("()=>{document.getElementById('amiHaut').click(); return true;}") and (pg.wait_for_timeout(1500) or True)) and E("()=>window.scrollY") < 50)
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', 'tortionnaire'); pg.wait_for_timeout(500)
ok('đ la recherche trouve « tortionnaire » et dit oĂč', E("()=>document.querySelectorAll('#rc-l button.rc-r').length")>=2 and '2 bis' in E("()=>document.querySelector('#rc-l .rc-c').textContent"))
E("()=>document.querySelectorAll('.apk-tb')[2].click()"); pg.wait_for_timeout(200)
E("()=>document.querySelector('#rc-l button.rc-r').click()"); pg.wait_for_timeout(1500)
ok('đ un rĂ©sultat ouvre le bon volet et y va', E("()=>document.querySelector('.apk-pane:not([hidden])').getAttribute('data-c')")=='k1' and not E("()=>document.getElementById('ami-rech').classList.contains('on')") and E("()=>window.scrollY")>1000)
ok('coins alignés à droite, cachés sur la une', E("()=>{const c=['ami-toc-toggle','amiHaut','amiRech','ami-glossary-toggle'].map(i=>document.getElementById(i)); return c.every(b=>b && getComputedStyle(b).position==='fixed'); }"))
ok('un seul bas de page (pas de apk-foot), citation du volume dans le pied', E("()=>!document.querySelector('.apk-foot') && /Le Déni, la Folie/.test(document.querySelector('.ami-pied .ap-cit').textContent)"))
ok('« Agdistys » en ligne dans le pied', E("()=>getComputedStyle(document.querySelector('.ami-pied .ap-legal span span')).display==='inline'"))
ok('une : đïž en haut, đŻïž en bas', E("()=>document.querySelector('.couv-logo').textContent.trim()==='đïž' && document.querySelector('.apk-une-em').textContent.trim()==='đŻïž'"))
ok('émojis de la une à leur taille (le plancher ne les rapetisse plus)', E("()=>{const r=document.createRange(); r.selectNodeContents(document.querySelector('.couv-logo')); return r.getBoundingClientRect().height>40;}"))
ok('sous-onglets ConScience : émoji au-dessus du libellé', E("()=>{const b=document.querySelector('.cs-tb'); const e=b.querySelector('.cs-em'), l=b.querySelector('.cs-lbl'); return e&&l&&e.getBoundingClientRect().bottom<=l.getBoundingClientRect().top+1;}"))
ok('Plan du Complexe : losange par les bouts des axes', E("()=>!!document.querySelector('path[d=\"M-190,0 L0,-104 L190,0 L0,104 Z\"]')"))
ok('Double Triangle : les nĆuds ne dĂ©bordent pas de leur carte', E("()=>[...document.querySelectorAll('.v12-triad')].every(t=>{const m=t.querySelector('.v12-triad-map').getBoundingClientRect(); return [...t.querySelectorAll('.v12-triad-node')].every(n=>n.getBoundingClientRect().bottom<=m.bottom+1);})"))
ok('Double Triangle : un triangle aussi sur tĂ©lĂ©phone (svg visible, 3 nĆuds aux sommets)', E("()=>{const t=document.querySelector('.v12-triad'); const s=t.querySelector('svg'); const ns=[...t.querySelectorAll('.v12-triad-node')].map(n=>n.getBoundingClientRect()); return getComputedStyle(s).display!=='none' && ns.length===3 && Math.abs(ns[0].top-ns[1].top)<8 && ns[2].top>ns[0].bottom-4;}"))
ok('Pyramide du Patriarcat en HTML : 8 niveaux, plus de SVG', E("()=>document.querySelectorAll('#ami-antibiais .pyr-lv').length===8 && !document.getElementById('pyr_Calque_2')"))
ok('barre du site : page marquée .ici, tiroir fermé au chargement', E("()=>!!document.querySelector('.ami-barre .v.ici') && !document.querySelector('.ami-barre .tiroir.on')"))
ok('entĂȘte « Navigation du Volume » absent', E("()=>!document.querySelector('.ami-volume-tabs-heading')"))
pg.emulate_media(reduced_motion='reduce'); pg.wait_for_timeout(100)
ok('prefers-reduced-motion coupe les animations', E("()=>[...document.querySelectorAll('*')].every(e=>{const cs=getComputedStyle(e); return cs.animationDuration==='0s'||cs.animationName==='none';})"))
pg.emulate_media(reduced_motion='no-preference')
ok('.v.ici sur 0.1', E("()=>{const v=document.querySelector('.ami-barre .v.ici'); return v && /0\\.1/.test(v.textContent);}"))
ok('pile collante : barre du site â volets â sous-barre (sous-barre libre sur tĂ©lĂ©phone)', E("()=>{const m=document.querySelector('.apk-mainbar'), s=document.querySelector('#apkSubHost'); const nh=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')); const ok1=getComputedStyle(m).position==='sticky' && parseFloat(getComputedStyle(m).top)===nh; return innerWidth<=640 ? ok1 && getComputedStyle(s).position==='static' : ok1 && parseFloat(getComputedStyle(s).top)>nh;}"))
pg.close()
b.close()
print('erreurs JS :', errs); print(sum(1 for r in R if r[1]), '/', len(R), 'réussis')
sonde_overflow.py sonde_overflow.py page.html 39018 lignes
Qui déborde ? Presse chaque onglet et nomme l'élément non rogné qui dépasse le cadre.
â§ Copier le script
import sys,os
from playwright.sync_api import sync_playwright
SRC=sys.argv[1]; W=int(sys.argv[2]) if len(sys.argv)>2 else 390
JS=r"""() => { const w=document.documentElement.clientWidth; const out=[];
document.querySelectorAll('body *').forEach(el=>{ if(el.closest('svg')) return; const r=el.getBoundingClientRect(); if(r.width<1) return;
if(r.right>w+1 || r.left<-1){ let e=el.parentElement, clip=false; while(e){ const o=getComputedStyle(e).overflowX; if(o==='hidden'||o==='clip'||o==='auto'||o==='scroll'){clip=true;break;} e=e.parentElement;}
if(!clip){ const cs=getComputedStyle(el); out.push({tag:el.tagName.toLowerCase()+(el.className&&typeof el.className==='string'?'.'+el.className.trim().split(/\s+/).slice(0,2).join('.'):''), right:Math.round(r.right), left:Math.round(r.left), w:Math.round(r.width), pos:cs.position, txt:(el.textContent||'').trim().slice(0,40)}); } } });
return {sw:document.documentElement.scrollWidth, cw:w, n:out.length, ex:out.slice(0,25)}; }"""
with sync_playwright() as p:
br=p.chromium.launch(); pg=br.new_page(viewport={'width':W,'height':844}); pg.goto('file://'+os.path.abspath(SRC)); pg.wait_for_timeout(800)
for sel in ('.apk-tb','.ami-volume-tab','.cs-tb'):
for b in pg.query_selector_all(sel):
try: b.click(timeout=1000); pg.wait_for_timeout(60)
except Exception: pass
r=pg.evaluate(JS)
if r['sw']>r['cw']: print(sel, (b.text_content() or '')[:30].strip(), r['sw'], r['cw'], [ (e['tag'],e['right'],e['w'],e['txt']) for e in r['ex'][:6]])
br.close()
p9_retours_apo.py p9_retours_apo.py (+ boite_css.txt)156 lignes
0.1 Apokalypse â retours du 17 sept : une đïž/đŻïž, entĂȘte ĂŽtĂ©, Manuel sans trous, un seul pied, boĂźte Ă outils de la Charte (đ recherche gĂ©nĂ©rique, â°, â, ?).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P9 · 0.1 Apokalypse â les retours de Diane du 17 sept (02h30) :
lien RefleXion â agdistys.fr/ami-miroir · une : đïž en haut, đŻïž en bas, grands · l'entĂȘte « â Navigation du Volume » ĂŽtĂ© ·
les cartes du Manuel ne creusent plus de trou (flex-basis 12rem en colonne) · un seul bas de page (le pied du volume s'en va,
sa citation passe dans le pied V16) · « Agdistys » ne saute plus de ligne dans le pied (span dans span) ·
la boĂźte Ă outils de la Charte : đ Recherche · â° Sommaire · â Haut (+ ? Glossaire, propre Ă la page) ·
les 8 parties sur un rang dĂšs 1100 px."""
import re
h = open('apo.html', encoding='utf-8').read()
BOITE_CSS = open('../spec/boite_css.txt', encoding='utf-8').read().replace('#ami-rech{display:none!important}\n', '')
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · le lien vers RefleXion (elle vivra dans Miroir) ââââââââââââââââââââââ
rep('est dans <strong>đ§ RefleXion</strong>, volet đ La ThĂ©orie, « Le DĂ©ni d\'AltĂ©ritĂ© ».',
'est dans <a href="https://agdistys.fr/ami-miroir"><strong>đ§ RefleXion</strong></a>, volet đ La ThĂ©orie, « Le DĂ©ni d\'AltĂ©ritĂ© ».', why='lien RefleXion â ami-miroir')
# ââ 2 · la une : đïž en haut, đŻïž en bas âââââââââââââââââââââââââââââââââââââ
rep('<div class="couv-logo">đŻïž</div>', '<div class="couv-logo">đïž</div>', why='une : đïž en haut')
assert h.count('<p class="apk-une-em">đŻïž</p>') == 1
# ââ 3 · l'entĂȘte de la sous-barre s'en va ââââââââââââââââââââââââââââââââââââ
h, n = re.subn(r'<div class="ami-volume-tabs-heading">.*?</div>', '', h, count=1, flags=re.S); J.append(f'entĂȘte « â Navigation du Volume » ĂŽtĂ© Ă{n}')
assert n == 1
# ââ 4 · un seul bas de page : le pied du volume s'en va, sa citation passe dans le pied V16 ââ
m = re.search(r'<footer class="apk-foot">.*?</footer>', h, re.S); assert m
h = h[:m.start()] + h[m.end():]; J.append('pied du volume (apk-foot) Îté : il doublait le pied V16')
rep('<p class="ap-cit">Vérité = Respect · Mensonge = Violence.\nLa ConScience est la Vie qui se reconnaßt et protÚge.</p>',
'<p class="ap-cit">« Le DĂ©ni, la Folie, est de tellement <i>dĂ©tester</i> avoir tort Ă en perdre la raison ;\nĂȘtre tellement lĂąche pour <i>avoir peur</i> de lâĂ©ducation qui ferait gagner en maturation,\nprĂ©fĂ©rant ses privilĂšges, sa prĂ©tention et son agression plutĂŽt que lâĂ©thique et sa sublimation quantique ;\navoir tellement peur de comprendre celles que lâon abandonne en les traitant de <i>folles</i>. »</p>', why='citation du volume dans le pied V16')
# ââ 5 · la feuille des retours âââââââââââââââââââââââââââââââââââââââââââââââ
css = '''
<style id="ami-retours">
/* ââ RETOURS DE DIANE, 17 SEPT ââ */
#ami-apokalypse .couv-logo{font-size:3.6rem;opacity:1;line-height:1}
#ami-apokalypse .apk-une-em{font-size:3rem}
/* les cartes du Manuel : en colonne, flex-basis 12rem creusait un trou de 12 rem sous le titre (Safari) */
#ami-antibiais .mb-n b{flex:0 0 auto!important;min-width:min(12rem,100%)}
/* le pied : « Agdistys » est un span dans un span de bloc â il reste en ligne */
.ami-pied .ap-legal span span{display:inline}
.ami-pied .ap-cit{max-width:min(72ch,100%)}
/* les 8 parties : un seul rang dĂšs 1100 px */
@media(min-width:1100px){#ami-apokalypse .apk-subhost .ami-volume-tab{flex:1 1 11%!important}}
/* ââ LA BOĂTE Ă OUTILS DE LA CHARTE : đ â° â (+ ? glossaire, propre Ă la page) ââ */
''' + BOITE_CSS + '''
#ami-apokalypse .ami-coin,#ami-apokalypse #ami-toc-toggle,#ami-apokalypse #ami-glossary-toggle{position:fixed!important;right:1rem!important;left:auto!important;width:2.1rem!important;height:2.1rem!important;min-width:0!important;padding:0!important;border-radius:50%!important;border:1px solid var(--or-somb)!important;background:rgba(9,9,14,.92)!important;color:var(--or)!important;font-size:.92rem!important;font-family:'Jost',sans-serif!important;line-height:1!important;box-shadow:none!important;transform:none!important;z-index:60!important;display:flex!important;align-items:center;justify-content:center}
#ami-apokalypse #ami-toc-toggle{bottom:1rem!important}
#ami-apokalypse #amiHaut{bottom:3.9rem!important}
#ami-apokalypse #amiRech{bottom:7.4rem!important}
#ami-apokalypse #ami-glossary-toggle{bottom:10.9rem!important}
#ami-apokalypse #amiHaut[hidden]{display:none!important}
#ami-apokalypse .ami-coin:hover,#ami-apokalypse #ami-toc-toggle:hover,#ami-apokalypse #ami-glossary-toggle:hover{background:rgba(255,255,255,.09)!important;color:var(--or-pale)!important}
#ami-apokalypse #ami-toc-toggle[aria-expanded="true"],#ami-apokalypse #ami-glossary-toggle[aria-expanded="true"],#ami-apokalypse #amiRech[aria-pressed="true"]{border-color:var(--or)!important;background:rgba(255,255,255,.12)!important;color:var(--or-pale)!important}
#ami-apokalypse #ami-toc-toggle.cover-hidden,#ami-apokalypse #ami-glossary-toggle.cover-hidden,#ami-apokalypse #amiHaut.cover-hidden,#ami-apokalypse #amiRech.cover-hidden{opacity:0!important;visibility:hidden!important;pointer-events:none!important}
#ami-apokalypse #ami-toc-panel{right:1rem;bottom:3.6rem}
#ami-apokalypse #ami-glossary-panel{right:1rem}
#ami-rech .rc-n,#ami-rech .rc-g,#ami-rech .rc-c{font-size:.72rem}
#ami-rech .rc-b{font-family:'Jost',sans-serif}
</style>
'''
rep('</head>', css + '</head>', why='feuille des retours + boĂźte Ă outils')
# ââ 6 · les coins : â° et ? existent (rebaptisĂ©s), â et đ arrivent ; la recherche est gĂ©nĂ©rique ââ
rep('<button aria-controls="ami-toc-panel" aria-expanded="false" id="ami-toc-toggle" title="Sommaire" type="button">â</button>',
'<button aria-controls="ami-toc-panel" aria-expanded="false" id="ami-toc-toggle" class="ami-coin d3" title="â° Le sommaire de la page" aria-label="Sommaire de la page" type="button">â°</button>', why='â° sommaire (coin)')
rep('<button aria-controls="ami-glossary-panel" aria-expanded="false" id="ami-glossary-toggle" title="Glossaire" type="button">â</button>',
'<button aria-controls="ami-glossary-panel" aria-expanded="false" id="ami-glossary-toggle" class="ami-coin g" title="? Le glossaire de la page" aria-label="Glossaire" type="button">?</button>', why='? glossaire (coin)')
coins = '''
<button class="ami-coin h" id="amiHaut" type="button" hidden aria-label="Revenir en haut" title="â Revenir en haut">â</button>
<button class="ami-coin r" id="amiRech" type="button" aria-label="Rechercher" title="Chercher dans le volume (Ctrl+F)"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></button>
<div id="ami-rech" role="dialog" aria-modal="true" aria-label="Rechercher dans le volume">
<div class="rc-b">
<div class="rc-h"><span class="rc-loupe"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></span>
<input type="search" id="rc-q" placeholder="Chercher dans les 79 chapitresâŠ" autocomplete="off" spellcheck="false" aria-label="Chercher">
<span class="rc-n" id="rc-n"></span>
<button class="rc-x" type="button" id="rc-x" aria-label="Fermer">â</button></div>
<div class="rc-l" id="rc-l" role="listbox"></div>
</div>
</div>
'''
js = r'''
<script id="ami-boite-js">
/* ââ LA BOĂTE Ă OUTILS : â Haut, đ Recherche (gĂ©nĂ©rique : elle indexe le texte de la racine, dit oĂč il est, et ouvre ce qu'il faut) ââ */
(function(){'use strict';
var R=document.getElementById('ami-apokalypse'); if(!R) return;
var H=document.getElementById('amiHaut');
if(H){ function suit(){ H.hidden = window.pageYOffset < 600; } window.addEventListener('scroll',suit,{passive:true}); suit();
H.addEventListener('click',function(){ window.scrollTo({top:0,behavior:'smooth'}); }); }
var B=document.getElementById('amiRech'), D=document.getElementById('ami-rech'), Q=document.getElementById('rc-q'), L=document.getElementById('rc-l'), N=document.getElementById('rc-n'), X=document.getElementById('rc-x');
if(!B||!D) return;
var IDX=null, RES=[], SEL=-1;
function nomVolet(c){ var b=R.querySelector('.apk-tb[data-c="'+c+'"] .tb-lbl'); return b?b.textContent.trim():c; }
function bati(){
IDX=[];
var w=document.createTreeWalker(R,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT), n, chap='', sous='';
while((n=w.nextNode())){
if(n.nodeType===1){ if(n.tagName==='H2'){chap=n.textContent.trim();sous='';} else if(n.tagName==='H3'){sous=n.textContent.trim();} continue; }
var s=n.textContent.replace(/\s+/g,' ').trim(); if(s.length<3) continue;
var p=n.parentElement; if(!p||p.closest('script,style,svg,#ami-rech,#ami-toc-panel,#ami-glossary-panel,.asom,.ami-barre,.ami-pied,.apk-mainbar,.apk-subhost')) continue;
var pane=p.closest('.apk-pane'), part=p.closest('.ami-volume-panel'), cs=p.closest('.cs-pane');
IDX.push({t:s, tl:s.toLowerCase(), el:p, volet:pane?pane.getAttribute('data-c'):null, part:part?part.id:null, cs:cs?cs.getAttribute('data-cs'):null, chap:chap, sous:sous});
}
}
function esc(s){ return s.replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function cherche(q){
q=q.trim().toLowerCase(); L.innerHTML=''; RES=[]; SEL=-1;
if(q.length<2){ N.textContent=''; return; }
if(!IDX) bati();
var vus={}, par={}, ordre=[];
IDX.forEach(function(e){ var i=e.tl.indexOf(q); if(i<0) return; var k=(e.volet||'')+'|'+e.chap+'|'+e.t.slice(0,60); if(vus[k]) return; vus[k]=1;
var g=e.volet?nomVolet(e.volet):'â'; if(!par[g]){par[g]=[];ordre.push(g);} if(par[g].length<40) par[g].push({e:e,i:i}); });
var tot=0; ordre.forEach(function(g){ tot+=par[g].length; });
N.textContent = tot? tot+(tot>1?' passages':' passage') : 'rien';
if(!tot){ L.innerHTML='<div class="rc-v">Rien pour « '+esc(q)+' ».</div>'; return; }
ordre.forEach(function(g){ var d=document.createElement('div'); d.className='rc-g'; d.textContent=g; L.appendChild(d);
par[g].forEach(function(r){ var e=r.e, a=Math.max(0,r.i-50), z=Math.min(e.t.length,r.i+q.length+70);
var ext=(a>0?'âŠ':'')+esc(e.t.slice(a,r.i))+'<mark>'+esc(e.t.slice(r.i,r.i+q.length))+'</mark>'+esc(e.t.slice(r.i+q.length,z))+(z<e.t.length?'âŠ':'');
var b=document.createElement('button'); b.type='button'; b.className='rc-r'; b.innerHTML=ext+'<span class="rc-c">'+esc(e.chap)+(e.sous?' · '+esc(e.sous):'')+'</span>';
b.addEventListener('click',function(){ va(e); }); L.appendChild(b); RES.push(b); }); });
}
function va(e){
ferme();
if(e.volet&&window.amiOuvreVolet) window.amiOuvreVolet(e.volet);
if(e.part){ var i=[].map.call(R.querySelectorAll('.ami-volume-panel'),function(p){return p.id;}).indexOf(e.part); var t=R.querySelectorAll('.ami-volume-tab')[i]; if(t) t.click(); }
if(e.cs){ var c=R.querySelector('.cs-tb[data-cs="'+e.cs+'"]'); if(c) c.click(); }
setTimeout(function(){ var pile=0; ['.ami-barre','.apk-mainbar','.apk-subhost'].forEach(function(s){ var x=document.querySelector(s); if(x&&getComputedStyle(x).position==='sticky') pile+=x.getBoundingClientRect().height; });
var y=e.el.getBoundingClientRect().top+window.pageYOffset-pile-16; window.scrollTo({top:Math.max(0,y),behavior:'smooth'});
e.el.style.transition='background .6s'; var bg=e.el.style.background; e.el.style.background='rgba(255,197,69,.18)'; setTimeout(function(){ e.el.style.background=bg; },1800); }, 250);
}
function ouvre(){ D.classList.add('on'); B.setAttribute('aria-pressed','true'); Q.value=''; L.innerHTML=''; N.textContent=''; setTimeout(function(){Q.focus();},30); }
function ferme(){ D.classList.remove('on'); B.setAttribute('aria-pressed','false'); }
B.addEventListener('click',function(){ D.classList.contains('on')?ferme():ouvre(); });
X.addEventListener('click',ferme);
D.addEventListener('click',function(ev){ if(ev.target===D) ferme(); });
var tm; Q.addEventListener('input',function(){ clearTimeout(tm); tm=setTimeout(function(){ cherche(Q.value); },120); });
Q.addEventListener('keydown',function(ev){ if(ev.key==='ArrowDown'||ev.key==='ArrowUp'){ ev.preventDefault(); if(!RES.length) return; SEL=(SEL+(ev.key==='ArrowDown'?1:-1)+RES.length)%RES.length; RES.forEach(function(b,i){b.classList.toggle('sel',i===SEL);}); RES[SEL].scrollIntoView({block:'nearest'}); }
else if(ev.key==='Enter'&&SEL>=0){ RES[SEL].click(); } });
document.addEventListener('keydown',function(ev){ if(ev.key==='Escape'&&D.classList.contains('on')){ ferme(); }
if((ev.ctrlKey||ev.metaKey)&&ev.key.toLowerCase()==='f'&&!ev.shiftKey){ var a=document.activeElement; if(a&&(a.tagName==='INPUT'||a.tagName==='TEXTAREA')&&a!==Q) return; ev.preventDefault(); ouvre(); } });
/* les coins se cachent sur la une, comme Ⱐet ? le font déjà */
var cover=document.getElementById('apok-cover');
if(cover){ function surUne(){ var hid=cover.getBoundingClientRect().bottom>120; [B,H].forEach(function(c){ if(c) c.classList.toggle('cover-hidden',hid); }); }
surUne(); window.addEventListener('scroll',surUne,{passive:true}); window.addEventListener('resize',surUne); }
})();
</script>
'''
rep('<style id="ami-contraste">', coins + js + '<style id="ami-contraste">', why='â et đ + recherche gĂ©nĂ©rique')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p10_retours2_apo.py p10_retours2_apo.py (aprĂšs p2_plancher.py)53 lignes
0.1 Apokalypse â retours 2 : span.ami-emoji hĂ©rite, Double Triangle libre sur tĂ©lĂ©phone, losange du Plan, sous-onglets ConScience Ă©moji au-dessus.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P10 · 0.1 Apokalypse â retours de Diane du 17 sept (03h) :
· l'Ă©chelle des Ă©mojis : le plancher avait Ă©crit `span.ami-emoji{font-size:.72rem}` â la page enveloppe chaque Ă©moji dans un
span par script, la rÚgle les ramenait TOUS à 11,5 px (une, onglets, titres). Ils héritent à nouveau : max(.72rem,1em).
· le Double Triangle sur tĂ©lĂ©phone : en liste (†720 px) la carte gardait sa hauteur fixe de 245 px â les nĆuds dĂ©bordaient
sur la légende. Hauteur libre.
· le Plan du Complexe d'Infériorité : le losange partait de (-190,-104) au lieu de (-190,0) ; les quatre sommets sont
maintenant les quatre bouts des axes. Ses couleurs passent aux pierres.
· les sous-onglets ConScience : émoji au-dessus du libellé, comme les 8 parties.
Ă rejouer APRĂS p2_plancher.py (il réécrit le bloc plancher)."""
import re
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# 1 · les émojis héritent leur taille
n = len(re.findall(r'span\.ami-emoji\{font-size:\.72rem\}', h)); h = re.sub(r'span\.ami-emoji\{font-size:\.72rem\}', 'span.ami-emoji{font-size:max(.72rem,1em)}', h); J.append(f'plancher : span.ami-emoji hĂ©rite Ă{n}')
assert n >= 1
# 2 · le Plan : le losange
rep('<path d="M-190,-104 L0,-104 L190,0 L0,104 Z" fill="none" stroke="rgba(200,168,74,.22)"/>', '<path d="M-190,0 L0,-104 L190,0 L0,104 Z" fill="none" stroke="rgba(255,197,69,.22)"/>', why='Plan : losange par les quatre bouts des axes')
i = h.find('aria-label="Le plan du complexe dâinfĂ©rioritĂ©'); j = h.find('</svg>', i)
svg = h[i:j]
for a, b in (('#3a7a52', '#0b8d38'), ('#90d8a8', '#32b15e'), ('#8a3030', '#e33232'), ('#2a4870', '#7697ee'), ('#c8a84a', '#ffc545'), ('#b2b2c8', '#b0b0bd'), ('#888898', '#9595a2')):
svg = svg.replace(a, b)
h = h[:i] + svg + h[j:]; J.append('Plan : couleurs aux pierres')
# 3 · les sous-onglets ConScience : émoji au-dessus
n = 0
def cs(m):
global n; n += 1
return f'{m.group(1)}<span class="cs-em">{m.group(2)}</span><span class="cs-lbl">{m.group(3).strip()}</span></button>'
h = re.sub(r'(<button type="button" class="cs-tb(?: on)?" data-cs="[a-d]">)([\U0001F300-\U0001FAFFâ-âż][ïž]?)\s*([^<]+)</button>', cs, h); J.append(f'sous-onglets ConScience : Ă©moji au-dessus Ă{n}')
assert n == 4, n
css = '''
<style id="ami-retours2">
/* ââ RETOURS DE DIANE, 17 SEPT, 03h ââ */
@media(max-width:720px){#ami-apokalypse .v12-triad-map{height:auto!important}#ami-apokalypse .v12-triad{min-height:0!important}#ami-apokalypse .v12-triad-node{width:100%!important;max-width:none!important;text-align:left!important}}
#ami-apokalypse .apk-subhost .cs-tb{display:flex!important;flex-direction:column;align-items:center;justify-content:center;gap:.1rem!important;padding:.45rem .3rem .5rem!important}
#ami-apokalypse .cs-tb .cs-em{font-size:1.15rem!important;line-height:1;font-style:normal;opacity:.75}
#ami-apokalypse .cs-tb.on .cs-em{opacity:1}
#ami-apokalypse .cs-tb .cs-lbl{font-size:.78rem!important;line-height:1.2;font-weight:500;text-align:center}
</style>
'''
rep('</head>', css + '</head>', why='feuille des retours 2')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p11_pyramide_triangles.py p11_pyramide_triangles.py100 lignes
0.1 Apokalypse â retours 3 : triangles en triangles sur tĂ©lĂ©phone ; Pyramide du Patriarcat SVG â HTML (8 niveaux, fracture, 3 biais).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P11 · 0.1 Apokalypse â retours de Diane du 17 sept (03h15) :
· les deux Triangles restent des triangles sur tĂ©lĂ©phone (la liste †720 px est ĂŽtĂ©e : carte en proportion, nĆuds aux trois
sommets, flĂšches visibles) ;
· la Pyramide du Patriarcat : le SVG Illustrator (rouge sur rouge, 28 px de texte dans 1427 de large, illisible sur téléphone,
glyphes accentuĂ©s Ă©clatĂ©s par le crĂ©nage) est remplacĂ© par la mĂȘme pyramide en HTML â huit niveaux, la zone de fracture,
les trois biais, les trois phrases â d'aprĂšs `Schemas SVG/pyramide du patriarcat.svg` (27 juin), aux pierres, lisible Ă 320 px."""
import re
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# ââ 1 · la pyramide en HTML ââââââââââââââââââââââââââââââââââââââââââââââââââ
i = h.find('<h3>đș Pyramide du Patriarcat'); assert i > 0
a = h.find('<svg', i); b = h.find('</svg>', a) + 6
assert 'id="pyr_Calque_2"' in h[a:a+400]
NIV = [
('rubis', 'Ălite des hommes riches', 'Millionnaires, PDG, politiciens, « philanthropes »',
['ContrĂŽle les mĂ©dias, lâĂ©conomie et la loi : gang puissant & fortunĂ©', 'Imposition du rĂ©cit dominant : contrĂŽle des mĂ©dias, de la loi, de lâĂ©conomie', 'Imposition du rĂ©cit personnel comme vĂ©ritĂ© absolue : « je suis la rĂ©incarnation de⊠»'], 'Biais dâAutoritĂ© Totale'),
('rubis', 'Classe moyenne masculine', 'Managers, superviseurs, diplÎmés',
['BĂ©nĂ©ficie du privilĂšge masculin', 'Garde lâaccĂšs aux ressources & aux emplois'], ''),
('cornaline', 'Classe laborieuse masculine', 'Travailleurs, arnaqueurs, bas revenus',
['Compensation par violence domestique / symbolique â violence Ă©motionnelle passive : « je reste calme pendant que je tâannule »', 'Violent chez lui sâil manque de domination Ă lâextĂ©rieur'], ''),
('grenat', 'Femmes « pick-me »', 'Clan de la misogynie intériorisée : matriarcat',
['Complice du patriarcat sur les minoritĂ©s : intĂ©riorisation du discours dominant pour plaire ; rĂ©pĂ©tition du modĂšle de validation externe â « je suis aimĂ©e si je mâĂ©teins »', 'DĂ©fend la suprĂ©matie masculine pour avoir des miettes de validation masculine'], 'Biais de Validation Masculine'),
('grenat', 'Femmes « idĂ©ales » / qui travaillent', 'Surtout ethniques â tradwives, jolies, soumises â « girl boss »',
['RĂ©compensĂ©es pour se conformer aux normes de genre', 'Toujours disponibles & dĂ©pendantes des faveurs masculines', 'Charge mentale, culpabilitĂ© dâexister', 'Tentative de se justifier face au mĂ©pris : « je dois prouver que mon travail existe »', 'Sous-payĂ©es, surchargĂ©es, souvent mĂ©prisĂ©es', 'AmĂ©nagement entre travail payĂ© & travail domestique'], ''),
('sugilite', 'Femmes pauvres', 'Travailleuses domestiques, mÚres célibataires',
['Colonne vertébrale de la société par le travail gratuit', 'Exploitées, censurées, vulnérables'], 'Biais de Honte Intériorisée'),
('tanzanite', 'Femmes marginalisĂ©es', 'Trans · queer · grosses · handicapĂ©es · sans domicile · prostituĂ©es · bĂ©nĂ©voles â invisibilisation systĂ©mique, exclues & attaquĂ©es',
['Effacement algorithmique : leur parole est disqualifiée comme « non crédible »', 'Droits à la sécurité et à la reconnaissance niés', 'Utilisées comme bouc émissaire pour distraire du pouvoir en place'], ''),
('emeraude', 'Vies non humaines', '', [], ''),
]
parts = ['<div class="pyr" role="img" aria-label="Pyramide du Patriarcat â hiĂ©rarchie des privilĂšges et discriminations">',
'<p class="pyr-titre">Hiérarchie des privilÚges & discriminations<br><b>Pyramide du Patriarcat · Culture sacrificielle</b><br><i>La structure du mensonge collectif : domination du verbe, effacement du vivant.</i></p>',
'<p class="pyr-ax"><span>đș <b>Pouvoir social</b> â vers le haut</span><span>đ» <b>Pouvoir narratif</b> â vers le bas</span></p>']
for k, (c, titre, sous, puces, biais) in enumerate(NIV):
w = 46 + k * (54 / (len(NIV) - 1))
parts.append(f'<div class="pyr-lv" style="--w:{w:.0f}%;--c:var(--{c});--c-ct:var(--{c}-ct);--c-rgb:var(--{c}-rgb)">'
+ (f'<span class="pyr-biais">{biais}</span>' if biais else '')
+ f'<b>{titre}</b>' + (f'<em>{sous}</em>' if sous else '')
+ ('<ul>' + ''.join(f'<li>{p}</li>' for p in puces) + '</ul>' if puces else '') + '</div>')
if k == 2:
parts.append('<div class="pyr-fract"><b>𩞠Zone de fracture â la double charge : physique & psychique</b><span>Patriarcat social & patriarcat cognitif : contrainte sociale et effacement mental.</span></div>')
parts.append('<div class="pyr-fin"><p>« La domination est virale : elle se transmet par le verbe, se rĂ©pare par la ConScience. »</p><p>« Luttons pour toutes les personnes qui nâont pas de voix, qui ne sont pas Ă©coutĂ©es. »</p><p><b>Dans la pyramide de ConScience : les derniĂšres sont les premiĂšres, les premiers sont les derniers. Les OubliĂ©es sont les Sources.</b></p></div></div>')
h = h[:a] + '\n'.join(parts) + h[b:]; J.append('Pyramide : SVG â HTML (8 niveaux, fracture, 3 biais, 3 phrases)')
rep('<div class="v12-schema-body always-open pyr-fix">', '<div class="v12-schema-body always-open pyr-html">', why='corps de la carte')
css = '''
<style id="ami-retours3">
/* ââ LA PYRAMIDE DU PATRIARCAT, EN HTML (d'aprĂšs Schemas SVG/pyramide du patriarcat.svg) ââ */
#ami-antibiais .pyr-html{display:block!important;padding:.6rem .4rem}
#ami-antibiais .pyr{display:flex;flex-direction:column;align-items:center;gap:.35rem;max-width:860px;margin:0 auto}
#ami-antibiais .pyr-titre{text-align:center;font-family:'Jost',sans-serif;font-size:.86rem;line-height:1.5;color:var(--gris-cl);margin:0 0 .4rem}
#ami-antibiais .pyr-titre b{color:var(--rubis-ct);font-size:1.02rem;letter-spacing:.04em}
#ami-antibiais .pyr-titre i{color:var(--gris)}
#ami-antibiais .pyr-ax{display:flex;flex-wrap:wrap;justify-content:center;gap:.3rem 1.4rem;font-size:.78rem;color:var(--gris);margin:0 0 .5rem}
#ami-antibiais .pyr-ax b{color:var(--nacre);font-weight:500}
#ami-antibiais .pyr-lv{position:relative;width:var(--w);min-width:min(100%,22rem);box-sizing:border-box;padding:.65rem 1rem .7rem;border:1px solid rgba(var(--c-rgb),.45);border-top:3px solid var(--c);border-radius:6px;background:linear-gradient(180deg,rgba(var(--c-rgb),.18),rgba(var(--c-rgb),.06));text-align:center}
#ami-antibiais .pyr-lv b{display:block;font-family:'Jost',sans-serif;font-size:.86rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--c-ct)}
#ami-antibiais .pyr-lv em{display:block;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:.98rem;color:var(--nacre);margin:.15rem 0 .3rem}
#ami-antibiais .pyr-lv ul{list-style:none;margin:0;padding:0;text-align:left;font-size:.8rem;line-height:1.5;color:var(--gris-cl)}
#ami-antibiais .pyr-lv li{padding-left:1rem;position:relative;margin:.15rem 0}
#ami-antibiais .pyr-lv li::before{content:'âą';position:absolute;left:.2rem;color:var(--c)}
#ami-antibiais .pyr-biais{position:absolute;right:-.5rem;top:-.7rem;transform:translateX(100%);white-space:nowrap;font-family:'Jost',sans-serif;font-size:.72rem;letter-spacing:.06em;color:var(--rubis-ct);border:1px solid rgba(var(--rubis-rgb),.5);border-radius:999px;padding:.2rem .6rem;background:var(--encre)}
#ami-antibiais .pyr-fract{width:100%;text-align:center;padding:.5rem .8rem;margin:.2rem 0;border-top:1px dashed var(--rubis);border-bottom:1px dashed var(--rubis);color:var(--rubis-ct);font-size:.82rem;line-height:1.45}
#ami-antibiais .pyr-fract b{display:block;font-weight:600}
#ami-antibiais .pyr-fract span{color:var(--gris-cl);font-size:.78rem}
#ami-antibiais .pyr-fin{margin-top:.6rem;text-align:center;font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1.02rem;line-height:1.5;color:var(--or-pale)}
#ami-antibiais .pyr-fin p{margin:.3rem 0}
#ami-antibiais .pyr-fin b{font-style:normal;color:var(--or);font-weight:500}
@media(max-width:1100px){#ami-antibiais .pyr-biais{position:static;transform:none;display:inline-block;margin-bottom:.35rem;white-space:normal}}
@media(max-width:640px){#ami-antibiais .pyr-lv{width:max(var(--w),86%);min-width:0}}
/* ââ LES DEUX TRIANGLES RESTENT DES TRIANGLES SUR TĂLĂPHONE ââ */
@media(max-width:720px){
#ami-apokalypse .v12-diptych .v12-triad-map{display:block!important;position:relative!important;height:auto!important;min-height:0!important;aspect-ratio:320/250;max-width:390px;margin:.4rem auto 1rem;padding:0!important}
#ami-apokalypse .v12-diptych .v12-triad-map svg{display:block!important;position:absolute;inset:16% 24% 18%;width:52%;height:66%;overflow:visible}
#ami-apokalypse .v12-diptych .v12-triad-node{position:absolute!important;width:31%!important;max-width:none!important;padding:0!important;border:0!important;background:none!important;text-align:center!important;font-size:.72rem!important;line-height:1.25!important;transform:none!important}
#ami-apokalypse .v12-diptych .v12-triad-node b{font-size:.88rem!important;line-height:1.15!important}
#ami-apokalypse .v12-diptych .v12-triad-node span:not(.ico){font-size:.72rem!important;line-height:1.25!important}
#ami-apokalypse .v12-diptych .v12-triad-node .ico{display:block!important;font-size:1.2rem!important;margin:0 0 .1rem!important}
#ami-apokalypse .v12-diptych .v12-triad-node.top-left{left:0!important;top:0!important;right:auto!important;bottom:auto!important}
#ami-apokalypse .v12-diptych .v12-triad-node.top-right{right:0!important;top:0!important;left:auto!important;bottom:auto!important}
#ami-apokalypse .v12-diptych .v12-triad-node.bottom{left:50%!important;bottom:0!important;top:auto!important;right:auto!important;transform:translateX(-50%)!important}
#ami-apokalypse .v12-diptych .v12-triad-node.top{left:50%!important;top:0!important;right:auto!important;bottom:auto!important;transform:translateX(-50%)!important}
#ami-apokalypse .v12-diptych .v12-triad-node.left{left:0!important;bottom:0!important;top:auto!important;right:auto!important}
#ami-apokalypse .v12-diptych .v12-triad-node.right{right:0!important;bottom:0!important;top:auto!important;left:auto!important}
#ami-apokalypse .v12-diptych .v12-triad-center{display:grid!important;left:50%;top:52%;transform:translate(-50%,-50%);width:44px;height:44px;font-size:1.1rem}
#ami-apokalypse .v12-diptych .v12-triad{min-height:0!important}
}
</style>
'''
rep('</head>', css + '</head>', why='feuille des retours 3')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p12_barre_triangles.py p12_barre_triangles.py37 lignes
0.1 Apokalypse â retours 4 : la barre n'ouvre plus son tiroir toute seule ; triangles, une seule mise en page.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P12 · 0.1 Apokalypse â retours de Diane du 17 sept (03h40) :
· la barre du site n'ouvre plus son tiroir (Tomes + Volumes) toute seule : la page courante reste marquée (.ici), le tiroir
s'ouvre d'un clic sur đł la ForĂȘt, comme les deux autres ;
· les deux Triangles : une seule mise en page Ă toutes les largeurs (carte en proportion, nĆuds aux sommets) â sur grand
écran la carte de 245 px laissait les sommets loin du dessin."""
import re
h = open('apo.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
rep(""" if(a){ a.classList.add('ici');
deplie(true,false);
var p=N.querySelector('.porte[data-c="foret"]'); if(p) pose(p,true);
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}""",
""" if(a){ a.classList.add('ici');
/* la page se marque, mais n'ouvre plus le tiroir toute seule (Diane, 17 sept) : un clic sur đł la ForĂȘt l'ouvre, Ă la bonne rangĂ©e */
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}""", why='barre : le tiroir ne sâouvre plus tout seul')
# les triangles : la rÚgle †720 px devient la rÚgle de toutes les largeurs
a = h.find('/* ââ LES DEUX TRIANGLES RESTENT DES TRIANGLES SUR TĂLĂPHONE ââ */'); assert a > 0
b = h.find('@media(max-width:720px){', a); assert b > 0 and b - a < 200
h = h[:b] + h[b + len('@media(max-width:720px){'):]
e = h.find('</style>', a); seg = h[a:e]
k = seg.rfind('}\n'); assert seg.rstrip().endswith('}')
h = h[:a] + seg.rstrip()[:-1] + '\n' + h[e:]; J.append('triangles : une seule mise en page, toutes largeurs')
open('apo.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p8_etude_osullivan.py p8_etude_osullivan.py (dans le dossier de v23.html)30 lignes
0.3 Union V23 â V24 : l'Ă©tude O'Sullivan & Ronis dans les Chiffres du RĂ©el, 33 â 34, lien de source en or.
â§ Copier le script
# -*- coding: utf-8 -*-
"""0.3 L'Union du TouT V23 â V24 â l'Ă©tude O'Sullivan & Ronis (JIV, 2 avril 2026) versĂ©e dans đ Les Chiffres du RĂ©el,
« Dans le monde » (Diane, 17 sept 04h) : un chiffre large + une note de lecture. 33 â 34 chiffres sourcĂ©s."""
h = open('v23.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
bloc = '''<div class="ch rouge wide"><span class="v">95 % â 65 %</span> <span class="l"><b>95,1 % de 2 689 hommes de 18 Ă 34 ans</b> (Ătats-Unis, Canada), interrogĂ©s <b>anonymement</b>, dĂ©clarent avoir rĂ©cemment usĂ© d'au moins une stratĂ©gie pour obtenir un rapport sexuel d'une femme <b>dont ils savaient qu'elle ne le voulait pas et n'avait pas consenti</b> â sans lien antĂ©rieur avec elle. <b>65 % de ces occasions ont « abouti ».</b> 8,94 stratĂ©gies employĂ©es en moyenne sur les 36 recensĂ©es ; pression physique rĂ©pĂ©tĂ©e et coercition verbale les plus frĂ©quentes, contrainte physique et douleur « moins frĂ©quentes mais pas rares ». <b>70 % n'y voient aucune consĂ©quence nĂ©gative.</b></span> <span class="s">â O'Sullivan & Ronis, <i>Journal of Interpersonal Violence</i>, en ligne le 2 avril 2026 â <a href="https://doi.org/10.1177/08862605261432630">doi 10.1177/08862605261432630</a> (rĂ©sumĂ© recoupĂ© ; l'article complet est sous accĂšs Ă©diteur)</span></div>
'''
note = '''<div class="note key"><p><b>â Ce que l'anonymat rĂ©vĂšle â le chiffre qui ferme la boucle.</b></p>
<p>Presque toutes les enquĂȘtes interrogent <b>les femmes</b> qui ont subi, ou des hommes qui acceptent d'ĂȘtre identifiĂ©s. Celle-ci ĂŽte le nom : <b>et les hommes disent ce que les femmes disaient dĂ©jĂ .</b> L'Ă©cart entre les deux comptes â le « 1 sur 3 » des victimes contre le « presque jamais » des auteurs â n'Ă©tait pas dans les faits : il Ă©tait dans le dĂ©ni. Les auteurs les plus violents sont ceux qui se jugent <em>meilleurs que leurs pairs « pour obtenir ce qu'ils veulent des femmes »</em> ; les motifs dĂ©clarĂ©s sont l'envie (38 %) et l'occasion (28 %).</p>
<p style="margin-bottom:0"><b>Les bornes du chiffre.</b> Ăchantillon recrutĂ© en ligne (Qualtrics), autodĂ©claration, « rĂ©cemment » sans durĂ©e dans le rĂ©sumĂ© ; il ne dit pas la prĂ©valence dans la population, il dit ce qu'un homme raconte <em>quand il ne risque rien</em>. C'est exactement le <b>Test 2</b> d'<a href="https://agdistys.fr/ami-apokalypse">Apokalypse</a> (« le mĂȘme rĂ©cit, dit par un autre, aurait-il Ă©tĂ© cru ? ») mesurĂ© en grand : le mĂȘme fait, cru dĂšs qu'il est dit par l'auteur.</p></div>
'''
anchor = '<span class="s">â PNUD · Indice des normes sociales relatif Ă l\'Ă©galitĂ© des sexes, 5 mars 2020 â confirmĂ© par l\'Ă©dition 2023 (80 pays, 85 % de la population)</span></div>'
rep(anchor, anchor + '\n' + bloc, why='chiffre OâSullivan & Ronis aprĂšs le PNUD')
# la note aprÚs la grille .chf de « Dans le monde »
i = h.find('<section id="chiffres-monde"'); j = h.find('<div class="cit" style="margin-top:1.6rem">« L\'Homme est la seule espÚce', i); assert i > 0 and j > i
h = h[:j] + note + h[j:]; J.append('note de lecture avant la citation dâHĂ©ritier')
rep('Trente-trois chiffres sourcés.', 'Trente-quatre chiffres sourcés.', why='carte : 34 chiffres')
# identité V24
rep('<meta name="ami:version" content="V23"', '<meta name="ami:version" content="V24"', why='meta version')
rep('data-ami="version">V23', 'data-ami="version">V24', n=2, why='data-ami version')
rep('</head>', '<style id="ami-retours-v24">#ami-union .ch .s a{color:var(--or);text-decoration:none;border-bottom:1px solid rgba(var(--or-rgb),.4);word-break:break-all}</style>\n</head>', why='lien de source en or')
open('v24.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
audit_rendu_leger.py audit_rendu_leger.py page.html ".ong" ".sog, .x-tb"136 lignes
Audit au rendu avec les sous-onglets ciblés par sélecteur (pages à instruments : le rendu « tout pressé » ne finit pas).
â§ Copier le script
# -*- coding: utf-8 -*-
"""Audit au rendu (Chromium) â on mesure ce que le navigateur FAIT, tous
les onglets ouverts, Ă plusieurs largeurs. Ne corrige rien."""
import sys, json, collections, time
from playwright.sync_api import sync_playwright
SRC = sys.argv[1]
URL = 'file://' + __import__('os').path.abspath(SRC)
TOPS = sys.argv[2] if len(sys.argv) > 2 else '.tab-btn-full, .tab-bar .tab-btn' # les onglets de premier niveau de la page
SUBS = sys.argv[3] if len(sys.argv) > 3 else 'button' # les sous-onglets à presser (sélecteur)
JS_MESURE = r"""
(opts) => {
const seen = opts.seen; const out = {texte:[], petits:[], ital:[], cibles:[], tables:[], grilles:[], nb:0};
const EMO = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{A9A0}-\u{A9FF}]/u;
function key(el){ const p=[]; let e=el; while(e && e.nodeType===1 && e!==document.body){ let i=0,s=e; while((s=s.previousElementSibling)) i++; p.push(e.tagName+i); e=e.parentElement;} return p.join('/'); }
function vis(el){ const r=el.getBoundingClientRect(); if(r.width<1||r.height<1) return false; const cs=getComputedStyle(el); if(cs.visibility==='hidden'||cs.display==='none'||parseFloat(cs.opacity)===0) return false; return true; }
function parse(c){ const m=c.match(/rgba?\(([^)]+)\)/); if(!m) return null; const a=m[1].split(',').map(parseFloat); return {r:a[0],g:a[1],b:a[2],a:a.length>3?a[3]:1}; }
function lum({r,g,b}){ const f=v=>{v/=255; return v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055,2.4)}; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
function comp(top,bot){ const a=top.a; return {r:top.r*a+bot.r*(1-a), g:top.g*a+bot.g*(1-a), b:top.b*a+bot.b*(1-a), a:1}; }
function bgOf(el){ // compose les fonds translucides jusqu'au premier opaque
const layers=[]; let e=el;
while(e && e.nodeType===1){ const cs=getComputedStyle(e); const c=parse(cs.backgroundColor); const img=cs.backgroundImage;
if(c && c.a>0){ layers.push(c); if(c.a>=1) break; }
if(img && img!=='none' && /gradient/.test(img)){ layers.push({r:9,g:9,b:14,a:1, grad:true}); break; }
e=e.parentElement; }
if(!layers.length || layers[layers.length-1].a<1) layers.push({r:9,g:9,b:14,a:1, fallback:true});
let bg=layers[layers.length-1]; for(let i=layers.length-2;i>=0;i--) bg=comp(layers[i],bg);
return {bg, grad: layers.some(l=>l.grad), fallback: layers.some(l=>l.fallback)};
}
function ratio(fg,bg){ const l1=lum(fg), l2=lum(bg); return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05); }
const walker=document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while((n=walker.nextNode())){
const t=n.textContent.replace(/\s+/g,' ').trim(); if(t.length<2) continue;
const el=n.parentElement; if(!el || ['SCRIPT','STYLE','NOSCRIPT','TITLE'].includes(el.tagName)) continue;
if(el.closest('svg')) continue;
if(!vis(el)) continue;
const k=key(el)+'|'+t.slice(0,40); if(seen[k]) continue; seen[k]=1; out.nb++;
const cs=getComputedStyle(el); const fs=parseFloat(cs.fontSize); const fw=parseInt(cs.fontWeight)||400;
const fg=parse(cs.color); const B=bgOf(el); const r=ratio(comp(fg,B.bg),B.bg);
const large = fs>=24 || (fs>=18.66 && fw>=700);
const seuil = large?3:4.5;
const tag=el.tagName.toLowerCase()+(el.className&&typeof el.className==='string'?'.'+el.className.trim().split(/\s+/).slice(0,2).join('.'):'');
if(r<seuil) out.texte.push({tag, t:t.slice(0,70), fs:+fs.toFixed(1), fw, r:+r.toFixed(2), seuil, fg:cs.color, bg:`rgb(${B.bg.r|0},${B.bg.g|0},${B.bg.b|0})`, grad:B.grad});
if(fs<11.5) out.petits.push({tag, t:t.slice(0,50), fs:+fs.toFixed(1)});
if(cs.fontStyle==='italic' && EMO.test(t)) out.ital.push({tag, t:t.slice(0,60)});
if(cs.textTransform==='uppercase' && /Cormorant/i.test(cs.fontFamily)) out.texte.push({tag, t:t.slice(0,50), capsCormorant:true, fs:+fs.toFixed(1)});
}
document.querySelectorAll('a[href],button,[role=button],[role=tab]').forEach(el=>{
if(!vis(el)) return; const k='c|'+key(el); if(seen[k]) return; seen[k]=1;
const r=el.getBoundingClientRect(); if(r.width<24||r.height<24) out.cibles.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], t:(el.textContent||'').trim().slice(0,30), w:+r.width.toFixed(1), h:+r.height.toFixed(1)});
});
document.querySelectorAll('table').forEach(tb=>{
if(!vis(tb)) return; const k='t|'+key(tb); if(seen[k]) return; seen[k]=1;
let e=tb.parentElement, roule=false; while(e && e!==document.body){ const o=getComputedStyle(e).overflowX; if(o==='auto'||o==='scroll'){roule=true;break;} e=e.parentElement; }
const r=tb.getBoundingClientRect(); const pr=tb.parentElement.getBoundingClientRect();
if(r.width>pr.width+1 && !roule) out.tables.push({t:(tb.textContent||'').trim().slice(0,50), w:+r.width.toFixed(0), pw:+pr.width.toFixed(0)});
});
document.querySelectorAll('*').forEach(el=>{
const cs=getComputedStyle(el); if(cs.display!=='grid'||!vis(el)) return; const k='g|'+key(el); if(seen[k]) return; seen[k]=1;
const cols=cs.gridTemplateColumns.split(' ').length; const kids=[...el.children].filter(vis).length;
if(cols>1 && kids>cols && kids%cols!==0) out.grilles.push({cls:(el.className||'').toString().trim().slice(0,40), cols, kids, orphelines: kids%cols});
});
return out;
}
"""
JS_OVERFLOW = r"""
() => { const d=document.documentElement; const w=d.clientWidth; const bad=[];
document.querySelectorAll('body *').forEach(el=>{ if(el.closest('svg')) return; const r=el.getBoundingClientRect(); if(r.width<1) return;
if(r.right>w+1 || r.left<-1){ let e=el.parentElement, clip=false; while(e){ const o=getComputedStyle(e).overflowX; if(o==='hidden'||o==='clip'||o==='auto'||o==='scroll'){clip=true;break;} e=e.parentElement;}
bad.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], right:+r.right.toFixed(0), left:+r.left.toFixed(0), clip}); } });
return {scrollW:d.scrollWidth, clientW:w, n:bad.length, ex:bad.slice(0,12)}; }
"""
def ouvre_tout(page, seen, acc):
"""Clique chaque onglet et chaque sous-onglet, mesure aprÚs chaque état."""
def mesure():
o = page.evaluate(JS_MESURE, {'seen': seen})
for k in ('texte','petits','ital','cibles','tables','grilles'): acc[k].extend(o[k])
acc['nb'] += o['nb']
mesure()
tops = page.query_selector_all(TOPS)
for b in tops:
try: b.click(timeout=2000); page.wait_for_timeout(150)
except Exception as e: acc['clics_rates'].append(str(e)[:80])
mesure()
# sous-onglets visibles
subs = page.query_selector_all(SUBS)
for s in subs:
try:
if not s.is_visible(): continue
s.click(timeout=1500); page.wait_for_timeout(120); mesure()
except Exception: pass
# sommaires : on les ouvre
for h in page.query_selector_all('.som-h, .som-hd, [aria-expanded]'):
try:
if h.is_visible(): h.click(timeout=800)
except Exception: pass
mesure()
with sync_playwright() as p:
br = p.chromium.launch()
res = {}
for W,H in ((390,844),(1280,900)):
ctx = br.new_context(viewport={'width':W,'height':H}, device_scale_factor=1)
page = ctx.new_page()
errs=[]; page.on('pageerror', lambda e: errs.append(str(e)[:200])); page.on('console', lambda m: errs.append('console.'+m.type+': '+m.text[:160]) if m.type in ('error','warning') else None)
page.goto(URL, wait_until='load'); page.wait_for_timeout(1200)
seen={}; acc=dict(texte=[],petits=[],ital=[],cibles=[],tables=[],grilles=[],nb=0,clics_rates=[])
ouvre_tout(page, seen, acc)
ov = page.evaluate(JS_OVERFLOW)
# fonts réellement chargées ?
fonts = page.evaluate("() => [...document.fonts].filter(f=>f.status==='loaded').map(f=>f.family+' '+f.weight+' '+f.style)")
res[W] = dict(erreurs=errs, mesures=acc, overflow=ov, fonts=sorted(set(fonts)))
page.screenshot(path=f'capture_{W}.png', full_page=False)
ctx.close()
for W in (320,768):
ctx = br.new_context(viewport={'width':W,'height':800}); page=ctx.new_page(); page.goto(URL, wait_until='load'); page.wait_for_timeout(800)
for b in page.query_selector_all(TOPS):
try: b.click(timeout=1500); page.wait_for_timeout(100)
except Exception: pass
res[W]=dict(overflow=page.evaluate(JS_OVERFLOW)); ctx.close()
br.close()
json.dump(res, open('audit_rendu.json','w'), ensure_ascii=False, indent=1)
for W,r in res.items():
print(f'\n=== {W} px ===')
if 'erreurs' in r:
print('erreurs JS/console :', len(r['erreurs'])); [print(' ',e) for e in r['erreurs'][:10]]
m=r['mesures']; print('nĆuds texte mesurĂ©s :', m['nb'], '· clics ratĂ©s :', len(m['clics_rates']))
print('contraste sous seuil :', len([x for x in m['texte'] if not x.get('capsCormorant')]), '· caps en Cormorant :', len([x for x in m['texte'] if x.get('capsCormorant')]))
print('texte < 11,5 px :', len(m['petits']), '· émoji en italique :', len(m['ital']), '· cibles < 24 px :', len(m['cibles']), '· tableaux qui débordent sans rouleau :', len(m['tables']), '· grilles à orphelines :', len(m['grilles']))
print('polices chargées :', r['fonts'])
print('overflow :', r['overflow']['scrollW'], '/', r['overflow']['clientW'], '· éléments hors cadre :', r['overflow']['n'], r['overflow']['ex'][:5])
p1_ini.py · p1b_ini.py · p2_ini.py · p4_ini.py · p4b_ini.py · p4c_ini.py · p5_ini_mutations.py python3 pX_ini.py (dans le dossier de ini.html)1 lignes
L'Initiation V18 â V19 : texte + cadence de la une · ConScience dans les donnĂ©es des instruments · forme · anatomie (voie minimale) · zoom + plancher une/pied · contraste/-ct · mutations (109).
â§ Copier le script
(script absent du dossier outils/)
p6_retours_ini.py python3 p6_retours_ini.py63 lignes
L'Initiation, retours du 17/09 14h : liens en ligne, cartes Ă leur pierre, colonne du Croisement, voiles .035, ⌠sur les plis, Donner Vie radial.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · L'Initiation â les retours de Diane du 17 sept 14h, aprĂšs la palette rĂ©parĂ©e (p3_palette.py : la racine est re-cherchĂ©e
avant d'ĂȘtre Ă©crite â la V19 livrĂ©e Ă 05h avait perdu ~130 variables, d'oĂč les figures noires, LE/Ă LA VIE en gris, la une sans
ses lueurs).
1. les trois liens locaux â adresses en ligne (le-respect, ami-karma, larborescence-psychique) ;
2. les cartes C1âŠC13 des GĂ©omĂ©tries prennent la couleur de leur pierre (bord + voile) ;
3. le Croisement : les paragraphes 5 et 6 (.x-chap) et les plis (.x-plie) partagent la mĂȘme colonne ;
4. le fond : les voiles d'or des cantons descendent de .07 Ă .035 (le fond lit onyx, la lueur reste) ;
5. chaque <details> visible porte un âŒ Ă droite, qui se retourne quand il est ouvert ;
6. la une : « Donner Vie » en dégradé radial ; LE · à LA VIE retrouvent l'or pùle (variable revenue)."""
import re
h = open('ini.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
# 1 · liens
rep('href="Le_Respect_La_Loi_du_Reel_aMi.html#pv-loi"', 'href="https://agdistys.fr/le-respect#pv-loi"', why='lien Le Respect (#pv-loi)')
rep('href="Le_Respect_La_Loi_du_Reel_aMi.html"', 'href="https://agdistys.fr/le-respect"', n=2, why='lien Le Respect')
rep('href="6.0_KARMA_MapMonde_aMi.html"', 'href="https://agdistys.fr/ami-karma"', why='lien Karma')
rep('href="Arborescence_Sante_Mentale_aMi_V10.html"', 'href="https://agdistys.fr/larborescence-psychique"', why='lien Arborescence')
assert not re.search(r'href="[^"#h][^"]*\.html', h), 'il reste un lien local'
# 4 · les voiles d'or
n = 0
for a, b in (('rgba(var(--ambre-rgb),.07),transparent 46%)', 'rgba(var(--ambre-rgb),.035),transparent 46%)'),
('rgba(var(--ambre-rgb),.07),transparent 70%)', 'rgba(var(--ambre-rgb),.035),transparent 70%)'),
('rgba(var(--ambre-rgb),.07),transparent 62%)', 'rgba(var(--ambre-rgb),.035),transparent 62%)')):
c = h.count(a); h = h.replace(a, b); n += c
J.append(f'voiles dâor des cantons .07 â .035 Ă{n}')
# 6 · Donner Vie en radial
old = 'background:linear-gradient(180deg,var(--nacre) 0%,var(--ambre-ct) 30%,var(--ambre) 60%,var(--ambre-gras) 100%);\n -webkit-background-clip:text'
assert h.count(old) == 1, h.count(old)
h = h.replace(old, 'background:radial-gradient(ellipse 70% 120% at 50% 35%,var(--nacre) 0%,var(--ambre-ct) 32%,var(--ambre) 64%,var(--ambre-gras) 100%);\n -webkit-background-clip:text'); J.append('« Donner Vie » : dégradé radial')
css = '''
<style id="ami-retours-1">
/* ââ RETOURS DU 17 SEPT (14h) ââ */
/* 2 · les cartes C1âŠC13 : la couleur de leur pierre (color = --pierre-txt, posĂ©e en ligne par le script) */
#i-contenu #ami-geometries .g-carte{box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 70%,transparent);background:color-mix(in srgb,currentColor 12%,var(--onyx))}
#ami-geometries .g-carte .g-c-n,#ami-geometries .g-carte em{color:currentColor;opacity:1}
#i-contenu #ami-geometries .g-carte:hover{background:color-mix(in srgb,currentColor 20%,var(--onyx));box-shadow:inset 0 0 0 1px currentColor}
@supports not (background:color-mix(in srgb,red 9%,blue)){#ami-geometries .g-carte{background:rgba(var(--onyx-rgb),.55)}}
/* 3 · le Croisement : une seule colonne de lecture pour les paragraphes et les plis */
#i-contenu #ami-croisement .x-plie{max-width:calc(760px + 2rem)}
/* 5 · un pli se voit : âŒ Ă droite, retournĂ© quand c'est ouvert */
#i-contenu details > summary{position:relative;padding-right:2.2rem!important;list-style:none}
#i-contenu details > summary::-webkit-details-marker{display:none}
#i-contenu details > summary::after{content:'âŒ';position:absolute;right:.9rem;top:50%;transform:translateY(-50%);font-size:.8em;opacity:.85;transition:transform .18s;color:currentColor}
#i-contenu details[open] > summary::after{transform:translateY(-50%) rotate(180deg)}
#ami-metamatrice .mm-d summary:before{content:''}
#ami-metamatrice .mm-d[open] summary:before{content:''}
@media(prefers-reduced-motion:reduce){#i-contenu details > summary::after{transition:none}}
</style>
'''
rep('</head>', css + '</head>', why='feuille des retours')
open('ini.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p7_retours2_ini.py python3 p7_retours2_ini.py53 lignes
L'Initiation, retours du 17/09 15h : toit sur 2 lignes, sous-onglets colorés et collés sous la barre, C12, Déflagration/Miroir.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P7 · L'Initiation â retours de Diane du 17 sept 15h.
1. C12 Le Miroir : « sugilite · quartz rose » (la pierre de la Charte, et le voile de la page) ;
2. l'onglet-toit sur deux lignes : le sous-titre sous le titre, toujours ;
3. les sous-onglets : sous-titre à la couleur de l'onglet principal (--tc/--tcl posés sur chaque .sbar par volet),
sans capitales, plus petit que le titre ; le titre de sous-onglet ne dépasse plus celui de l'onglet ;
4. la sous-barre collait sous la barre du site et passait DERRIĂRE la barre des onglets (top:--nav-h seul) â sous les onglets ;
5. la Spirale : « Déflagration » sortait du mode Miroir seulement si la forme changeait ; il en sort toujours (une ligne du moteur,
Ă la demande de Diane : le bouton Miroir se comportait comme une case Ă cocher)."""
h = open('ini.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'â attendu {n}, trouvĂ© {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} Ă{n}')
rep("p:'quartz-rose', pn:'quartz rose',", "p:'quartz-rose', pn:'sugilite · quartz rose',", why='C12 : sugilite · quartz rose')
rep(""" if(S.cible!=='tore'){ S.prec = S.cible; S.cible = 'tore'; S.morph = 0; S.miroir=false;
RACINE.classList.remove('miroir'); majBarre(); }
S.cVise = 0.09;""",
""" if(S.cible!=='tore'){ S.prec = S.cible; S.cible = 'tore'; S.morph = 0; }
/* la DĂ©flagration sort TOUJOURS du Miroir â pas seulement quand la forme change (Diane, 17 sept) */
S.miroir=false; RACINE.classList.remove('miroir'); majBarre();
S.cVise = 0.09;""", why='Spirale : Déflagration sort du Miroir dans tous les cas')
# les couleurs des cinq volets, telles que les onglets les portent
VOL = {'contrat': ('cornaline', '255,120,0'), 'nature': ('emeraude', '11,141,56'), 'croisement': ('amethyste', '160,74,250'),
'culture': ('rubis', '227,50,50'), 'formule': ('ambre', '255,197,69')}
sbars = '\n'.join(f'#pan-{k} .sbar{{--tc:var(--{p});--tcl:var(--{p}-ct);--tc-rgb:{rgb}}}' for k, (p, rgb) in VOL.items())
css = '''
<style id="ami-retours-2">
/* ââ RETOURS DU 17 SEPT (15h) ââ */
/* 2 · l'onglet-toit : titre, puis sous-titre dessous */
#i-tl .ong.ong-toit{flex-wrap:wrap;row-gap:.1rem}
#i-tl .ong.ong-toit .os{flex:1 0 100%;text-align:center}
/* 3 · les sous-onglets : la couleur de leur volet, et pas plus gros que les onglets */
''' + sbars + '''
#i-contenu .sbar .sng{color:var(--gris-cl)}
#i-contenu .sbar .sng .sot{font-size:.84rem;color:var(--tcl)}
#i-contenu .sbar .sng .sos{font-size:.72rem;text-transform:none;letter-spacing:.03em;color:var(--tcl);opacity:.82}
#i-contenu .sbar .sng[aria-selected="true"] .sos{opacity:1}
#i-tl .ong .ot{font-size:.9rem}
@media(max-width:720px){#i-tl .ong .ot{font-size:.8rem}#i-contenu .sbar .sng .sot{font-size:.78rem}}
/* 4 · la sous-barre colle SOUS la barre des onglets, pas derriÚre */
#i-contenu .sbar{top:calc(min(var(--nav-h,0px),9rem) + var(--bar-h,0px))!important;z-index:19}
</style>
'''
rep('</head>', css + '</head>', why='feuille des retours 2')
open('ini.html', 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p8_sans_pont_iframe.py python3 p8_sans_pont_iframe.py ini.html18 lignes
L'Initiation, 17/09 20h53 : ĂŽte le pont iframe mort des 5 instruments (window.parent/postMessage) â hypothĂšse « mots filtrĂ©s », rĂ©futĂ©e ensuite.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P8 · L'Initiation â le pont iframe s'en va. Cinq instruments gardaient un « mesure() » qui envoyait leur hauteur Ă
window.parent par postMessage (hĂ©ritage du temps oĂč ils vivaient dans des iframes). Ici la page est collĂ©e en ligne :
window.parent === window, le code ne faisait rien â mais « window.parent » et « postMessage » sont exactement les mots
qu'un hébergeur filtre « par mesure de sécurité », et l'Initiation est LA SEULE page du corpus à les porter (Apokalypse,
PsychaTrice, l'Union : 0). HypothĂšse pour le bloc Webnode qui reste vide (Diane, 17 sept 20h50)."""
import re
h = open('ini.html', encoding='utf-8').read()
n0 = h.count('postMessage') + h.count('window.parent')
h2, a = re.subn(r"try\{ if\(window\.parent && window\.parent!==window\)\s*window\.parent\.postMessage\(\{type:'astroHeight',\s*height:[^}]*\}, '\*'\); \}catch\(_\)\{\}",
"/* pont iframe Îté (P8, 17 sept) : la page est collée en ligne, rien à mesurer pour un parent */", h)
h2, b = re.subn(r"try\{var h=document\.documentElement\.scrollHeight;\s*parent\.postMessage\(\{type:'astroHeight',height:h\},'\*'\);\s*parent\.postMessage\(\{type:'ami-widget-resize',height:h\},'\*'\);\}catch\(e\)\{\}",
"/* pont iframe Îté (P8, 17 sept) */", h2)
h = h2
assert 'postMessage' not in h and 'window.parent' not in h and 'parent.postMessage' not in h, (h.count('postMessage'), h.count('window.parent'))
open('ini.html', 'w', encoding='utf-8').write(h)
print(f'â pont iframe ĂŽtĂ© : {a} mesure() + {b} sendH() â {n0} mots window.parent/postMessage â 0')
p9_bom_dans_le_texte.py python3 p9_bom_dans_le_texte.py11 lignes
17/09 : un BOM (U+FEFF) copiĂ© dans le texte â cherchĂ©, ĂŽtĂ©.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P9 · L'Initiation â un caractĂšre U+FEFF (marque d'ordre d'octets, invisible) vivait EN CLAIR au milieu d'un script
(l'export CSV d'un instrument : new Blob(['ï»ż'+csv]) pour qu'Excel lise l'UTF-8). Aucune des trois pages qui passent
sur Webnode n'en porte ; l'Initiation (V18 comme V19) en a un â deuxiĂšme candidat pour le bloc vide (17 sept 21h10).
RemplacĂ© par son Ă©chappement JS '\\uFEFF' : mĂȘme octets dans le fichier CSV produit, plus de caractĂšre invisible dans la page."""
h = open('ini.html', encoding='utf-8').read()
n = h.count('ï»ż'); assert n == 1 and "['ï»ż'+csv]" in h, n
h = h.replace("['ï»ż'+csv]", "['\\uFEFF'+csv]")
assert 'ï»ż' not in h
open('ini.html', 'w', encoding='utf-8').write(h); print('â U+FEFF en clair â \\uFEFF Ă©chappĂ© Ă1')
p10_moins_de_lignes.py python3 p10_moins_de_lignes.py (lit et réécrit ini.html)51 lignes
Compacte sans changer le sens : terser sur les blancs/commentaires des scripts, une rĂšgle CSS par ligne (24 531 â 7 668 lignes, rendu identique).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P10 · L'Initiation â MOINS DE LIGNES. Les quatre tests Webnode (17 sept 21h30) montrent : chaque morceau passe, le fichier
entier ne passe pas â ce n'est donc pas un contenu, c'est un seuil. Pas les octets (Apokalypse 3,0 Mo passe, l'Initiation
2,78 Mo non) : les LIGNES. Apokalypse 6 640 lignes, PsychaTrice 7 189, l'Union 11 239, le test D (passe) 16 995 â
l'Initiation 24 771. HypothÚse : un plafond entre 17 000 et 24 700 lignes dans le bloc « Code HTML ».
RemĂšde sans rien changer au rendu : les scripts passent par terser (blancs et commentaires seulement â ni compress ni
mangle, le code reste lisible mot Ă mot), les feuilles perdent leurs commentaires et leurs retours Ă la ligne,
les lignes vides du HTML s'en vont. Le HTML lui-mĂȘme n'est pas touchĂ© (un retour Ă la ligne y est un espace)."""
import re, subprocess, tempfile, os
h = open('ini.html', encoding='utf-8').read()
L0 = h.count('\n')
# un commentaire HTML qui cite « <script> » trompe tout dĂ©coupage (le mien, et peut-ĂȘtre celui de l'hĂ©bergeur) : on le neutralise
h = re.sub(r'<!--(.*?)-->', lambda m: '<!--' + m.group(1).replace('<script', 'âčscript').replace('</script', 'âč/script') + '-->', h, flags=re.S)
def js_min(code):
with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False, encoding='utf-8') as f: f.write(code); p = f.name
try:
r = subprocess.run(['terser', p, '--format', 'beautify=false,comments=false,max_line_len=800'], capture_output=True, text=True, encoding='utf-8')
if r.returncode != 0: raise SystemExit('â terser : ' + r.stderr[:400])
return r.stdout
finally: os.unlink(p)
n_js = 0
def sub_script(m):
global n_js
ouv, code, ferm = m.group(1), m.group(2), m.group(3)
if 'src=' in ouv or 'type="application' in ouv or 'type="text/template' in ouv: return m.group(0)
n_js += 1
return ouv + '\n' + js_min(code).replace('</script', '<\\/script') + '\n' + ferm
h = re.sub(r'(<script\b[^>]*>)(.*?)(</script>)', sub_script, h, flags=re.S)
n_css = 0
def sub_style(m):
global n_css; n_css += 1
css = m.group(2)
css = re.sub(r'/\*.*?\*/', '', css, flags=re.S) # commentaires
css = re.sub(r'\s*\n\s*', ' ', css) # retours Ă la ligne â espace
css = re.sub(r'\}', '}\n', css) # une rĂšgle par ligne (lisible, et trĂšs en-dessous du seuil)
return m.group(1) + '\n' + css.strip() + '\n' + m.group(3)
# les feuilles, hors des scripts (un script écrit parfois « <style> » dans une chaßne : on ne le découpe pas)
parts = re.split(r'(<script\b[^>]*>.*?</script>)', h, flags=re.S)
for k in range(0, len(parts), 2):
parts[k] = re.sub(r'(<style\b[^>]*>)(.*?)(</style>)', sub_style, parts[k], flags=re.S)
h = ''.join(parts)
# les lignes vides du HTML (hors <pre>/<textarea> : la page n'en a pas qui comptent sur des vides)
h = re.sub(r'\n[ \t]*\n(?:[ \t]*\n)+', '\n', h)
L1 = h.count('\n')
open('ini.html', 'w', encoding='utf-8').write(h)
print(f'â {n_js} scripts par terser (blancs/commentaires), {n_css} feuilles compactĂ©es · lignes {L0:,} â {L1:,} · {len(h.encode())/1e6:.2f} Mo')
p12_test_k_jetons.py python3 p12_test_k_jetons.py J.html K.html33 lignes
TEST K : les 27 jetons d'API JS propres Ă l'Initiation Ă©crits en deux morceaux â mĂȘme code, aucune signature textuelle. RefusĂ© par Webnode : pas un filtre de mots.
â§ Copier le script
# -*- coding: utf-8 -*-
"""TEST K · 17 sept 23h. Mesuré sur les 236 pages du sitemap agdistys.fr : l'hébergeur accepte 3,1 Mo (ami-astroglobe, un
script de 1,8 Mo), donc ni les octets ni les lignes. Les seuls jetons d'API JS de l'Initiation qu'AUCUNE page en ligne
ne contient : AudioContext (+ createGain/createOscillator/frequency/currentTime/âŠRampToValueAtTime/setValueAtTime),
Float32Array, RegExp, acceptNode/nodeValue, createImageData/putImageData/drawImage, insertAdjacentHTML, onerror,
pointerId/setPointerCapture, roundRect, scrollBehavior, removeProperty, shadowBlur/shadowColor, zIndex, cancelable.
K = J avec chacun de ces jetons Ă©crit en deux morceaux (x["insert"+"AdjacentHTML"]) : mĂȘme code, mĂȘmes appels,
mais aucune signature textuelle. Si K passe â c'est un filtre de mots ; on cherche lequel. Si K ne passe pas â autre chose."""
import re, sys
SRC = sys.argv[1]; OUT = sys.argv[2]
h = open(SRC, encoding='utf-8').read()
TOK = ['AudioContext','webkitAudioContext','createGain','createOscillator','frequency','currentTime','setValueAtTime',
'exponentialRampToValueAtTime','linearRampToValueAtTime','Float32Array','RegExp','acceptNode','nodeValue',
'createImageData','putImageData','drawImage','insertAdjacentHTML','onerror','pointerId','setPointerCapture',
'roundRect','scrollBehavior','removeProperty','shadowBlur','shadowColor','zIndex','cancelable']
def deux(t): k = len(t)//2; return '"' + t[:k] + '"+"' + t[k:] + '"'
n = {}
def sub_script(m):
ouv, code, ferm = m.group(1), m.group(2), m.group(3)
for t in TOK:
c0 = code
code = re.sub(r'\.' + t + r'\b', '[' + deux(t) + ']', code) # x.tok â x["t"+"ok"]
code = re.sub(r'\bnew\s+' + t + r'\s*\(', 'new (window[' + deux(t) + '])(', code) # new Tok( â new (window["T"+"ok"])(
code = re.sub(r'(?<![\w$.\'"])' + t + r'\s*\(', '[' + deux(t) + '](', code) if t == 'acceptNode' else code # méthode d'objet littéral
code = re.sub(r'(?<=[{,])\s*' + t + r'\s*:', '[' + deux(t) + ']:', code) # {tok: â {["t"+"ok"]:
if code != c0: n[t] = n.get(t, 0) + 1
return ouv + code + ferm
h = re.sub(r'(<script\b[^>]*>)(.*?)(</script>)', sub_script, h, flags=re.S)
h = h.replace('data-test="J"', 'data-test="K"')
reste = [t for t in TOK if re.search(r'(?<!["\w])' + t + r'(?!["\w])', ' '.join(re.findall(r'<script\b[^>]*>(.*?)</script>', h, re.S)))]
open(OUT, 'w', encoding='utf-8').write(h)
print('â K :', ', '.join(f'{k}Ă{v}' for k, v in n.items()), '\n jetons encore nus :', reste or 'aucun', f'\n {len(h.encode())/1e6:.2f} Mo')
p13_retours3_ini.py python3 p13_retours3_ini.py ini_p10_avant.html ini_p13_lisible.html127 lignes
18/09 03h : 4 onglets (Formule Magique Îtée, la Dette versée dans le widget Respect), Emporter avant le pied, tanzanite/émeraude, Contrat ouvert au chargement, script « colle » (sticky mort sous overflow hidden).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P13 · L'Initiation â retours de Diane du 18 sept (01h50-03h20), sur la source LISIBLE (ini_p10_avant â ini.html ; p10 rejoue ensuite).
1. L'onglet đŹïž Formule Magique s'en va : vĂ©rifiĂ© phrase Ă phrase, son contenu est celui du volet đŹïž du widget Le Respect
(đ la Culture) â seule la table « La Dette, en Quatre Langages » manquait : elle est versĂ©e dans le widget, Ă sa place.
4 onglets de base : đ le toit des Contrats · đł la Nature · đ± le Croisement · đ la Culture.
2. â Emporter cette page sort du volet et revient avant le pied (toujours visible), comme dans 0.1/0.2/0.3.
3. Couleurs : une pierre par volet qui descend partout (onglet â sous-barre â halo du portail). Croisement = TANZANITE
(l'Alliance : Nature + Culture â dit par Diane) ; MĂ©taMatrice â Ă©meraude (son volet) ; Vibration â halo Ă©meraude, le rubis
reste sur la ligne « la pierre » ; le portail Culture perd le fond noir plat (#ami-respect â transparent).
4. Onglets collĂ©s sur Webnode : mesurĂ© en ligne, .b-c et .wnd-page ont overflow:hidden â position:sticky ne colle jamais.
Script « colle » : si un ancĂȘtre coupe le sticky, les barres (primaire, secondaires, tertiaires) sont fixĂ©es par script
sous l'en-tĂȘte fixe du site, en pile, avec un fantĂŽme de mĂȘme hauteur. Sur le fichier autonome, sticky natif + tertiaires collants."""
import re, sys
SRC = sys.argv[1] if len(sys.argv) > 1 else 'ini_p10_avant.html'
OUT = sys.argv[2] if len(sys.argv) > 2 else 'ini.html'
h = open(SRC, encoding='utf-8').read()
n = 0
def rep(old, new, k=1, label=''):
global h, n
c = h.count(old)
assert c == k, f'â {label or old[:60]!r} : {c} occurrence(s), {k} attendue(s)'
h = h.replace(old, new); n += 1
# ââ 1. l'onglet Formule Magique âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# le bouton de la barre
m = re.search(r'<button class="ong n-b" type="button" role="tab" data-p="formule".*?</button>', h, re.S)
assert m and h.count('data-p="formule"') >= 1
h = h[:m.start()] + h[m.end():]; n += 1
rep(', et la Formule Magique"', '"', 1, 'aria-label barre')
rep('đŹïž La Formule Magique tient le tout ensemble, et ferme le volume.',
'đŹïž La Formule Magique tient le tout ensemble â elle vit dans đ la Culture, avec Le Respect.', 1, 'rĂšgle')
rep('<b>Ici, les plans sont les quatre onglets de cette page</b> â đł la Nature, đ± le Croisement, đ la Culture et đŹïž La Formule Magique.',
'<b>Ici, les plans sont les trois onglets de cette page</b> â đł la Nature, đ± le Croisement, đ la Culture (oĂč vit đŹïž La Formule Magique, avec Le Respect).', 1, 'contrat')
rep('#pan-formule .sbar{--tc:var(--ambre);--tcl:var(--ambre-ct);--tc-rgb:255,197,69}\n', '', 1, 'css sbar formule')
# le volet entier, l'Emporter extrait
i = h.find('<div class="i-panneau" id="pan-formule"'); e = h.find('</main>', i); assert i > 0 and e > i
F = h[i:e]
emp = re.search(r'<section class="emporter".*?</section>', F, re.S); assert emp
# la table de la Dette, versée dans le widget Le Respect (fmq du volet Formule), aprÚs l'huissier
dette = ('<h5 class="fm-h3 fm-centre">La Dette, en Quatre Langages</h5>\n<div class="fmq">'
'<b>1 ·</b> « La dette est strictement proportionnelle aux dĂ©gĂąts infligĂ©s, et ne devient mesurable qu\'une fois la ConScience restaurĂ©e. » <i>â la plus directe et juridique</i><br>'
'<b>2 ·</b> « Le coĂ»t de la Dette Ă©quivaut aux violences infligĂ©es sur autrui ; elle ne peut se solder qu\'Ă ConScience restaurĂ©e. » <i>â version « Axiome aMi » pour le lexique</i><br>'
'<b>3 ·</b> « L\'Agresseur accumule une dette proportionnelle aux dĂ©gĂąts qu\'il inflige ; une dette que seule la restauration de la VĂ©ritĂ© (la ConScience) peut solder. » <i>â plus percutante, façon punchline</i><br>'
'<b>4 ·</b> « La Dette Karmique est la somme exacte des dĂ©gĂąts infligĂ©s sous couvert de dĂ©ni. Son seul mode de remboursement est la restauration de la ConScience. » <i>â Ă©quation complĂšte</i></div>\n')
h = h[:i] + emp.group(0) + '\n' + h[e:]; n += 1
anc = 'La ConScience est La Vie qui Se ReConnait & ProtĂšge.</b></div>\n'
assert h.count(anc) == 1, 'ancre huissier'
h = h.replace(anc, anc + dette); n += 1
assert h.count('id="pan-formule"') == 0 and h.count('data-p="formule"') == 0
# ââ 3. couleurs ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
TZ = '--tc:var(--tanzanite);--tcl:var(--tanzanite-ct);--tc-rgb:107,99,255'
rep('--tc:var(--amethyste);--tcl:var(--amethyste-ct);--tc-rgb:160,74,250;--tcx:var(--amethyste-ct)', TZ + ';--tcx:var(--tanzanite-ct)', 1, 'onglet croisement')
rep('#pan-croisement .sbar{--tc:var(--amethyste);--tcl:var(--amethyste-ct);--tc-rgb:160,74,250}', '#pan-croisement .sbar{' + TZ + '}', 1, 'sbar croisement')
rep('<div class="portail" style="--tc:var(--emeraude);--tcl:var(--emeraude);--tc-rgb:11,141,56">', '<div class="portail" style="' + TZ + '">', 1, 'portail croisement')
rep('<div class="portail" style="--tc:var(--amazonite);--tcl:var(--amazonite);--tc-rgb:0,159,157">', '<div class="portail" style="' + TZ + '">', 1, 'portail justifications')
rep('<div class="portail" style="--tc:var(--saphir);--tcl:var(--saphir);--tc-rgb:0,119,238">', '<div class="portail" style="--tc:var(--emeraude);--tcl:var(--emeraude-ct);--tc-rgb:11,141,56">', 1, 'portail métamatrice')
rep('<div class="portail" style="--tc:var(--rubis);--tcl:var(--rubis);--tc-rgb:227,50,50">', '<div class="portail" style="--tc:var(--emeraude);--tcl:var(--emeraude-ct);--tc-rgb:11,141,56">', 1, 'portail vibration')
# ââ 4. la colle + les tertiaires + le fond de Culture âââââââââââââââââââââââââââââââââââââ
CSS = '''
<style id="ami-retours-3">
/* 18 sept â retours de Diane : tertiaires collants ; le fond noir plat sous le portail Culture s'en va ;
fantÎme et barres fixées par le script « colle » quand l'hébergeur coupe le sticky (overflow:hidden). */
#i-contenu #ami-respect{background:transparent}
#i-contenu .bande,#i-contenu .tabs,#i-contenu .rc-tabs{position:sticky;top:var(--pile,0px);z-index:17;background:var(--onyx)}
.i-colle-ph{display:block;width:100%}
.i-colle-fixe{box-shadow:0 8px 20px rgba(0,0,0,.45);background:var(--onyx,#09090e)}
</style>
'''
JS = r'''
<script id="ami-colle-js">
(function(){"use strict";
var R=document.documentElement,C=document.getElementById("i-contenu");if(!C)return;
function coupe(el){for(var e=el.parentElement;e&&e!==document.body;e=e.parentElement){var o=getComputedStyle(e).overflowY;if(o==="hidden"||o==="clip"||o==="auto"||o==="scroll")return true}return false}
var TL=document.getElementById("i-tl");if(!TL||!coupe(TL))return; /* sticky natif : rien Ă faire */
R.classList.add("i-colle-js");
var SEL="#i-tl,.i-panneau .sbar,.i-panneau .bande,.i-panneau .tabs,.i-panneau .rc-tabs,.i-panneau .x-vues";
function hote(){var m=0,ls=document.querySelectorAll("header,nav,div");for(var i=0;i<ls.length&&i<400;i++){var el=ls[i];if(C.contains(el))continue;var s=getComputedStyle(el);if(s.position!=="fixed")continue;var r=el.getBoundingClientRect();if(r.top<=1&&r.height>24&&r.height<320&&r.width>innerWidth*.6)m=Math.max(m,r.bottom)}return m}
function cont(el){if(el===TL)return C;return el.closest(".svolet,#ami-respect,#ami-croisement,.i-panneau")||C}
var E=new Map();
/* un ancĂȘtre avec backdrop-filter / transform / filter devient le repĂšre d'un fixed : on le neutralise le temps du collage */
var PROPS=["backdrop-filter","-webkit-backdrop-filter","transform","filter","perspective"];
function libere(el,st){st.anc=[];for(var e=el.parentElement;e&&e!==document.body;e=e.parentElement){var s=getComputedStyle(e);if(s.backdropFilter!=="none"||s.webkitBackdropFilter!=="none"||s.transform!=="none"||s.filter!=="none"||s.perspective!=="none"){st.anc.push(e);PROPS.forEach(function(p){e.style.setProperty(p,"none","important")})}}}
function rend(st){(st.anc||[]).forEach(function(e){PROPS.forEach(function(p){e.style.removeProperty(p)})});st.anc=[]}
function defixe(el,st){if(!st.fixe)return;["position","top","left","width","z-index"].forEach(function(p){el.style.removeProperty(p)});el.classList.remove("i-colle-fixe");rend(st);if(st.ph&&st.ph.parentNode)st.ph.parentNode.removeChild(st.ph);st.fixe=false}
function maj(){
var top=hote(),z=60,bars=[].slice.call(document.querySelectorAll(SEL));
bars.forEach(function(el){
var st=E.get(el);if(!st){st={ph:null,fixe:false};E.set(el,st)}
var ref=st.fixe?st.ph:el;
if(ref.offsetParent===null||(!st.fixe&&el.getBoundingClientRect().height<8)){defixe(el,st);return}
var r=ref.getBoundingClientRect(),cr=cont(el).getBoundingClientRect(),hh=st.fixe?st.ph.offsetHeight:r.height;
if(r.top<=top&&cr.bottom>top+hh){
if(!st.fixe){st.ph=st.ph||document.createElement("div");st.ph.className="i-colle-ph";st.ph.style.height=r.height+"px";el.parentNode.insertBefore(st.ph,el);st.fixe=true;el.classList.add("i-colle-fixe");libere(el,st)}
var pr=st.ph.getBoundingClientRect();
el.style.setProperty("position","fixed","important");el.style.setProperty("top",top+"px","important");
el.style.setProperty("left",pr.left+"px","important");el.style.setProperty("width",pr.width+"px","important");
el.style.setProperty("z-index",String(z--),"important");
top+=el.offsetHeight;
}else defixe(el,st);
});
R.style.setProperty("--pile",top+"px");
}
addEventListener("scroll",maj,{passive:true});addEventListener("resize",maj);document.addEventListener("click",function(){setTimeout(maj,60);setTimeout(maj,400)});
setInterval(maj,700);maj();
})();
</script>
'''
rep('</body></html>', CSS + JS + '</body></html>', 1, 'fin de fichier')
open(OUT, 'w', encoding='utf-8').write(h)
print(f'â p13 : {n} gestes · {len(h.encode())/1e6:.2f} Mo â {OUT}')
# ââ 5. le Contrat (le toit) ouvert au chargement â dit par Diane le 18/09 (01h50) ââââââââââââ
h = open(OUT, encoding='utf-8').read(); n = 0
rep('id="tab-contrat" aria-controls="pan-contrat" aria-selected="false" tabindex="-1"', 'id="tab-contrat" aria-controls="pan-contrat" aria-selected="true" tabindex="0"', 1, 'tab contrat')
rep('id="tab-nature" aria-controls="pan-nature" aria-selected="true" tabindex="0"', 'id="tab-nature" aria-controls="pan-nature" aria-selected="false" tabindex="-1"', 1, 'tab nature')
rep('aria-labelledby="tab-contrat" tabindex="0" hidden>', 'aria-labelledby="tab-contrat" tabindex="0">', 1, 'pan contrat')
rep('aria-labelledby="tab-nature" tabindex="0">', 'aria-labelledby="tab-nature" tabindex="0" hidden>', 1, 'pan nature')
rep("o.dataset.p==='nature'", "o.dataset.p==='contrat'", 1, 'export : onglet premier')
rep("p.dataset.p!=='nature'", "p.dataset.p!=='contrat'", 1, 'export : volet premier')
rep(" majNavH();\n try { if((location.hash || '').slice(1) !== k) history.replaceState",
" majNavH();\n /* 18/09 â un volet qui n'Ă©tait pas ouvert au chargement a des toiles mesurĂ©es Ă 0 (les Quatre Constances) : on leur redonne une mesure */\n requestAnimationFrame(function(){ try { window.dispatchEvent(new Event('resize')); } catch(e){} });\n try { if((location.hash || '').slice(1) !== k) history.replaceState", 1, 'resize aprĂšs ouverture')
rep("length) dep = 'vibration';", "length) dep = 'contrat';", 1, 'volet par défaut au chargement')
open(OUT, 'w', encoding='utf-8').write(h)
print(f'â p13 (5) : {n} gestes â le toit ouvert au chargement')
p14_retours4_ini.py python3 p14_retours4_ini.py ini_p13_lisible.html ini_p14_lisible.html56 lignes
18/09 05h : portail Culture fusionnĂ©, intro dans âïž La Loi du RĂ©el, trois rangĂ©es collĂ©es, SEL colle unique, dĂ©tection comportementale du sticky mort.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P14 · L'Initiation â retours de Diane du 18 sept 04h, sur la source lisible (aprĂšs p13, avant p10).
1. đ Culture : un seul portail â « La Culture Respectueuse / l'Acquis · le Pourquoi · ce qui se transmet / La FiertĂ© Ă Honorer /
Vérité · Liberté · Coût du Non · Mémoire » ; la une du widget (« Le Respect Culturel ») fusionnée dedans.
2. L'intro gĂ©nĂ©rale (Si la Nature est le champ⊠/ â La Mitose / La mĂȘme planche existe en page autonomeâŠ) descend dans le volet
âïž La Loi du RĂ©el, aprĂšs son en-tĂȘte : aprĂšs le portail viennent directement les volets et leurs sous-onglets.
3. Trois rangĂ©es collĂ©es l'une sous l'autre : onglets · volets (.pbar du widget) · sous-onglets (.sbar) â et une dĂ©tection
COMPORTEMENTALE : si, une fois la barre dépassée, elle a défilé avec le contenu (sticky mort, quelle qu'en soit la cause :
overflow d'un conteneur, aperçu dans un cadreâŠ), le script « colle » prend la main."""
import re, sys
SRC = sys.argv[1]; OUT = sys.argv[2]
h = open(SRC, encoding='utf-8').read(); n = 0
def rep(old, new, k=1, label=''):
global h, n
c = h.count(old); assert c == k, f'â {label or old[:60]!r} : {c} occurrence(s), {k} attendue(s)'
h = h.replace(old, new); n += 1
# ââ 1. le portail unique âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
old_por = ('<div class="portail" style="--tc:var(--rubis);--tcl:var(--rubis-ct);--tc-rgb:227,50,50"><span class="po-em">đ</span><p class="po-t">Culture</p><p class="po-s">lâAcquis · le Pourquoi · ce qui se transmet</p></div>'
'<div class="une cu-une"><h1>Le Respect Culturel</h1><p class="sub">La Fierté à Honorer<br><span class="q1">Vérité</span> · <span class="q2">Liberté</span> · <span class="q3">Coût du Non</span> · <span class="q4">Mémoire</span></p></div>')
new_por = ('<div class="portail" style="--tc:var(--rubis);--tcl:var(--rubis-ct);--tc-rgb:227,50,50"><span class="po-em">đ</span><p class="po-t">La Culture Respectueuse</p>'
'<p class="po-s">lâAcquis · le Pourquoi · ce qui se transmet<br><span class="po-s2">La FiertĂ© Ă Honorer</span></p>'
'<p class="po-q"><span class="q1">Vérité</span> · <span class="q2">Liberté</span> · <span class="q3">Coût du Non</span> · <span class="q4">Mémoire</span></p></div>')
rep(old_por, new_por, 1, 'portail Culture fusionné')
# ââ 2. l'intro descend dans âïž La Loi du RĂ©el ââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<div class="cu-wrap">.*?</div>(?=\s*<div class="pwrap">)', h, re.S); assert m, 'cu-wrap'
intro = m.group(0).replace('au-dessus des quatre onglets', 'au-dessus des trois onglets')
h = h[:m.start()] + h[m.end():]; n += 1
anc = "<em>L'article complet, Les Lois de la Vie, en donne la démonstration pas à pas.</em></p></div>"
assert h.count(anc) == 1, 'ancre en-tĂȘte Loi du RĂ©el'
h = h.replace(anc, anc + intro); n += 1
# ââ 3. la colle : trois rangĂ©es, et la dĂ©tection comportementale âââââââââââââââââââââââââ
rep('#i-contenu .bande,#i-contenu .tabs,#i-contenu .rc-tabs{position:sticky;top:var(--pile,0px);z-index:17;background:var(--onyx)}',
'#i-contenu .rc-tabs{position:sticky;top:var(--pile,0px);z-index:17;background:var(--onyx)}\n'
'#i-contenu #ami-respect .pvolet .sbar{top:var(--pile,0px)!important}\n'
'#i-contenu .portail .po-q{margin:.5rem 0 0;font-family:\'Cormorant Garamond\',serif;font-style:italic;font-size:1.05rem}\n'
'#i-contenu .portail .po-q .q1{color:var(--amazonite-ct)}#i-contenu .portail .po-q .q2{color:var(--sugilite-ct)}#i-contenu .portail .po-q .q3{color:var(--tanzanite-ct)}#i-contenu .portail .po-q .q4{color:var(--emeraude-ct)}',
1, 'css tertiaires')
rep('var SEL="#i-tl,.i-panneau .sbar,.i-panneau .bande,.i-panneau .tabs,.i-panneau .rc-tabs,.i-panneau .x-vues";',
'var SEL="#i-tl,.i-panneau .sbar,.i-panneau .pbar,.i-panneau .rc-tabs,.i-panneau .x-vues";', 1, 'SEL colle')
old_dem = ' var TL=document.getElementById("i-tl");if(!TL||!coupe(TL))return; /* sticky natif : rien Ă faire */\n R.classList.add("i-colle-js");'
new_dem = ''' var TL=document.getElementById("i-tl");if(!TL)return;
var actif=false;
function demarre(){if(actif)return;actif=true;R.classList.add("i-colle-js");document.addEventListener("scroll",maj,{passive:true,capture:true});addEventListener("resize",maj);document.addEventListener("click",function(){setTimeout(maj,60);setTimeout(maj,400)});setInterval(maj,700);maj()}
/* détection comportementale : la barre dépassée a-t-elle collé ? sinon le script prend la main (cause indifférente) */
function verif(){if(actif)return;var r=TL.getBoundingClientRect();var att=parseFloat(getComputedStyle(TL).top)||0;if(r.top<att-6&&r.top<0)demarre()}
if(coupe(TL))demarre();else document.addEventListener("scroll",verif,{passive:true,capture:true});'''
rep(old_dem, new_dem, 1, 'démarrage colle')
rep(''' addEventListener("scroll",maj,{passive:true});addEventListener("resize",maj);document.addEventListener("click",function(){setTimeout(maj,60);setTimeout(maj,400)});
setInterval(maj,700);maj();
})();''', '})();', 1, 'ancien démarrage')
open(OUT, 'w', encoding='utf-8').write(h)
print(f'â p14 : {n} gestes · {len(h.encode())/1e6:.2f} Mo â {OUT}')
p15_cadre_webnode.py python3 p15_cadre_webnode.py Page_Vn.html Page_Vn_WEBNODE.html50 lignes
LA VOIE WEBNODE : la page entiĂšre dans un iframe srcdoc (Ă©chappĂ©e), le Pont injectĂ©, dimensionneur de hauteur (se tait en plein Ă©cran / repli). DĂ©rivĂ© Ă rĂ©gĂ©nĂ©rer Ă chaque version â rĂ©glages cosmo, decr (FIX + #ami-zoom), ego (19/09) â rĂ©glage 'et' pour 5.3 Astro · Espace-Temps (20/09 : #et-toc-btn et #et-toc-panel dans FIX).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P15 · LA VOIE DE PUBLICATION WEBNODE â la page entiĂšre dans un cadre (iframe srcdoc) : la seule forme que Webnode accepte
pour une page Ă gros scripts (TEST S, 18/09/2026). Le cadre suit la hauteur de son contenu â c'est la page du site qui dĂ©file,
naturellement, en-tĂȘte et pied du site compris â et LE PONT (pont_cadre.js, injectĂ© dans le cadre) rebranche sur la fenĂȘtre
du site tout ce qui comptait sur un dĂ©filement propre : barres collĂ©es (3 rangĂ©es), coins đ â° â au bas de l'Ă©cran, barre de
progression, bouton â, scrollTo, plein Ă©cran autorisĂ© (allowfullscreen : lĂ , le cadre a sa fenĂȘtre et le pont se tait).
Usage : python3 p15_cadre_webnode.py source.html sortie.html (pont_cadre.js à cÎté du script)"""
import sys, os
SRC, OUT = sys.argv[1], sys.argv[2]
PAGE = sys.argv[3] if len(sys.argv) > 3 else 'initiation' # initiation | psy | union | apo | astro (2.2 AstroGlobe) | et (5.3 Espace-Temps) | cosmo | decr | ego | eco | man
REGLAGES = { # 18/09 18h30 â le pont se rĂšgle par page (voir pont_cadre.js : window.AMI_PONT)
'initiation': dict(titre="L'Initiation aMi", cadre='ami-ini', pont=None),
'psy': dict(titre='0.2 PsychaTrice aMi', cadre='ami-psy-cadre', pont="{SEL:'.ami-barre,.pt-bar,.sbar,.pt-pane .sbar',FIX:'.ami-coin,.coin,.ami-top,.ami-toc,#ami-rech,#prog',HAUT:'.ami-top',FAUX:'ami-faux-fs'}"),
'union': dict(titre="0.3 L'Union du TouT aMi", cadre='ami-union-cadre', pont="{SEL:'.ami-barre,.tab-bar-full,.sbar,.tab-content .sbar',FIX:'.ami-coin,.coin,.ami-top,.toc-btn,.toc-panel,#ami-rech,#prog',HAUT:'.ami-top',FAUX:'ami-faux-fs'}"),
'apo': dict(titre='0.1 Apokalypse aMi', cadre='ami-apo-cadre', pont="{SEL:'.ami-barre,.apk-mainbar,.apk-subhost',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-glossary-toggle,#ami-glossary-panel,#ami-rech,#prog',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'astro': dict(titre='2.2 AstroGlobe aMi', cadre='ami-astro-cadre', pont="{SEL:'.ami-barre,.tab-nav',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'cosmo': dict(titre='3.3 CosmoLogie aMi', cadre='ami-cosmo-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini,#ami-zoom',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'decr': dict(titre='4.1 Décryptage aMi', cadre='ami-decr-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini,#ami-zoom',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'et': dict(titre='5.3 Astro · Espace-Temps aMi', cadre='ami-et-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini,#ami-zoom,#et-toc-btn,#et-toc-panel',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'eco': dict(titre='2.1 ECO · Bonus Plaisir aMi', cadre='ami-eco-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'man': dict(titre="đč Manuel de l'Accordage aMi", cadre='ami-man-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'legs': dict(titre='5.2 Legs · Corpus Re-Source aMi', cadre='ami-legs-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'renc': dict(titre='3.1 Rencontre · Tu Sais Apprivoiser aMi', cadre='ami-renc-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
'ego': dict(titre='1.1 EGO aMi', cadre='ami-ego-cadre', pont="{SEL:'.ami-barre,#amiPile',FIX:'.ami-coin,.coin,#ami-toc-toggle,#ami-toc-panel,#ami-rech,#prog,#ami-pile-mini',HAUT:'#amiHaut',FAUX:'ami-faux-fs'}"),
}[PAGE]
h = open(SRC, encoding='utf-8').read()
pont = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pont_cadre.js'), encoding='utf-8').read()
assert h.count('</body>') == 1
CSS = ('<style id="ami-pont-cadre-css">'
'/* le cadre suit sa hauteur : les voiles plein Ă©cran (recherche) se posent sur la fenĂȘtre visible, connue du pont */'
'html.i-cadre-fil #ami-rech{position:absolute!important;top:var(--vue-top,0px)!important;height:var(--vue-h,100vh)!important;bottom:auto!important}'
'html.i-cadre-fil #ami-rech .rc-b{top:1rem;max-height:calc(100% - 2rem)}'
'</style>')
reglage = ('<script id="ami-pont-reglage">window.AMI_PONT=' + REGLAGES['pont'] + ';</script>\n') if REGLAGES['pont'] else ''
h = h.replace('</body>', CSS + reglage + '<script id="ami-pont-cadre">\n' + pont + '\n</script>\n</body>')
esc = h.replace('&', '&').replace('"', '"')
CID = REGLAGES['cadre']; TITRE = REGLAGES['titre']
# 21/09 : le fichier se déclare en UTF-8 dÚs son premier octet. Ouvert tel quel (aperçu, Safari), un fichier sans
# dĂ©claration est lu en Latin-1 : tous les accents et les Ă©mojis deviennent « Ă© », « ðƞâ„ »⊠et le â€ïžâđ„ disparaĂźt.
# Dans Webnode la balise est sans effet (la page du site est déjà en UTF-8) : elle ne coûte rien.
out = ('<meta charset="utf-8">\n<!-- ' + TITRE + ' â version WEBNODE : la page entiĂšre dans un cadre qui suit sa hauteur, le pont rebranche le dĂ©filement (p15_cadre_webnode.py) -->\n'
'<div id="' + CID + '-hote" style="width:100%;margin:0;padding:0;line-height:0">\n'
'<iframe id="' + CID + '" title="' + TITRE.replace('"','') + '" allowfullscreen allow="fullscreen" scrolling="no" style="display:block;width:100%;height:100vh;border:0;overflow:hidden;background:#09090e" srcdoc="' + esc + '"></iframe>\n'
'</div>\n'
'<script>(function(){var f=document.getElementById("' + CID + '");if(!f)return;document.documentElement.setAttribute("data-ami-cadre","fil");'
'function h(){try{var d=f.contentDocument;if(!d||!d.documentElement)return;if(d.fullscreenElement||(d.body&&d.body.classList.contains("i-faux-fs")||d.querySelector(".ami-faux-fs")))return;var n=d.documentElement.scrollHeight;if(n>200&&Math.abs(n-f.offsetHeight)>4)f.style.height=n+"px"}catch(e){}}'
'f.addEventListener("load",function(){h();setTimeout(h,800)});setInterval(h,600);addEventListener("resize",h)})();</script>\n')
open(OUT, 'w', encoding='utf-8').write(out)
print(f'â cadre : {len(out.encode())/1e6:.2f} Mo â {OUT}')
pont_cadre.js (injecté dans le srcdoc par p15_cadre_webnode.py)126 lignes
Le Pont : dans un cadre qui suit sa hauteur, rebranche sur la fenĂȘtre du site les barres collĂ©es (position:absolute + fantĂŽme + --pile), les coins fixĂ©s (ancrage lu dans la feuille), la progression, le â, scrollTo, le plein Ă©cran vrai et le repli (le cadre prend l'Ă©cran du site).
â§ Copier le script
/* ââ LE PONT DU CADRE â 18 sept 2026 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
La page vit dans un iframe srcdoc Ă la hauteur de son contenu (la seule forme que Webnode accepte, TEST S) :
rien ne défile DEDANS, c'est la page du site qui défile. Tout ce qui comptait sur un défilement propre
(barres collĂ©es, coins đ â° â fixĂ©s Ă l'Ă©cran, barre de progression, bouton â, scrollTo) est rebranchĂ© sur
la fenĂȘtre du parent â mĂȘme origine, donc lisible. En plein Ă©cran, le cadre a sa propre fenĂȘtre : le pont se tait. */
(function(){"use strict";
var F = window.frameElement; if(!F) return;
/* 18/09 18h30 â le pont se rĂšgle par page : window.AMI_PONT = {SEL: barres collĂ©es, FIX: Ă©lĂ©ments fixĂ©s, HAUT: le bouton â, FAUX: classe du repli} ; sans rĂ©glage, l'Initiation */
var CFG = window.AMI_PONT || {};
var CFG_SEL = CFG.SEL || '.ami-barre,#i-tl,.i-panneau .sbar,.i-panneau .pbar,.i-panneau #ami-consent .tabwrap,.i-panneau .rc-tabs,.i-panneau .x-vues';
var CFG_FIX = CFG.FIX || '.ami-coin, .i-coin, .coin, #i-rech, #prog, #i-haut-b, #i-toc, .i-sommaire, .ami-boite, .life-topzone, [data-cadre-fixe]';
var CFG_HAUT = CFG.HAUT || '#i-haut-b', CFG_FAUX = CFG.FAUX || 'i-faux-fs';
var P; try { P = window.parent; if(!P || !P.document || !P.document.body) return; } catch(e){ return; }
var R = document.documentElement, B = document.body;
/* le cadre suit-il la hauteur du contenu ? sinon (cadre à hauteur d'écran, qui défile dedans) : rien à faire */
function suit(){ return Math.abs(F.getBoundingClientRect().height - R.scrollHeight) < 40 || innerHeight >= R.scrollHeight - 2; }
R.classList.add('i-cadre-fil');
/* 18/09 18h40 â les scripts de la page lisent window.pageYOffset (la barre qui se replie, le â, les sommaires) : dans un cadre qui ne dĂ©file pas,
ils liraient 0 pour toujours. On leur donne le haut de la fenĂȘtre visible, en coordonnĂ©es du cadre. */
try{
var _pyo = Object.getOwnPropertyDescriptor(window,'pageYOffset'), _sy = Object.getOwnPropertyDescriptor(window,'scrollY');
function yo(){ if(plein||faux||!suit()) return (_pyo&&_pyo.get)?_pyo.get.call(window):R.scrollTop; return vue().top; }
Object.defineProperty(window,'pageYOffset',{configurable:true,get:yo}); Object.defineProperty(window,'scrollY',{configurable:true,get:yo});
}catch(e){}
/* 19/09 13h30 â les scripts de la page lisent aussi getBoundingClientRect().top comme « distance au haut de la fenĂȘtre » (sommaires, đ, pile qui se replie,
renvois) : dans un cadre qui ne dĂ©file pas, top est une coordonnĂ©e de document â rect.top + pageYOffset comptait deux fois le dĂ©filement
(les sauts du â° et du đ tombaient 2 000 px trop bas). On leur rend la fenĂȘtre visible : top/bottom mesurĂ©s depuis son haut. Le pont, lui, lit les rects bruts. */
var _gbcr = Element.prototype.getBoundingClientRect, VT = 0;
function raw(el){ return _gbcr.call(el); }
try{ Element.prototype.getBoundingClientRect = function(){ var r=_gbcr.call(this); if(plein||faux||!VT||!suit()) return r;
return {top:r.top-VT, bottom:r.bottom-VT, y:r.y-VT, left:r.left, right:r.right, x:r.x, width:r.width, height:r.height, toJSON:function(){ return {top:this.top,bottom:this.bottom,left:this.left,right:this.right,width:this.width,height:this.height,x:this.x,y:this.y}; }}; }; }catch(e){}
function hote(){ var m=0, ls=P.document.querySelectorAll('header,nav,div'); for(var i=0;i<ls.length&&i<400;i++){ var el=ls[i]; if(el===F||el.contains(F)) continue; var s=P.getComputedStyle(el); if(s.position!=='fixed'&&s.position!=='sticky') continue; var r=raw(el); if(r.top<=1&&r.height>24&&r.height<320&&r.width>P.innerWidth*.6) m=Math.max(m,r.bottom); } return m; }
/* la fenĂȘtre visible, en coordonnĂ©es du document du cadre (qui ne dĂ©file pas : doc = viewport du cadre) */
function vue(){ var ft=F.getBoundingClientRect().top; var h=hote(); var top=Math.max(0, h-ft); var bas=Math.min(F.offsetHeight, P.innerHeight-ft); return {top:top, bas:bas, h:Math.max(0, bas-top), ft:ft, hote:h}; }
var plein = false, faux = false, memo = null;
document.addEventListener('fullscreenchange', function(){ plein = !!document.fullscreenElement; if(plein){ tout_rendre(); F.setAttribute('scrolling','yes'); F.style.setProperty('overflow','auto','important'); R.style.overflowY='auto'; } else { F.setAttribute('scrolling','no'); F.style.setProperty('overflow','hidden','important'); R.style.overflowY=''; } tick(); });
/* le faux plein écran (API refusée) : le CADRE prend tout l'écran du site, la page défile dedans, le pont se tait */
function fauxPlein(on){
if(on===faux) return; faux=on;
try{
if(on){ memo={f:F.getAttribute('style')||'', b:P.document.body.style.overflow||''}; tout_rendre();
F.style.setProperty('position','fixed','important'); F.style.setProperty('inset','0','important'); F.style.setProperty('top','0','important'); F.style.setProperty('left','0','important');
F.style.setProperty('width','100%','important'); F.style.setProperty('height','100%','important'); F.style.setProperty('z-index','2147483000','important'); F.style.setProperty('overflow','auto','important');
F.setAttribute('scrolling','yes'); P.document.body.style.overflow='hidden'; R.style.overflowY='auto'; window.scrollTo(0,0); }
else { F.setAttribute('style', memo?memo.f:''); F.setAttribute('scrolling','no'); P.document.body.style.overflow = memo?memo.b:''; R.style.overflowY=''; memo=null; tick(); }
}catch(e){}
}
function estFaux(){ return B.classList.contains(CFG_FAUX) || !!document.querySelector('.'+CFG_FAUX); }
new MutationObserver(function(){ fauxPlein(estFaux()); }).observe(B, {attributes:true, attributeFilter:['class'], subtree:true});
document.addEventListener('keydown', function(e){ if(e.key==='Escape' && faux){ var b=document.getElementById('amiPlein'); if(b) b.click(); } });
/* ââ 1. ce qui Ă©tait fixĂ© Ă l'Ă©cran ââ */
var FIX = []; /* {el, top, bottom, left, right, ph?} */
function scanFixes(){
var ls = document.querySelectorAll(CFG_FIX);
for(var i=0;i<ls.length;i++){ var el=ls[i]; if(FIX.some(function(x){return x.el===el})) continue;
var s=getComputedStyle(el); if(s.position!=='fixed') continue;
/* ancré en bas ou en haut ? la feuille le dit (top:auto + bottom posé = ancré en bas) ; sinon sa place réelle */
var bas, off, etire=false;
if(s.top!=='auto' && s.bottom!=='auto' && s.display==='none'){ etire=true; bas=false; off=0; }
else if(s.top==='auto' && s.bottom!=='auto'){ bas=true; off=parseFloat(s.bottom)||0; }
else if(s.display==='none'){ bas=false; off=parseFloat(s.top)||0; }
else { var r=raw(el), H=innerHeight; var duBas=H-r.bottom, duHaut=r.top; bas = duBas<duHaut; off = Math.round(bas?duBas:duHaut); }
FIX.push({el:el, bas:bas, off:off, etire:etire}); }
}
function poseFixe(x, v){
var el=x.el, s=getComputedStyle(el); if(s.display==='none') return;
var op = el.offsetParent || B; var oy = op===B ? 0 : raw(op).top;
var h = el.offsetHeight; var y = x.bas ? (v.bas - h - x.off) : (v.top + x.off);
/* 19/09 : data-ami-sous-pile â se pose juste sous les barres collĂ©es, quelle que soit leur hauteur du moment (la ligne « oĂč je suis ») */
if(el.hasAttribute('data-ami-sous-pile')) y = v.top + (parseFloat(getComputedStyle(R).getPropertyValue('--pile'))||0);
y = Math.max(0, Math.min(y, F.offsetHeight - h));
el.style.setProperty('position','absolute','important');
if(x.etire){ el.style.setProperty('top', Math.round(v.top - oy)+'px','important'); el.style.setProperty('height', Math.round(v.h)+'px','important'); el.style.setProperty('bottom','auto','important'); return; }
el.style.setProperty('top', Math.round(y - oy)+'px','important');
el.style.setProperty('bottom','auto','important');
}
/* ââ 2. les barres collĂ©es ââ */
var SEL = CFG_SEL;
var E = new Map();
function cont(el){ var C=document.getElementById('i-contenu')||B; if(el.id==='i-tl'||el.classList.contains('ami-barre')||el.matches('.pt-bar,.tab-bar-full,.tab-nav,.apk-mainbar')) return B; return el.closest('.svolet,#ami-respect,#ami-croisement,.i-panneau,.pt-pane,.tab-content,.tab-panel,.apk-pane')||C; }
function defixe(el, st){ if(!st.fixe) return; ['position','top','left','width','z-index'].forEach(function(p){ el.style.removeProperty(p); }); el.classList.remove('i-colle-fixe'); if(st.ph&&st.ph.parentNode) st.ph.parentNode.removeChild(st.ph); st.fixe=false; }
function colle(v){
var top = v.top, z = 60, bars = [].slice.call(document.querySelectorAll(SEL));
bars.forEach(function(el){
var st=E.get(el); if(!st){ st={ph:null,fixe:false}; E.set(el,st); }
var ref = st.fixe ? st.ph : el;
/* 19/09 : une barre qui se dĂ©clare repliĂ©e (data-ami-decolle â la pile qui se replie) reprend sa place dans le flux */
if(ref.offsetParent===null || el.hasAttribute('data-ami-decolle') || (!st.fixe && raw(el).height<8)){ defixe(el,st); return; }
var r=raw(ref), cr=raw(cont(el)), hh=st.fixe?st.ph.offsetHeight:r.height;
if(r.top<=top && cr.bottom>top+hh){
if(!st.fixe){ st.ph=st.ph||document.createElement('div'); st.ph.className='i-colle-ph'; st.ph.style.height=r.height+'px'; el.parentNode.insertBefore(st.ph,el); st.fixe=true; el.classList.add('i-colle-fixe'); }
var pr=raw(st.ph); var op=el.offsetParent||B; var opr = op===B ? {top:0,left:0} : raw(op);
el.style.setProperty('position','absolute','important'); el.style.setProperty('top',Math.round(top-opr.top)+'px','important');
el.style.setProperty('left',Math.round(pr.left-opr.left)+'px','important'); el.style.setProperty('width',pr.width+'px','important');
el.style.setProperty('z-index',String(z--),'important');
top += el.offsetHeight;
} else defixe(el,st);
});
R.style.setProperty('--pile', top - v.top + 'px');
}
function tout_rendre(){ E.forEach(function(st,el){ defixe(el,st); }); FIX.forEach(function(x){ ['position','top','bottom','height','opacity','pointer-events'].forEach(function(p){ x.el.style.removeProperty(p); }); }); }
/* ââ 3. progression et â ââ */
function progression(v){
var prog=document.getElementById('prog'); var total=F.offsetHeight - v.h; var p = total>0 ? v.top/total : 0;
if(prog) prog.style.width = (Math.max(0,Math.min(1,p))*100).toFixed(2)+'%';
var haut=document.querySelector(CFG_HAUT); if(haut){ var vu = v.top > 420; haut.classList.toggle('vu', vu); haut.classList.toggle('on', vu); if(haut.hidden!==undefined && haut.hasAttribute('hidden')===vu) { if(vu) haut.removeAttribute('hidden'); else haut.setAttribute('hidden',''); } haut.style.setProperty('opacity', vu?'1':'0', 'important'); haut.style.setProperty('pointer-events', vu?'auto':'none', 'important'); }
}
/* ââ 4. scrollTo : vers la page du site ââ */
var _st = window.scrollTo.bind(window);
window.scrollTo = function(a,b){ if(plein||faux||!suit()) return _st.apply(window, arguments); var y = (typeof a==='object' && a) ? (a.top||0) : (b||0); var ft = F.getBoundingClientRect().top + P.scrollY; P.scrollTo({top: ft + y - hote(), behavior: (a&&a.behavior)||'auto'}); };
window.scrollBy = function(a,b){ var y=(typeof a==='object'&&a)?(a.top||0):(b||0); P.scrollBy(0,y); };
R.scrollTo = window.scrollTo;
/* ââ la boucle ââ */
var attente=false;
function tick(){ if(attente) return; attente=true; requestAnimationFrame(function(){ attente=false;
if(plein || faux || !suit()){ return; }
/* les scripts de la page écoutent « scroll » : on le leur redonne, puis on corrige ce qu'ils ne peuvent pas savoir */
try { window.dispatchEvent(new Event('scroll')); } catch(e){}
var v=vue(); VT=v.top; R.style.setProperty('--vue-top', v.top+'px'); R.style.setProperty('--vue-h', v.h+'px'); colle(v); scanFixes(); FIX.forEach(function(x){ poseFixe(x,v); }); progression(v);
}); }
P.addEventListener('scroll', tick, {passive:true}); P.addEventListener('resize', tick);
document.addEventListener('click', function(){ setTimeout(tick,60); setTimeout(tick,400); });
setInterval(tick, 700); tick(); setTimeout(tick, 800); setTimeout(tick, 2500);
})();
p16_retours5_ini.py python3 p16_retours5_ini.py ini_p14_lisible.html ini_p16_lisible.html25 lignes
18/09 07h : đ§ remplie quand active ; le repli de plein Ă©cran garde â¶/â + zoom visibles.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P16 · L'Initiation â retours de Diane du 18/09 07h (sur la source lisible, aprĂšs p14, avant p10).
1. đ§ Lecture continue : quand elle est active, la goutte est REMPLIE et porte un halo rouge autour (dit par Diane).
2. Le faux plein Ă©cran (quand l'API est refusĂ©e, ex. dans un cadre sans allowfullscreen) cachait TOUTE la barre â donc le
zoom et le bouton â pour en sortir. DĂ©sormais la barre ne garde que â¶/â et le zoom, fixĂ©s en haut Ă droite."""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read()
CSS = '''
<style id="ami-retours-5">
/* 18 sept â đ§ lecture continue active : goutte remplie + halo rouge ; faux plein Ă©cran : â¶/â et le zoom restent visibles */
.ami-barre #amiLecture[aria-pressed="true"]{color:var(--rubis-ct);border-color:var(--rubis);
box-shadow:0 0 0 3px rgba(227,50,50,.28),0 0 16px 4px rgba(227,50,50,.65)}
.ami-barre #amiLecture[aria-pressed="true"] svg{fill:var(--rubis);stroke:var(--rubis-ct)}
body.i-faux-fs .ami-barre{display:flex!important;position:fixed;top:.4rem;right:.5rem;left:auto;bottom:auto;width:auto;height:auto;
padding:0;margin:0;background:none;border:0;box-shadow:none;gap:.4rem;align-items:center;z-index:80}
body.i-faux-fs .ami-barre > *:not(.coin-zoom):not(#amiPlein){display:none!important}
body.i-faux-fs .ami-barre .coin-zoom,body.i-faux-fs .ami-barre #amiPlein{position:static;display:flex}
</style>
'''
assert h.count('</body></html>') == 1
h = h.replace('</body></html>', CSS + '</body></html>')
open(OUT, 'w', encoding='utf-8').write(h)
print('â p16 : 2 gestes')
p17_retours6_ini.py python3 p17_retours6_ini.py ini_p16_lisible.html ini_p17_lisible.html40 lignes
18/09 08h-09h : la rĂšgle des boutons d'accessibilitĂ© (places, formes pleines, sans halo) ; sous-onglets du đȘ· RepĂšre collĂ©s (SEL + CSS forcĂ©e).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P17 · L'Initiation â retours de Diane du 18/09 08h10 (source lisible, aprĂšs p16, avant p10) : les boutons d'accessibilitĂ©.
1. đ§ lecture continue active = goutte REMPLIE de sa couleur, sans halo (le halo rouge faisait « alerte »). â contraste : DEMI-cercle
plein au repos (le pictogramme du contraste), cercle PLEIN quand le contraste est actif (mĂȘme retour UX que la goutte).
(09h : Diane â « je voulais le demi cercle plein et le cercle plein » ; le cercle vide de 08h Ă©tait une erreur.)
3. 09h â les sous-onglets du đȘ· RepĂšre du Consentement (#ami-consent .tabwrap, 5 vues) collent sous la pile comme les .sbar
des autres volets : la feuille du widget les rendait « static » sous 700 px ; forcé, et ajouté au SEL des scripts colle.
2. Places : en haut Ă gauche â contraste + đ§ lecture ; en haut Ă droite â¶ plein Ă©cran + zoom â/+ (plein Ă©cran) ;
en bas Ă droite, de haut en bas : â TOP · â° TOC · đ recherche."""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read()
demi = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 1 0 18Z" fill="currentColor" stroke="none"/></svg>'
assert h.count(demi) == 2, 'svg contraste â (barre + widget Respect) : le demi-cercle reste'
old_sel = 'var SEL="#i-tl,.i-panneau .sbar,.i-panneau .pbar,.i-panneau .rc-tabs,.i-panneau .x-vues";'
assert h.count(old_sel) == 1, 'SEL colle'
h = h.replace(old_sel, 'var SEL="#i-tl,.i-panneau .sbar,.i-panneau .pbar,.i-panneau #ami-consent .tabwrap,.i-panneau .rc-tabs,.i-panneau .x-vues";')
a = h.find('<style id="ami-retours-5">'); b = h.find('</style>', a) + len('</style>'); assert a > 0
CSS = '''<style id="ami-retours-5">
/* 18 sept â boutons d'accessibilitĂ© (retours de Diane) : formes pleines quand c'est actif, sans halo ;
â Ă gauche avec đ§ ; â¶ + zoom Ă droite ; en bas Ă droite â â° đ ; faux plein Ă©cran : â¶/â et zoom restent visibles */
.ami-barre #amiLecture[aria-pressed="true"] svg{fill:currentColor}
.ami-barre #amiContraste[aria-pressed="true"] svg circle,#ami-respect #rContraste[aria-pressed="true"] svg circle,#ami-respect .coin.actif svg circle{fill:currentColor}
.ami-barre .coin.d2#amiContraste{left:2.9rem;right:auto}
.ami-barre .coin.g#amiLecture{left:.5rem}
#i-haut-b{right:1rem!important;bottom:7rem!important}
.i-coin.d3#iSommaire{right:1rem!important;bottom:4rem!important}
.ami-coin.r#amiRech{right:1rem!important;bottom:1rem!important}
body.i-faux-fs .ami-barre{display:flex!important;position:fixed;top:.4rem;right:.5rem;left:auto;bottom:auto;width:auto;height:auto;
padding:0;margin:0;background:none;border:0;box-shadow:none;gap:.4rem;align-items:center;z-index:80}
body.i-faux-fs .ami-barre > *:not(.coin-zoom):not(#amiPlein):not(#amiLecture):not(#amiContraste){display:none!important}
body.i-faux-fs .ami-barre .coin-zoom,body.i-faux-fs .ami-barre #amiPlein{position:static;display:flex}
body.i-faux-fs .ami-barre #amiLecture,body.i-faux-fs .ami-barre #amiContraste{position:fixed;top:.4rem;display:flex}
body.i-faux-fs .ami-barre #amiLecture{left:.5rem}body.i-faux-fs .ami-barre #amiContraste{left:2.9rem}
#i-contenu #ami-respect #ami-consent .tabwrap{position:sticky!important;top:var(--pile,0px)!important;z-index:13;background:linear-gradient(180deg,rgba(var(--onyx-rgb),.97),rgba(var(--onyx-rgb),.92))!important}
</style>'''
h = h[:a] + CSS + h[b:]
open(OUT, 'w', encoding='utf-8').write(h)
print('â p17 : boutons d\'accessibilitĂ©')
verif_contraste.py python3 verif_contraste.py (ini.html à cÎté)11 lignes
Playwright : fill calculĂ© du cercle â et du demi-disque au repos puis aprĂšs le clic â â â â.
â§ Copier le script
import asyncio, os
from playwright.async_api import async_playwright
D=os.path.abspath('.')
async def main():
async with async_playwright() as p:
b=await p.chromium.launch(); pg=await b.new_page(viewport={'width':390,'height':800})
await pg.goto('file://'+D+'/ini.html'); await pg.wait_for_timeout(1000)
js='''()=>{const c=document.querySelector('#amiContraste svg circle'),p=document.querySelector('#amiContraste svg path');return {pressed:document.getElementById('amiContraste').getAttribute('aria-pressed'),circle:getComputedStyle(c).fill,demi:!!p&&getComputedStyle(p).fill}}'''
print('repos', await pg.evaluate(js)); await pg.click('#amiContraste'); await pg.wait_for_timeout(300); print('actif', await pg.evaluate(js)); await b.close()
asyncio.run(main())
verif_repere.py python3 verif_repere.py (ini.html + sim_cadre.html à cÎté)27 lignes
Playwright : les sous-onglets du RepÚre du Consentement collent exactement sous la rangée des volets, en page seule (390/1280) et dans le cadre simulé (colle du Pont).
â§ Copier le script
import asyncio, os
from playwright.async_api import async_playwright
D=os.path.abspath('.')
async def main():
async with async_playwright() as p:
b=await p.chromium.launch()
for w in (390,1280):
pg=await b.new_page(viewport={'width':w,'height':800})
await pg.goto('file://'+D+'/ini.html'); await pg.wait_for_timeout(1200)
await pg.click('#i-tl [data-k="culture"], #i-tl button:has-text("Culture")'); await pg.wait_for_timeout(500)
await pg.click('#pt-repere'); await pg.wait_for_timeout(700)
r=await pg.evaluate('''()=>{const t=document.querySelector('#ami-consent .tabwrap');t.scrollIntoView();window.scrollBy(0,900);return new Promise(r=>setTimeout(()=>{const b=t.getBoundingClientRect();const pile=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--pile'));const tl=document.getElementById('i-tl').getBoundingClientRect();const pb=document.querySelector('#ami-respect .pbar').getBoundingClientRect();r({top:Math.round(b.top),pile,tlBottom:Math.round(tl.bottom),pbarBottom:Math.round(pb.bottom),pos:getComputedStyle(t).position,vis:b.height>0})},500))}''')
print('seule',w,r)
await pg.close()
# cadre simulé
pg=await b.new_page(viewport={'width':390,'height':800})
await pg.goto('file://'+D+'/sim_cadre.html'); await pg.wait_for_timeout(1500)
fr=pg.frame(name='ami-ini') or [f for f in pg.frames if f!=pg.main_frame][0]
await fr.click('#i-tl [data-k="culture"], #i-tl button:has-text("Culture")'); await pg.wait_for_timeout(500)
await fr.click('#pt-repere'); await pg.wait_for_timeout(700)
await fr.evaluate('''()=>{document.querySelector('#ami-consent .tabwrap').scrollIntoView();window.scrollBy(0,900)}''')
await pg.wait_for_timeout(1200)
r=await fr.evaluate('''()=>{const t=document.querySelector('#ami-consent .tabwrap');const b=t.getBoundingClientRect();const F=window.frameElement.getBoundingClientRect();const tl=document.getElementById('i-tl').getBoundingClientRect();const pb=document.querySelector('#ami-respect .pbar').getBoundingClientRect();return {topEcran:Math.round(b.top+F.top),tlBottomEcran:Math.round(tl.bottom+F.top),pbarBottomEcran:Math.round(pb.bottom+F.top),fixe:t.classList.contains('i-colle-fixe'),pos:getComputedStyle(t).position}}''')
print('cadre 390',r)
await b.close()
asyncio.run(main())
p18_charte_webnode.py python3 p18_charte_webnode.py Charte_V6.html Charte_V7.html145 lignes
La Charte allĂ©gĂ©e V6 â V7 : § LII bis Publier sur Webnode (le Cadre et le Pont), § XXIX places et Ă©tats des boutons, volet âïž refermĂ©, entrĂ©es de sommaire.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P18 · La Charte de Communication (BLOC_13 allĂ©gĂ©e) V6 â V7, 18 sept 2026.
Deux rÚgles trouvées à l'usage (+ un volet non fermé, C) pendant le contrÎle qualité de l'Initiation (17-18 sept), rendues transmissibles :
A. dans « La Boßte à Outils de page » : les places et les états des boutons d'accessibilité (retour de Diane, 08h10) ;
B. une section nouvelle aprĂšs « Le protocole de hauteur » : publier sur Webnode â le Cadre (iframe srcdoc) et le Pont.
Usage : python3 p18_charte_webnode.py source_V6.html sortie_V7.html"""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); n = 0
def ins_after(anchor, block, label):
global h, n
i = h.find(anchor); assert i > 0, 'â ancre absente : ' + label
assert h.count(anchor) == 1, 'â ancre non unique : ' + label
i += len(anchor); h = h[:i] + block + h[i:]; n += 1
def ins_before(anchor, block, label):
global h, n
i = h.find(anchor); assert i > 0 and h.count(anchor) == 1, 'â ancre : ' + label
h = h[:i] + block + h[i:]; n += 1
# ââ A · les boutons d'accessibilitĂ© â aprĂšs le tableau V16 « Bouton · Position · RĂŽle » ââââââââââââââââ
i = h.find('Contraste élevé'); assert i > 0; j = h.find('</table></div>', i) + len('</table></div>')
A = '''
<h3 class="sub" id="s-boite-places-etats">â Les places et les Ă©tats â la rĂšgle du 18 septembre</h3>
<p>Le tableau V16 ci-dessus reste le point de dĂ©part ; <strong>ce qui suit le remplace lĂ oĂč ils diffĂšrent.</strong> TrouvĂ© sur lâInitiation, avec Diane qui regarde sur tĂ©lĂ©phone : <em>« rester simple et pas trop voyant »</em>. Trois familles, trois places â et une seule façon de dire « câest actif ».</p>
<div class="tw"><table>
<thead><tr><th>Place</th><th>Boutons</th><th>Ce quâils dĂ©cident</th><th>DĂ©tail</th></tr></thead>
<tbody>
<tr><td class="acc">En haut Ă gauche</td><td class="mono">â contraste · đ§ lecture continue</td><td>la <em>forme</em> de la lecture</td><td class="mono">left:.5rem · left:2.9rem, top:.4rem</td></tr>
<tr><td class="acc">En haut Ă droite</td><td class="mono">â¶ plein Ă©cran · â % + zoom</td><td>le <em>cadre</em> de la lecture</td><td>le zoom ne paraĂźt quâen plein Ă©cran (rĂšgle V16) ; â¶ devient â une fois dedans</td></tr>
<tr><td class="acc">En bas Ă droite</td><td class="mono">â haut · â° sommaire · đ recherche</td><td>oĂč lâon <em>va</em></td><td class="mono">de haut en bas â bottom:7rem · 4rem · 1rem, right:1rem</td></tr>
</tbody>
</table></div>
<div class="or-box"><strong>LâĂ©tat actif se dit par la forme, jamais par un halo.</strong> La forme intĂ©rieure du pictogramme <em>se remplit</em> de sa propre couleur : la goutte đ§ pleine quand la lecture continue est en marche, le cercle â â demi-plein au repos, câest le signe du contraste â devient un cercle plein â quand le contraste est actif. <em>Un halo rouge autour dâun bouton a Ă©tĂ© lu comme une alerte (« notification dangereuse ») : on lâa retirĂ©.</em> MĂȘme trait, mĂȘme opacitĂ© pour tous les coins ; le plein et le vide suffisent.</div>
<div class="tw"><table>
<thead><tr><th>Bouton</th><th>Au repos</th><th>Actif</th><th>Comment</th></tr></thead>
<tbody>
<tr><td class="acc">đ§ lecture continue</td><td>goutte en trait</td><td class="sain">goutte pleine</td><td class="mono">#amiLecture[aria-pressed="true"] svg{fill:currentColor}</td></tr>
<tr><td class="acc">â contraste</td><td>demi-cercle plein</td><td class="sain">cercle plein</td><td class="mono">#amiContraste[aria-pressed="true"] svg circle{fill:currentColor}</td></tr>
<tr><td class="acc">â¶ plein Ă©cran</td><td>â¶</td><td class="sain">â</td><td>et <code translate="no">Ăchap</code> sort des deux plein Ă©crans, le vrai et le repli</td></tr>
</tbody>
</table></div>
<div class="alerte"><strong>Le repli de plein Ă©cran ne cache pas la barre : il la rĂ©duit.</strong> Quand lâAPI est refusĂ©e (page servie dans un cadre), <code translate="no">body.i-faux-fs</code> garde visibles â¶/â et le zoom en haut Ă droite, â et đ§ en haut Ă gauche, et cache le reste de la barre â <em>avant, on entrait sans pouvoir ni zoomer ni sortir.</em> Et le plein Ă©cran, vrai ou repliĂ©, <strong>dĂ©file</strong> : câest <code translate="no">documentElement</code> quâon met en plein Ă©cran, pas la racine dâun widget (« je veux ma page immersive comme si câĂ©tait un jeu »). Un widget qui met sa propre racine en plein Ă©cran fige sa une et ne montre rien derriĂšre â dĂ©faut relevĂ© le 18 sept sur Apokalypse et lâUnion, Ă corriger avec le composant.</div>
<div class="note"><b>Le contrĂŽle se fait Ă la machine.</b> Un script Playwright lit, au repos puis aprĂšs le clic, le <code translate="no">fill</code> calculĂ© du cercle et de la goutte (<code translate="no">verif_contraste.py</code>), et vĂ©rifie lâordre des coins de bas en haut đ â° â (mutation « ordre des coins » de <code translate="no">p5_ini_mutations.py</code>). <em>Un bouton se mesure, il ne se regarde pas.</em></div>
'''
h = h[:j] + A + h[j:]; n += 1
# ââ B · publier sur Webnode â le Cadre et le Pont â avant « 36 · LA COULEUR D'UN WIDGET » âââââââââââââââ
B = '''<!-- ââ 35 bis · PUBLIER SUR WEBNODE â LE CADRE ET LE PONT (18 sept 2026) âââââââââââ -->
<div class="section" id="wg-cadre">
<div class="sec-header"><span class="sec-num">LII bis</span><span class="sec-em">đȘ</span>
<h2 id="s-publier-sur-webnode-le-cadre-et-le-pont">Publier sur Webnode â le Cadre et le Pont</h2></div>
<p>Une page se publie en collant <strong>le fichier entier</strong> dans un bloc « Code HTML » de Webnode. Apokalypse (3 Mo) passe ainsi, en ligne, sans cadre. <strong>LâInitiation, elle, ne sâest jamais affichĂ©e</strong> â V18, V19, fragments, moitiĂ©s : bloc vide en version publiĂ©e, sans un message. Dix-neuf essais sur deux nuits (17-18 sept, TEST A â S) ont Ă©tabli ce que lâhĂ©bergeur fait, et ce quâil ne fait pas. <em>On Ă©crit ici le rĂ©sultat, pas les hypothĂšses : chacune a Ă©tĂ© rĂ©futĂ©e par une mesure.</em></p>
<div class="tw"><table>
<thead><tr><th>Ce quâon croyait</th><th>Ce qui a Ă©tĂ© mesurĂ©</th><th>Verdict</th></tr></thead>
<tbody>
<tr><td class="acc">Un plafond dâoctets</td><td>236 pages du sitemap lues : <span translate="no">ami-astroglobe</span> 3,1 Mo en ligne (un script de 1,8 Mo), Apokalypse 3,0 Mo â et un bloc de 30 Ko refusĂ© (TEST P5)</td><td class="alert">non</td></tr>
<tr><td class="acc">Une ligne trop longue</td><td>PsychaTrice V20 passe avec une ligne de 808 000 signes ; P5 refusĂ© nâa que des lignes de 800</td><td class="alert">non</td></tr>
<tr><td class="acc">Un filtre de mots (API JS)</td><td>TEST K : les 27 jetons que lâInitiation est seule Ă contenir Ă©crits en deux morceaux â refusĂ©</td><td class="alert">non</td></tr>
<tr><td class="acc">LâexĂ©cution des scripts</td><td>TEST L : scripts rendus inertes (<code translate="no">type=text/plain</code>) â refusĂ© : câest le <em>texte</em> cĂŽtĂ© serveur qui est jugĂ©</td><td class="alert">non</td></tr>
<tr><td class="acc">Un trait ES6</td><td>N2 accepté a 326 flÚches, <code translate="no">const</code>, gabarits, <code translate="no">async</code></td><td class="alert">non</td></tr>
<tr><td class="acc">Le refus est silencieux</td><td>le bloc garde son contenu prĂ©cĂ©dent ; deux blocs page + scripts sur la mĂȘme page â <strong>Error 500</strong> ; <code translate="no"><head></code>/<code translate="no"><body></code> ĂŽtĂ©s ; page + styles seuls passent, les scripts Ă©chouent en combinaison</td><td class="sain">mesurĂ©</td></tr>
</tbody>
</table></div>
<div class="or-box"><strong>La cause nâa pas Ă©tĂ© identifiĂ©e, et la rĂšgle nâen a pas besoin :</strong> <em>la page entiĂšre dans <u>un</u> <code translate="no"><iframe srcdoc></code></em>. Le serveur ne voit ni scripts ni HTML â une chaĂźne Ă©chappĂ©e dans un attribut â et le navigateur rend la page complĂšte, mĂȘme origine que le site. <strong>TEST S sâest affichĂ© du premier coup (« OURRA ça marche »).</strong> Câest la voie pour toute grosse page Ă scripts que le bloc refuse : lâInitiation, PsychaTrice V23, AstroGlobe 3,9 Mo. Le fichier source reste la Page (§ L) ; la version <code translate="no">_WEBNODE</code> est un <em>dĂ©rivĂ©</em>, rĂ©gĂ©nĂ©rĂ© Ă chaque version.</div>
<pre translate="no"><code translate="no">python3 p15_cadre_webnode.py Page_Vn.html Page_Vn_WEBNODE.html
<div id="ami-ini-hote"><iframe id="ami-ini" allowfullscreen allow="fullscreen" scrolling="no"
style="display:block;width:100%;height:100vh;border:0;overflow:hidden;background:#09090e"
srcdoc="âŠla page, & â &amp; et " â &quot; âŠ"></iframe></div>
<script>/* le dimensionneur : height = document du cadre .scrollHeight, toutes les 600 ms â
sauf en plein écran ou en repli (body.i-faux-fs) */</script></code></pre>
<p><strong>Le cadre suit la hauteur de son contenu et câest la page du site qui dĂ©file</strong> â jamais un cadre Ă hauteur dâĂ©cran qui dĂ©file <em>dedans</em> : chez Diane il sâarrĂȘtait aux onglets (le dĂ©filement pris depuis la marge nâatteint pas le cadre). Mais un document qui ne dĂ©file pas perd tout ce qui comptait sur son propre dĂ©filement : barres collĂ©es, coins fixĂ©s Ă lâĂ©cran, progression, â, <code translate="no">scrollTo</code>. <strong>Le Pont</strong> (<code translate="no">outils/pont_cadre.js</code>, injectĂ© dans le srcdoc par p15) rebranche chacun sur la fenĂȘtre du parent.</p>
<div class="tw"><table>
<thead><tr><th>Dans la Page seule</th><th>Dans le Cadre â ce que fait le Pont</th></tr></thead>
<tbody>
<tr><td class="acc">La fenĂȘtre visible</td><td><code translate="no">vue()</code> = de <code translate="no">hote()</code> (bas de lâen-tĂȘte fixe du site, mesurĂ©) au bas de lâĂ©cran, en coordonnĂ©es du cadre â Ă©crite en <code translate="no">--vue-top</code>/<code translate="no">--vue-h</code> pour la CSS (le voile đ sây pose)</td></tr>
<tr><td class="acc"><code translate="no">position:sticky</code> â barre, onglets, volets, sous-onglets</td><td><code translate="no">colle(v)</code> : dĂšs quâune barre passe sous le haut de la vue, elle est posĂ©e en <code translate="no">position:absolute</code> Ă cette hauteur, un fantĂŽme de mĂȘme taille garde sa place, les barres sâempilent et <code translate="no">--pile</code> dit oĂč la pile sâarrĂȘte. SĂ©lecteur unique <code translate="no">SEL</code>, le mĂȘme que le script « colle » de la page</td></tr>
<tr><td class="acc"><code translate="no">position:fixed</code> â coins â â° đ, â đ§, #prog, voiles</td><td><code translate="no">scanFixes()</code> lit lâancrage <em>dans la feuille</em> (<code translate="no">top:auto</code> + <code translate="no">bottom</code> posĂ© = ancrĂ© en bas) ; <code translate="no">poseFixe()</code> les rend absolus au bord voulu de la vue ; un voile Ă©tirĂ© suit la vue en hauteur</td></tr>
<tr><td class="acc">Progression et â</td><td>calculĂ©s sur <code translate="no">parent.scrollY</code> ; le â est forcĂ© en ligne <code translate="no">!important</code> parce que le script de la page le re-cache</td></tr>
<tr><td class="acc"><code translate="no">window.scrollTo</code> / <code translate="no">scrollBy</code></td><td>traduits en <code translate="no">parent.scrollTo</code> (haut du cadre + y â en-tĂȘte) : sommaire, â, « retour oĂč jâĂ©tais »</td></tr>
<tr><td class="acc">Plein Ă©cran</td><td>vrai : <code translate="no">allowfullscreen</code>, <code translate="no">scrolling=yes</code> le temps du plein Ă©cran, le Pont se tait. Repli : le <em>cadre</em> prend lâĂ©cran du site (<code translate="no">position:fixed;inset:0</code>, body du site <code translate="no">overflow:hidden</code>), la page dĂ©file dedans, Ăchap ou â restaure</td></tr>
<tr><td class="acc">Les Ă©vĂ©nements <code translate="no">scroll</code></td><td>redistribuĂ©s Ă la page Ă chaque tick (elle croit dĂ©filer) ; tick sur scroll/resize du parent, clic, et toutes les 700 ms</td></tr>
</tbody>
</table></div>
<div class="alerte"><strong>Le sticky meurt sous Webnode mĂȘme sans cadre.</strong> <code translate="no">.b-c</code> et <code translate="no">.wnd-page</code> portent <code translate="no">overflow:hidden</code>, et un ancĂȘtre Ă <code translate="no">overflow:hidden</code> tue tout <code translate="no">position:sticky</code> dessous. Une page publiĂ©e porte donc son script « colle » (<code translate="no">ami-colle-js</code>) Ă <strong>dĂ©tection comportementale</strong> : si la barre dĂ©passĂ©e a dĂ©filĂ© avec le contenu â quelle quâen soit la cause â le script prend la main. <em>On ne devine pas lâhĂŽte, on constate.</em></div>
<div class="tw"><table>
<thead><tr><th>MesurĂ© dans le simulateur (<code translate="no">sim_cadre.html</code>, en-tĂȘte fixe 80 px, <code translate="no">.b-c</code> overflow hidden, 390 px)</th><th>Attendu</th></tr></thead>
<tbody>
<tr><td>barre 0 · onglets 48 · volets 164 · sous-onglets 240 px sous lâen-tĂȘte</td><td class="sain">trois rangĂ©es, en pile</td></tr>
<tr><td>đ â° â au bas de la fenĂȘtre, â aprĂšs 420 px, â ramĂšne en haut du site</td><td class="sain">coins Ă lâĂ©cran, pas au bas du cadre</td></tr>
<tr><td>repli de plein Ă©cran : cadre plein Ă©cran, dĂ©filement dedans, zoom 110 %, â restaure</td><td class="sain">immersif, et on en sort</td></tr>
</tbody>
</table></div>
<h3 class="sub" id="s-cadre-verifier-en-ligne">â VĂ©rifier en ligne â sans accĂšs au code du bloc</h3>
<p>Webnode ne laisse pas relire un bloc collĂ©. On vĂ©rifie <em>dans le code servi</em> : un marqueur <code translate="no">data-test="S"</code> sur la racine et une sonde <code translate="no">data-ami-js</code> posĂ©e par le premier script ; lâadresse publiĂ©e est lue au navigateur, et lâon cherche les deux. <strong>Marqueur absent : refusĂ© (le bloc a gardĂ© lâancien contenu). Marqueur prĂ©sent, sonde absente : le texte passe mais les scripts ne tournent pas.</strong> Le rendu se vĂ©rifie ensuite Ă lâĆil et au script, sur tĂ©lĂ©phone dâabord.</p>
<div class="check-liste">
<div class="check-item"><div class="check-box"></div><span>La page source est finie et contrÎlée (mutations, débordements 320/390/768/1280, 0 <code translate="no">var(--x)</code> sans déclaration) <b>avant</b> de générer le dérivé.</span></div>
<div class="check-item"><div class="check-box"></div><span><code translate="no">p15_cadre_webnode.py</code> rĂ©gĂ©nĂ©rĂ© Ă chaque version ; le fichier <code translate="no">_WEBNODE</code> nâest jamais Ă©ditĂ© Ă la main.</span></div>
<div class="check-item"><div class="check-box"></div><span>Le simulateur est rejouĂ© : pile des barres, coins au bas de lâĂ©cran, â, plein Ă©cran et repli.</span></div>
<div class="check-item"><div class="check-box"></div><span>Un seul bloc « Code HTML » par page publiĂ©e : jamais la page dans lâun et des scripts dans lâautre (500).</span></div>
<div class="check-item"><div class="check-box"></div><span>En ligne : marqueur et sonde lus dans le code servi, puis le rendu sur tĂ©lĂ©phone.</span></div>
</div>
</div>
'''
ins_before('<!-- ââ 36 · LA COULEUR D\'UN WIDGET', B, 'section 36')
# ââ C · dĂ©faut trouvĂ© au contrĂŽle : le volet âïž Â« Les OpĂ©rations » (tvp-t5) du Tableau Vivant n'Ă©tait pas fermĂ© âââââ
# â 𧿠« Le Widget » (tvp-t6) devenait son ENFANT dans le DOM : cachĂ© avec lui, la face Widget ne s'affichait jamais
# (mesuré sur V6 : #s-la-couleur-d-un-widget hauteur 0 aprÚs navigation). Un </div> avant tvp-t6.
import re
a = h.find('<div class="pane" id="tvp-t5"'); b = h.find('<div class="pane" id="tvp-t6"'); assert a > 0 and b > a
st = 0
for m in re.finditer(r'<div\b[^>]*>|</div>', h[a:b]): st += -1 if m.group(0).startswith('</') else 1
assert st == 1, f'â t5 : {st} div non fermĂ©(s), 1 attendu'
h = h[:b] + '</div>\n' + h[b:]; n += 1
# ââ D · les deux entrĂ©es de sommaire (le sommaire des faces est Ă©crit, pas construit) âââââââââââââââââââââ
som6 = '<div class="som-p"><button class="som-pt" type="button" aria-expanded="false"><span class="som-pe">đ</span><span class="som-pn">LII</span><a href="#s-le-protocole-de-hauteur-astroheight">Le protocole de hauteur â astroHeight</a></button></div>'
ins_after(som6, '<div class="som-p"><button class="som-pt" type="button" aria-expanded="false"><span class="som-pe">đȘ</span><span class="som-pn">LII bis</span><a href="#s-publier-sur-webnode-le-cadre-et-le-pont">Publier sur Webnode â le Cadre et le Pont</a><span class="som-px">ïŒ</span></button><div class="som-d"><ol><li><a href="#s-cadre-verifier-en-ligne">VĂ©rifier en ligne â sans accĂšs au code du bloc</a></li></ol></div></div>', 'sommaire t6')
som5 = '<span class="som-pn">XXIX</span><a href="#s-la-boite-a-outils-de-page">La BoĂźte Ă Outils de page â â¶ â â° â % +</a></button></div>'
assert h.count(som5) == 1, 'sommaire : la boĂźte Ă outils'
h = h.replace(som5, '<span class="som-pn">XXIX</span><a href="#s-la-boite-a-outils-de-page">La BoĂźte Ă Outils de page â â¶ â â° â % +</a><span class="som-px">ïŒ</span></button><div class="som-d"><ol><li><a href="#s-boite-places-etats">Les places et les Ă©tats â la rĂšgle du 18 septembre</a></li></ol></div></div>'); n += 1
# ââ le titre ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
old_t = '<title>La Charte de Communication aMi â V6 allĂ©gĂ©e</title>'
assert h.count(old_t) == 1; h = h.replace(old_t, '<title>La Charte de Communication aMi â V7 allĂ©gĂ©e</title>'); n += 1
open(OUT, 'w', encoding='utf-8').write(h)
print(f'â p18 : {n} gestes · {len(h.encode())/1e6:.2f} Mo â {OUT}')
p19_plein_ecran_v16.py python3 p19_plein_ecran_v16.py entree.html sortie.html id-racine71 lignes
Le plein écran du composant V16 : la barre et les coins entrent dans la racine, la racine défile, body.ami-plein (zoom), repli fixed, petit pont pageYOffset/scrollTo, boutons à la rÚgle du 18/09.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P19 · le composant V16 (barre + coins), sur 0.1 Apokalypse · 0.2 PsychaTrice · 0.3 L'Union du TouT â 18 sept 2026.
Retours de Diane du 18/09 (06h20 : « le plein écran fige la une », « les onglets font un gros trou » ; 08h10 : la rÚgle des boutons).
MesurĂ© (Chromium, plein Ă©cran rĂ©el) : le composant met en plein Ă©cran LA RACINE DU BLOC (#ami-x) â la barre V16 et ses coins
(â đ§ â¶ zoom) vivent AVANT la racine, donc hors du plein Ă©cran : invisibles, --nav-h (204 px dĂ©ployĂ©e) reste posĂ© â le trou ;
la racine n'a pas overflow â rien ne dĂ©file (Apokalypse, PsychaTrice) ; `body.ami-plein .coin-zoom{display:flex}` mais la
classe Ă©tait posĂ©e sur la racine â le zoom n'a jamais paru.
Ce que fait P19, sans toucher au moteur :
1. en plein Ă©cran (vrai ou repli), la barre V16 ENTRE dans la racine (avec les coins â â° đ ? et le voile đ d'Apokalypse, qui vivent hors racine aussi ; un commentaire garde chaque place) :
coins visibles, â¶ââ atteignable, --nav-h juste ; la racine dĂ©file (overflow-y:auto) ; la classe ami-plein va aussi sur body â zoom ;
2. le repli (API refusĂ©e) : la racine prend l'Ă©cran (position:fixed inset 0, dĂ©file dedans) â mĂȘme rĂšgle sur les trois volumes ;
3. la rĂšgle des boutons : đ§ lecture Ă gauche (.5rem) puis â contraste (2.9rem), comme l'Initiation ; actif = forme PLEINE
(goutte pleine, â â â), pas de halo.
Usage : python3 p19_plein_ecran_v16.py entree.html sortie.html id-racine"""
import sys
SRC, OUT, ROOT = sys.argv[1], sys.argv[2], sys.argv[3]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(why or old[:50])
# ââ 1 · la barre entre dans la racine en plein Ă©cran ; ami-plein aussi sur body ââââââââââââââââââââââââââ
rep(" function majPlein(){\n var on=pleinActif();\n RAC.classList.toggle('ami-plein',on);",
" /* 18/09 â en plein Ă©cran, la barre V16 (qui vit AVANT la racine) entre dans la racine, sinon ni coins ni â, et --nav-h\n"
" creuse un trou de sa hauteur au-dessus des onglets. Un commentaire garde sa place ; elle y revient en sortant. */\n"
" var DEDANS=[]; /* la barre, et les coins de la boĂźte Ă outils (â â° đ ?, le voile đ) qui vivent hors de la racine */\n"
" function rentre(){ if(DEDANS.length) return; var ls=document.querySelectorAll('#amiBarre,.ami-coin,#ami-rech');\n"
" for(var i=0;i<ls.length;i++){ var el=ls[i]; if(RAC.contains(el)) continue; var ph=document.createComment('ami-place'); el.parentNode.insertBefore(ph,el);\n"
" if(el.id==='amiBarre') RAC.insertBefore(el,RAC.firstChild); else RAC.appendChild(el); DEDANS.push({el:el,ph:ph}); } }\n"
" function sort(){ DEDANS.forEach(function(x){ if(x.ph.parentNode) x.ph.parentNode.replaceChild(x.el,x.ph); }); DEDANS=[]; pontOff(); }\n"
" /* le petit pont : en plein écran c'est la racine qui défile, mais les scripts de la page lisent window.pageYOffset et\n"
" Ă©coutent window scroll (â aprĂšs 600 px, coins cachĂ©s sur la une, sommaire qui suit). On leur redonne les deux. */\n"
" var DESC=null, ST0=null;\n"
" function relai(){ try{ window.dispatchEvent(new Event('scroll')); }catch(x){} }\n"
" function pontOn(){ if(DESC) return; DESC={y:Object.getOwnPropertyDescriptor(window,'pageYOffset'), s:Object.getOwnPropertyDescriptor(window,'scrollY')}; ST0=window.scrollTo;\n"
" try{ Object.defineProperty(window,'pageYOffset',{configurable:true,get:function(){return RAC.scrollTop;}}); Object.defineProperty(window,'scrollY',{configurable:true,get:function(){return RAC.scrollTop;}}); }catch(x){}\n"
" window.scrollTo=function(a,b){ if(typeof a==='object'&&a) RAC.scrollTo(a); else RAC.scrollTo(a||0,b||0); };\n"
" RAC.addEventListener('scroll',relai,{passive:true}); relai(); }\n"
" function pontOff(){ if(!DESC) return; try{ if(DESC.y) Object.defineProperty(window,'pageYOffset',DESC.y); if(DESC.s) Object.defineProperty(window,'scrollY',DESC.s); }catch(x){}\n"
" window.scrollTo=ST0; RAC.removeEventListener('scroll',relai); DESC=null; relai(); }\n"
" function majPlein(){\n var on=pleinActif();\n RAC.classList.toggle('ami-plein',on);\n document.body.classList.toggle('ami-plein',on);\n"
" if(on){ rentre(); pontOn(); } else sort();\n try{ dispatchEvent(new Event('resize')); }catch(x){}",
1, 'majPlein : barre dans la racine + body.ami-plein')
# ââ 1 bis · Ăchap : un volume absorbĂ© (Union) ĂŽte la classe avant nous â majPlein ne partait pas, la barre restait dedans, â restait
rep(" if(e.key==='Escape'&&RAC.classList.contains('ami-faux-fs')){\n RAC.classList.remove('ami-faux-fs');\n document.documentElement.style.overflow=''; majPlein();\n }",
" if(e.key==='Escape'){\n if(RAC.classList.contains('ami-faux-fs')){ RAC.classList.remove('ami-faux-fs'); document.documentElement.style.overflow=''; }\n setTimeout(majPlein,0);\n }", 1, 'Ăchap : majPlein toujours')
# ââ 2 · la feuille ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
CSS = f'''
<style id="ami-acces-18-09">
/* 18 sept 2026 â plein Ă©cran qui DĂFILE, barre dedans, repli qui prend l'Ă©cran ; boutons d'accessibilitĂ© Ă la rĂšgle du 18/09 */
#{ROOT}:fullscreen,#{ROOT}:-webkit-full-screen{{overflow-y:auto!important;overflow-x:hidden!important;background:var(--onyx)}}
#{ROOT}.ami-faux-fs{{position:fixed!important;inset:0!important;z-index:99998!important;margin:0!important;max-width:none!important;overflow-y:auto!important;overflow-x:hidden!important;background:var(--onyx)}}
#{ROOT}.ami-plein > .ami-barre{{position:sticky;top:0;z-index:130}}
body.ami-plein .ami-pied{{display:none}}
.ami-barre .coin.g2#amiLecture{{left:.5rem}}
.ami-barre .coin.g#amiContraste{{left:2.9rem}}
.ami-barre #amiLecture[aria-pressed="true"] svg{{fill:currentColor}}
.ami-barre #amiContraste[aria-pressed="true"] svg circle{{fill:currentColor}}
.ami-barre .coin.actif,.ami-barre .coin[aria-pressed="true"]{{box-shadow:none}}
</style>
'''
if 'id="amiRech"' in h and 'id="ami-toc-toggle"' in h and 'id="amiHaut"' in h: # 0.1 Apokalypse : les coins de bas en haut â â° đ (+ ? au-dessus)
CSS = CSS.replace('</style>', f'#amiRech,#{ROOT} #amiRech{{bottom:1rem!important}}#{ROOT} #ami-toc-toggle{{bottom:4rem!important}}#amiHaut,#{ROOT} #amiHaut{{bottom:7rem!important}}#{ROOT} #ami-glossary-toggle{{bottom:10rem!important}}\n</style>')
J.append('coins â â° đ dans l\'ordre du 18/09')
rep('</head>', CSS + '</head>', 1, 'feuille ami-acces-18-09')
open(OUT, 'w', encoding='utf-8').write(h)
print('â p19 :', ' · '.join(J), f'· {len(h.encode())/1e6:.2f} Mo â {OUT}')
verif_plein_ecran.py python3 verif_plein_ecran.py page.htmlâŠ27 lignes
Playwright, plein Ă©cran rĂ©el : racine qui dĂ©file, barre dedans, --nav-h = hauteur de barre (pas de trou), zoom, â.
â§ Copier le script
import asyncio, os, sys
from playwright.async_api import async_playwright
D=os.path.abspath('.')
async def main():
async with async_playwright() as p:
b=await p.chromium.launch()
for f in sys.argv[1:]:
pg=await b.new_page(viewport={'width':1280,'height':800}); errs=[]
pg.on('pageerror', lambda e: errs.append(str(e)))
await pg.goto('file://'+D+'/'+f); await pg.wait_for_timeout(1500)
await pg.click('#amiPlein'); await pg.wait_for_timeout(900)
J='''()=>{const fe=document.fullscreenElement;const RAC=fe||document.getElementById('amiBarre').parentNode;const barre=document.getElementById('amiBarre');
const nav=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'));const zb=document.querySelector('.coin-zoom');const pl=document.getElementById('amiPlein');
const sticky=[...RAC.querySelectorAll('*')].filter(e=>getComputedStyle(e).position==='sticky'&&e!==barre&&e.getBoundingClientRect().height>10&&e.getBoundingClientRect().width>600).slice(0,3).map(e=>(e.className+'').slice(0,25)+'@'+Math.round(e.getBoundingClientRect().top)+' top:'+getComputedStyle(e).top);
return {fe:fe?fe.id:null, barreDedans:RAC.contains(barre), barreTop:Math.round(barre.getBoundingClientRect().top), barreH:Math.round(barre.getBoundingClientRect().height), navH:nav, overflowY:getComputedStyle(RAC).overflowY, zoom:getComputedStyle(zb).display, bouton:pl.textContent, bodyPlein:document.body.classList.contains('ami-plein'), sticky, racScroll:RAC.scrollTop}}'''
r=await pg.evaluate(J); print(f, 'PLEIN', r)
await pg.mouse.move(640,500); await pg.mouse.wheel(0,1500); await pg.wait_for_timeout(600)
r=await pg.evaluate(J); print(' aprĂšs molette : racScroll', r['racScroll'], 'barreTop', r['barreTop'], 'sticky', r['sticky'])
await pg.screenshot(path=f.replace('.html','_fs.png'))
await pg.click('#amiZoomP'); await pg.wait_for_timeout(300)
z=await pg.evaluate("()=>({zv:document.getElementById('amiZoomV').textContent, zoom:(document.fullscreenElement||document.body).style.zoom})"); print(' zoom +', z)
await pg.click('#amiPlein'); await pg.wait_for_timeout(800)
r=await pg.evaluate(J); print(' SORTIE', {k:r[k] for k in ('fe','barreDedans','bouton','bodyPlein','zoom')}, 'JS', errs[:2])
await pg.close()
await b.close()
asyncio.run(main())
verif_repli_coins.py python3 verif_repli_coins.py page.htmlâŠ32 lignes
Playwright, API refusĂ©e : le repli prend l'Ă©cran et dĂ©file, Ăchap ; positions et Ă©tats des coins â đ§ â¶.
â§ Copier le script
import asyncio, os, sys
from playwright.async_api import async_playwright
D=os.path.abspath('.')
async def main():
async with async_playwright() as p:
b=await p.chromium.launch()
for f in sys.argv[1:]:
pg=await b.new_page(viewport={'width':390,'height':800}); errs=[]
pg.on('pageerror', lambda e: errs.append(str(e)))
await pg.add_init_script("Element.prototype.requestFullscreen=function(){return Promise.reject(new Error('refus'))};")
await pg.goto('file://'+D+'/'+f); await pg.wait_for_timeout(1500)
parent0=await pg.evaluate("()=>document.getElementById('amiBarre').parentNode.tagName+'#'+document.getElementById('amiBarre').parentNode.id")
await pg.click('#amiPlein'); await pg.wait_for_timeout(700)
J='''()=>{const RAC=document.getElementById('amiBarre').closest('.ami-faux-fs')||document.getElementById('amiBarre').parentNode;const barre=document.getElementById('amiBarre');
return {faux:RAC.classList.contains('ami-faux-fs'), pos:getComputedStyle(RAC).position, ov:getComputedStyle(RAC).overflowY, bouton:document.getElementById('amiPlein').textContent, zoom:getComputedStyle(document.querySelector('.coin-zoom')).display, racScroll:RAC.scrollTop, sw:document.documentElement.scrollWidth}}'''
r=await pg.evaluate(J); print(f,'REPLI',r)
await pg.mouse.move(200,500); await pg.mouse.wheel(0,1200); await pg.wait_for_timeout(500)
r=await pg.evaluate(J); print(' molette racScroll', r['racScroll'])
await pg.screenshot(path=f.replace('.html','_faux390.png'))
await pg.keyboard.press('Escape'); await pg.wait_for_timeout(600)
r=await pg.evaluate(J); parent1=await pg.evaluate("()=>document.getElementById('amiBarre').parentNode.tagName+'#'+document.getElementById('amiBarre').parentNode.id")
print(' Ăchap â faux', r['faux'], 'bouton', r['bouton'], '| barre parent avant/aprĂšs', parent0, parent1)
# boutons : positions et états
await pg.evaluate("()=>document.getElementById('amiCoeur').click()"); await pg.wait_for_timeout(400)
await pg.click('#amiContraste'); await pg.wait_for_timeout(300)
s=await pg.evaluate('''()=>{const q=id=>document.getElementById(id);const g=e=>{const r=e.getBoundingClientRect();return {l:Math.round(r.left),t:Math.round(r.top),op:getComputedStyle(e).opacity}};
return {lecture:g(q('amiLecture')), contraste:g(q('amiContraste')), plein:g(q('amiPlein')), pressed:q('amiContraste').getAttribute('aria-pressed'), circle:getComputedStyle(q('amiContraste').querySelector('circle')).fill, shadow:getComputedStyle(q('amiContraste')).boxShadow}}''')
print(' coins', s, 'JS', errs[:2])
await pg.click('#amiContraste'); await pg.close()
await b.close()
asyncio.run(main())
verif_coins_plein.py python3 verif_coins_plein.py page.htmlâŠ23 lignes
Playwright : coins â â° đ visibles et placĂ©s en plein Ă©cran, pageYOffset pontĂ© puis restaurĂ©.
â§ Copier le script
import asyncio, os, sys
from playwright.async_api import async_playwright
D=os.path.abspath('.')
async def main():
async with async_playwright() as p:
b=await p.chromium.launch()
for f in sys.argv[1:]:
pg=await b.new_page(viewport={'width':390,'height':800}); errs=[]
pg.on('pageerror', lambda e: errs.append(str(e)))
await pg.goto('file://'+D+'/'+f); await pg.wait_for_timeout(1500)
J='''()=>[...document.querySelectorAll('.ami-coin,#ami-toc-toggle,.ami-top,[id^=amiHaut]')].filter(e=>e.getBoundingClientRect().width>0||e.id).slice(0,6).map(e=>{const b=e.getBoundingClientRect();const cs=getComputedStyle(e);return (e.id||e.className.slice(0,14))+' bas:'+Math.round(innerHeight-b.bottom)+' vis:'+(cs.visibility!=='hidden'&&cs.opacity!=='0'&&!e.hidden&&cs.display!=='none')})'''
await pg.click('#amiPlein'); await pg.wait_for_timeout(800)
await pg.evaluate("()=>document.fullscreenElement.scrollTo(0,3000)"); await pg.wait_for_timeout(700)
print(f, 'plein+3000', await pg.evaluate(J), 'pageYOffset', await pg.evaluate('window.pageYOffset'))
await pg.screenshot(path=f.replace('.html','_fs390.png'))
await pg.click('#amiPlein'); await pg.wait_for_timeout(800)
print(' sortie pageYOffset', await pg.evaluate('window.pageYOffset'), 'fe', await pg.evaluate('document.fullscreenElement'), 'JS', errs[:2])
await pg.evaluate("window.scrollTo(0,2000)"); await pg.wait_for_timeout(500)
print(' page+2000', await pg.evaluate(J), await pg.evaluate('window.pageYOffset'))
await pg.close()
await b.close()
asyncio.run(main())
p1_astro.py python3 p1_astro.py entree.html sortie.html89 lignes
AstroGlobe P1 : #dodec rĂ©parĂ©, faces â Cockpit ici, onclick â data-act + Ă©couteur dĂ©lĂ©guĂ©, title/meta, đź, casse mixte, orthographes.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · 2.2 AstroGlobe V21 â V22 â le vivant, l'identitĂ©, le texte (audit du 18/09, A·1 A·4 A·5 A·17 ; arbitrages de Diane 17h40).
1. L'erreur JS au chargement : getElementById('dodec') â l'Ă©lĂ©ment s'appelle dodec2 depuis une version ; garde-fou + bon id.
Le bouton « đ Ouvrir l'AstroGlobe â » visait .tab-btn[2] (= la MĂ©taMatrice) : il vise l'onglet astro.
2. 36 onclick= â data-act= et UN Ă©couteur dĂ©lĂ©guĂ© (rĂšgle du Bloc § LI) ; les deux onclick construits par script restent.
3. IdentitĂ© : <title>, meta ami:volume/version/date, đź pour 2.2 (đ« est 5.3 Astro â sommaire dĂ©taillĂ© de Diane), casse mixte « DodĂ©caĂšdre ».
4. Orthographes arrĂȘtĂ©es dans le TEXTE seulement (ConScience, TransMission, CosmoLogie) ; ⥠et â Ă une graphie (avec U+FE0F)."""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · le vivant âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep("const dodec = document.getElementById('dodec');\ndodec.addEventListener('mouseenter', () => dodec.style.animationPlayState = 'paused');\ndodec.addEventListener('mouseleave', () => dodec.style.animationPlayState = 'running');",
"const dodec = document.getElementById('dodec2') || document.getElementById('dodec');\n"
"if (dodec) {\n dodec.addEventListener('mouseenter', () => dodec.style.animationPlayState = 'paused');\n dodec.addEventListener('mouseleave', () => dodec.style.animationPlayState = 'running');\n"
" /* pause au toucher (tĂ©lĂ©phone) â la rotation est le geste de la page, elle se laisse arrĂȘter */\n"
" dodec.addEventListener('touchstart', () => { dodec.style.animationPlayState = dodec.style.animationPlayState === 'paused' ? 'running' : 'paused'; }, {passive:true});\n}",
1, "#dodec null â dodec2, garde-fou, pause au toucher")
rep('''onclick="switchDodTab('astro',document.querySelectorAll('.tab-btn')[2])"''',
'''onclick="switchDodTab('astro',document.querySelector('.tab-btn[data-tab="astro"]'))"''', 1, "« Ouvrir l'AstroGlobe » visait la MétaMatrice")
# ââ 1 ter · le patch Isil V15.5 envoyait chaque face vers « 2.2_AstroGlobe_aMi_V75_COCKPIT_STABILISE.html#champ-n » â un fichier local qui n'existe pas en ligne (page morte).
# Les faces ouvrent le Cockpit ICI, sur le champ n (amiOuvreCockpit, défini en P2).
rep("const url='2.2_AstroGlobe_aMi_V75_COCKPIT_STABILISE.html#champ-'+n;\n window.location.href=url;",
"if(window.amiOuvreCockpit){ amiOuvreCockpit('ici', n); return; }\n const url='2.2_AstroGlobe_aMi_V75_COCKPIT_STABILISE.html#champ-'+n;\n window.location.href=url;", 1, 'faces â Cockpit ici (patch Isil : lien local mort)')
J.append('note : le patch Isil V15.5 reste (rĂŽle/tabindex des faces), seule sa destination change')
# ââ 2 · onclick â data-act + Ă©couteur dĂ©lĂ©guĂ© âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
n_before = len(re.findall(r'\sonclick="', h))
h, n_act = re.subn(r'\sonclick="((?:switchDodTab|navDod|switchMMView|buildMMCerveaux|loadUnifiedFrame|openUnifiedFrame|clearMMTableFilter)\([^"]*)"', r' data-act="\1"', h)
J.append(f'onclick â data-act Ă{n_act} (restent {n_before - n_act} construits par script)')
assert n_act == 34, n_act
h, n_kd = re.subn(r'\sonkeydown="[^"]*switchDodTab[^"]*"', '', h); J.append(f'onkeydown de la une ĂŽtĂ© Ă{n_kd} (le dĂ©lĂ©guĂ© Ă©coute EntrĂ©e/Espace)')
DELEGUE = r'''
<script id="ami-delegue">
/* ââ UN SEUL ĂCOUTEUR : data-act="fn('a',this)" â plus de fonction globale exigĂ©e par l'attribut (Charte § LI) ââ */
(function(){'use strict';
function arg(s, el){ s=s.trim();
if(s==='this') return el; if(s==='null') return null; if(/^'.*'$/.test(s)||/^".*"$/.test(s)) return s.slice(1,-1);
var m=s.match(/^document\.querySelectorAll\(\s*'([^']*)'\s*\)\s*\[(\d+)\]$/); if(m) return document.querySelectorAll(m[1])[+m[2]]||null;
m=s.match(/^document\.querySelector\(\s*'([^']*)'\s*\)$/); if(m) return document.querySelector(m[1]);
if(/^-?\d+(\.\d+)?$/.test(s)) return +s; return s; }
function args(txt, el){ var out=[], cur='', q=null, d=0; for(var i=0;i<txt.length;i++){ var c=txt[i];
if(q){ cur+=c; if(c===q) q=null; continue; } if(c==="'"||c==='"'){ q=c; cur+=c; continue; }
if(c==='('||c==='[') d++; if(c===')'||c===']') d--; if(c===','&&d===0){ out.push(cur); cur=''; continue; } cur+=c; }
if(cur.trim()) out.push(cur); return out.map(function(a){ return arg(a, el); }); }
function agit(el, e){ var m=(el.getAttribute('data-act')||'').match(/^(\w+)\((.*)\)\s*;?\s*(return false;?)?\s*$/); if(!m) return;
var fn=window[m[1]]; if(typeof fn!=='function') return; if(m[3]||el.tagName==='A') e.preventDefault(); fn.apply(el, args(m[2], el)); }
document.addEventListener('click', function(e){ var el=e.target.closest&&e.target.closest('[data-act]'); if(el) agit(el, e); });
document.addEventListener('keydown', function(e){ if(e.key!=='Enter'&&e.key!==' ') return; var el=e.target.closest&&e.target.closest('[data-act][role=button],[data-act][tabindex]'); if(el&&el.tagName!=='BUTTON'&&el.tagName!=='A'){ e.preventDefault(); agit(el, e); } });
})();
</script>
'''
rep('</body>', DELEGUE + '</body>', 1, 'écouteur délégué')
rep('''return (b.getAttribute('onclick')||'').indexOf("switchDodTab('"+tabId+"'") !== -1;''',
'''return (b.getAttribute('data-act')||b.getAttribute('onclick')||'').indexOf("switchDodTab('"+tabId+"'") !== -1;''', 1, 'navDod lit data-act')
# ââ 3 · identitĂ© ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<title>DodĂ©caĂšdre · AstroGlobe · Astro-aMi â Volume unifiĂ© · Protocole aMi · Vol. 2.2</title>',
'<title>2.2 AstroGlobe · DodĂ©caĂšdre â Tu Sais Introspecter · Protocole de ConScience aMi</title>\n'
'<meta name="ami:volume" content="2.2"><meta name="ami:version" content="V22"><meta name="ami:date" content="18 septembre 2026">', 1, 'title + meta ami:*')
rep('<h1 class="hero-h1" style="margin-bottom:.4rem;line-height:1.2;">đ«<br/>DODĂCAĂDRE<br/><em style="font-size:.7em;letter-spacing:.15em;">· AstroGlobe ·</em></h1>',
'<h1 class="hero-h1" style="margin-bottom:.4rem;line-height:1.2;"><span class="e" aria-hidden="true">đź</span><br/>DodĂ©caĂšdre<br/><em style="font-size:.7em;letter-spacing:.15em;">· AstroGlobe ·</em></h1>', 1, 'une : đź, casse mixte')
rep('<p class="hero-vol">Protocole de ConScience aMi â Volume 2.2 · Tu Sais Introspecter · Portail & Cockpit</p>',
'<p class="hero-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">2.2</span> · Tu Sais Introspecter</p>', 1, 'kicker')
# la topnav et le pied propre disent đ« pour 2.2 : ils s'en vont en P4 ; les đ« restants sont des ornements de section (đ« l.1890) et le manifeste
h, n = re.subn(r'"emoji"\s*:\s*"đ«"', '"emoji":"đź"', h); J.append(f'manifeste emoji đ« â đź Ă{n}')
# ââ 4 · orthographes, dans le texte seulement ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
def texte_seulement(fn):
"""applique fn aux nĆuds texte hors <script>/<style>/attributs/SVG"""
global h
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<svg\b.*?</svg>|<[^>]+>)', h, flags=re.S)
for i in range(0, len(parts), 2): parts[i] = fn(parts[i])
h = ''.join(parts)
cnt = {}
def ortho(t):
for a, b in (('Conscience','ConScience'),('conscience','ConScience'),('Cosmologie','CosmoLogie'),('Transmission','TransMission'),('transmission','TransMission')):
n = len(re.findall(r'\b' + a + r'\b', t)); t = re.sub(r'\b' + a + r'\b', b, t); cnt[b] = cnt.get(b, 0) + n
t = t.replace('âĄïž', 'âĄ').replace('âĄ', 'âĄïž').replace('âïž', 'â').replace('â', 'âïž')
return t
texte_seulement(ortho); J.append('orthographes ' + ' · '.join(f'{k} +{v}' for k, v in cnt.items()) + ' · ⥠â Ă une graphie')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p2_astro.py python3 p2_astro.py entree.html sortie.html117 lignes
AstroGlobe P2 : six onglets qui replient, la porte (dodécaÚdre cliquable), reduced-motion, pulsations ℠7 s, émojis droits, tableaux .tw, grilles, cibles.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · 2.2 AstroGlobe â la forme (audit A·2 A·3 A·10 A·11 A·12 A·14 A·15 A·16 ; arbitrages B·2 B·3).
- Six onglets qui REPLIENT (plus de rouleau horizontal : « interdit car on voit pas »), libellé en couleur, sous-titre gris .72 rem.
- La une : « DodĂ©caĂšdre » en casse mixte (plus d'uppercase en Cormorant), l'aperçu PNG (0,96 Mo) s'en va, LE DODĂCAĂDRE 3D devient
la porte : il monte de l'onglet Architecture dans la une, cliquable â charge le Cockpit sur place ; Ă cĂŽtĂ© « đ Charger le cockpit ici »
et « â Ouvrir en plein Ă©cran » (les mots dĂ©jĂ employĂ©s par la page). 320 px : la une ne dĂ©borde plus.
- reduced-motion en tĂȘte ; pulsations â„ 7 s en opacitĂ© seule ; rotation du dodĂ©caĂšdre gardĂ©e (B·3), pause au survol/toucher (P1).
- Ămojis jamais en italique ; tableaux en rouleau ; grilles sans orpheline (auto-fit + dernier en pleine colonne) ; cibles â„ 24 px ;
min-height:100vh Îté de la une."""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · la une : le dodĂ©caĂšdre monte, l'image descend ââââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<div aria-label="Ouvrir le cockpit AstroGlobe" class="hero-live-preview"[^>]*>.*?</div></div>', h, re.S); assert m, 'hero-live-preview'
assert 'data:image/png' in m.group(0)
h = h[:m.start()] + '<!--ami:porte-->' + h[m.end():]; J.append("l'aperçu PNG de la une Îté (0,96 Mo)")
m = re.search(r'<div style="display:flex;justify-content:center;margin:2rem 0">\n<div class="dodec-wrapper" style="transform:scale\(\.8\)">.*?<div class="face-13-core">.*?</div>\n</div>\n</div>\n', h, re.S); assert m, 'dodec-wrapper'
dodec = m.group(0)
h = h[:m.start()] + '<p class="ami-renvoi-une">Le dodĂ©caĂšdre tourne en tĂȘte de page â il ouvre le Cockpit.</p>\n' + h[m.end():]; J.append('le dodĂ©caĂšdre 3D quitte lâonglet Architecture')
porte = ('<div class="ami-porte" id="ami-porte">\n'
' <div class="ami-porte-dodec" role="button" tabindex="0" aria-label="Ouvrir le Cockpit AstroGlobe ici" data-act="amiOuvreCockpit(\'ici\')">\n'
+ dodec.replace('style="transform:scale(.8)"', '') +
' </div>\n'
' <div class="ami-porte-b">\n'
' <button type="button" class="btn btn-or" data-act="amiOuvreCockpit(\'ici\')">đ Charger le cockpit ici</button>\n'
' <button type="button" class="btn" data-act="openUnifiedFrame(\'cockpit\')">â Ouvrir en plein Ă©cran</button>\n'
' </div>\n'
' <p class="ami-porte-l">12 faces + 1 centre = les 13 Champs. Touche le dodĂ©caĂšdre pour lâarrĂȘter, clique pour entrer.</p>\n'
'</div>')
rep('<!--ami:porte-->', porte, 1, 'la porte : dodécaÚdre cliquable + Charger ici / Ouvrir en plein écran')
OUVRE = '''
<script id="ami-porte-js">
window.amiOuvreCockpit=function(mode, n){ var b=document.querySelector('.tab-btn[data-tab="astro"]'); if(b&&window.switchDodTab) switchDodTab('astro',b);
if(window.loadUnifiedFrame) loadUnifiedFrame('cockpit', document.querySelector('[data-act="loadUnifiedFrame(\\'cockpit\\',this)"]'));
if(n){ var f=document.getElementById('unified-cockpit-frame'); if(f&&f.src){ var base=f.src.split('#')[0]; f.src=base+'#champ-'+n; } } };
</script>
'''
rep('<script id="ami-delegue">', OUVRE + '<script id="ami-delegue">', 1, 'amiOuvreCockpit')
# ââ 2 · animations et reduced-motion âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
assert '@keyframes pulseCenter' in h
h, n = re.subn(r'animation:\s*pulseCenter\s+3s', 'animation: pulseCenter 8s', h); J.append(f'pulseCenter 3 s â 8 s Ă{n}')
h, n = re.subn(r'animation:\s*glowPulse\s+4s', 'animation: glowPulse 9s', h); J.append(f'glowPulse 4 s â 9 s Ă{n}')
h, n = re.subn(r'animation:\s*c13Pulse\s+4s', 'animation: c13Pulse 9s', h); J.append(f'c13Pulse 4 s â 9 s Ă{n}')
m = re.search(r'@keyframes c13Pulse \{.*?\n\}', h, re.S); assert m
k = m.group(0); k2 = re.sub(r'transform:\s*translate\(-50%, -50%\) scale\([\d.]+\)', 'transform: translate(-50%, -50%)', k)
h = h.replace(k, k2); J.append('c13Pulse : plus dâĂ©chelle (translate seul = centrage, pas un mouvement)')
m = re.search(r'@keyframes glowPulse \{.*?\n\}', h, re.S); assert m
k = m.group(0); k2 = re.sub(r'\s*transform:\s*scale\([\d.]+\);', '', k); h = h.replace(k, k2); J.append('glowPulse : opacité seule')
# ââ 3 · la feuille de forme ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
CSS = '''
<style id="ami-forme-p2">
/* ââ P2 · 18 sept 2026 â la forme Ă la Charte ââ */
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
/* les six onglets replient : jamais de rouleau horizontal sur téléphone */
.tab-nav{flex-wrap:wrap;overflow:visible;gap:0}
.tab-btn{flex:1 1 33.33%;min-width:0;padding:.7rem .5rem .55rem;font-size:.86rem;letter-spacing:0;text-transform:none;line-height:1.2;color:var(--tb,var(--or-pale));opacity:.78;min-height:44px}
.tab-btn>span:first-child{display:block;font-size:1.15rem;line-height:1.1;margin-bottom:.15rem;font-style:normal}
.tab-btn .tb-step{font-size:.72rem;color:var(--gris);letter-spacing:0;text-transform:none;opacity:1;margin-top:2px}
.tab-btn{display:block}.tab-btn,.tab-btn>span{white-space:normal;overflow-wrap:normal;hyphens:auto;-webkit-hyphens:auto}.tab-btn>span:nth-child(2){display:block}
.tab-btn.active{opacity:1;border-bottom-color:var(--tb,var(--or));background:rgba(255,255,255,.04)}
.tab-btn:hover{opacity:1;color:var(--tb,var(--or-pale))}
.tab-btn[data-tab="overview"]{--tb:var(--or-pale)}.tab-btn[data-tab="geo"]{--tb:var(--saphir-ct)}.tab-btn[data-tab="matrice"]{--tb:var(--emeraude-ct)}
.tab-btn[data-tab="arbo"]{--tb:var(--moldavite-ct)}.tab-btn[data-tab="astro"]{--tb:var(--amethyste-ct)}.tab-btn[data-tab="vibrations"]{--tb:var(--tanzanite-ct)}
@media(min-width:900px){.tab-btn{flex:1 1 16%}}
/* la une : casse mixte, pas de 100vh, la porte */
.hero{min-height:0}
.hero-h1{text-transform:none;letter-spacing:.02em;font-weight:400}
.hero-h1 .e{font-style:normal;font-size:.7em}
.hero-vol{font-size:.72rem;letter-spacing:.28em;color:var(--or)}
.hero .ami-porte{width:100%;max-width:34rem;margin:1.2rem auto 2rem;text-align:center;padding:0;box-sizing:border-box;overflow:hidden}
.ami-porte-dodec{display:flex;justify-content:center;cursor:pointer;border-radius:50%;outline:none}
.ami-porte-dodec:focus-visible{box-shadow:0 0 0 2px var(--or)}
.ami-porte .dodec-wrapper{margin:0 auto 1rem;transform:scale(.9)}
.ami-porte-b{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:.6rem 0 .4rem}
.ami-porte-b .btn{min-height:44px;font-family:'Jost',sans-serif;font-size:.9rem;letter-spacing:0;text-transform:none;border-radius:999px;padding:.6rem 1.2rem}
.ami-porte-l{font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1rem;color:var(--gris-cl);margin:.4rem 0 0}
.ami-renvoi-une{font-family:'Cormorant Garamond',serif;font-style:italic;color:var(--gris-cl);text-align:center;margin:1rem 0 2rem}
@media(max-width:360px){.ami-porte .dodec-wrapper{transform:scale(.72);margin-bottom:-1rem}}
/* émojis droits, jamais en italique */
.hero-sub .e,.hero-axiome .e,.teaser-note .e,.mm-phrase .e,.reveal em .e,em .e,i .e,.e{font-style:normal}
/* tableaux : rouleau par rĂšgle */
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch}
/* grilles : le dernier prend la colonne pleine s'il est seul sur son rang */
.mm-grid-13,.champs-grille,.geo-grid,.cosmo-row{grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr))}
.geo-grid>*:last-child:nth-child(3n+1),.cosmo-row>*:last-child:nth-child(3n+1){grid-column:1/-1}
.mm-grid-13>*:last-child:nth-child(4n+1),.champs-grille>*:last-child:nth-child(4n+1){grid-column:1/-1}
/* cibles â„ 24 px */
.mm-view-btn{min-height:32px;font-size:.78rem}
.tab-btn,.btn,button{min-height:32px}
</style>
'''
rep('</head>', CSS + '</head>', 1, 'feuille ami-forme-p2')
# ââ 4 · Ă©mojis en italique : on les enveloppe (texte seulement, hors scripts/styles/svg) âââââââââââââââââ
EMO = re.compile('([\U0001F300-\U0001FAFFâ-âżâŹ-⯿\U0001F900-\U0001F9FFêŠ -ê§ż]ïž?)')
def enveloppe(t):
return EMO.sub(r'<span class="e">\1</span>', t)
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<svg\b.*?</svg>|<[^>]+>)', h, flags=re.S)
n = 0
for i in range(0, len(parts), 2):
t2 = enveloppe(parts[i]); n += t2.count('<span class="e">'); parts[i] = t2
h = ''.join(parts); J.append(f'Ă©mojis enveloppĂ©s .e Ă{n}')
# les tableaux en .tw
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>)', h, flags=re.S); n = 0
for i in range(0, len(parts), 2):
t2, a = re.subn(r'(<table\b)', r'<div class="tw">\1', parts[i]); t2, b = re.subn(r'(</table>)', r'\1</div>', t2); assert a == b; n += a; parts[i] = t2
h = ''.join(parts); J.append(f'tableaux en .tw Ă{n} (hors scripts)')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p4_astro.py python3 p4_astro.py entree.html sortie.html274 lignes
AstroGlobe P4 : racine, barre/pied V16, bande, Emporter, boĂźte đ â° â, contraste, cadres des widgets en srcdoc Ă hauteur suivie.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · 2.2 AstroGlobe â l'anatomie de Page (audit A·6 A·7 A·16 A·19 ; arbitrages B·1 B·7).
Racine #ami-astroglobe posĂ©e autour de la page ; barre + #prog en tĂȘte ; bande des registres aprĂšs la une ; â Emporter avant le
pied ; pied V16 aprÚs la racine (devise et maxime de l'ancien pied gardées) ; l'ancienne topnav et l'ancien pied s'en vont
(les 19 liens vivent dans la ForĂȘt de la barre â et ils pointaient vers des fichiers locaux morts) ; scripts du composant
(đ§ â â¶ zoom Emporter), progression ; la boĂźte Ă outils đ â° â (recherche gĂ©nĂ©rique, sommaire bĂąti depuis les onglets) ;
contraste élevé ; les cadres des widgets : Arborescence et Géométries en srcdoc, hauteur qui suit le contenu ; le Cockpit
garde sa lucarne (B·1 : intouché)."""
import re, sys, json
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open('../spec/barre_extrait.json'))
BOITE_CSS = open('../spec/boite_css.txt', encoding='utf-8').read().replace('#ami-rech{display:none!important}\n', '')
h = open(SRC, encoding='utf-8').read(); J = []
ROOT = '#ami-astroglobe'; RID = 'ami-astroglobe'
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · l'ancienne topnav et l'ancien pied s'en vont âââââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<nav class="topnav">.*?</nav>\s*', h, re.S); assert m; h = h[:m.start()] + h[m.end():]; J.append('topnav dâaoĂ»t ĂŽtĂ©e (19 liens locaux morts ; la ForĂȘt de la barre les porte)')
m = re.search(r'<footer>\s*<div class="footer-inner">.*?</footer>', h, re.S); assert m and 'nav-footer' in m.group(0)
h = h[:m.start()] + '<!--ami:emport-->' + h[m.end():]; J.append('ancien pied Îté (devise + maxime passent dans le pied V16)')
# ââ 2 · la racine, la barre, #prog, la bande, le pied âââââââââââââââââââââââââââââââââââââââââââââââââââââ
mb = re.search(r'<body[^>]*>\n?', h)
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
f'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="{RID}">')
assert nav.count('data-ici') == 1
h = h[:mb.end()] + d['prog'] + '\n' + nav + '\n<div id="' + RID + '">\n' + h[mb.end():]; J.append('barre V16 + #prog + racine ouverte')
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="https://agdistys.fr/ami-charte" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('</section>\n<!-- ââ 4 ONGLETS INITIATIQUES ââ -->', '</section>\n' + bande + '\n<!-- ââ 4 ONGLETS INITIATIQUES ââ -->', 1, 'bande des registres aprĂšs la une')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>', '<div class="ap-sceau" title="đź Le sceau du volume">đź</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', '<p class="ap-titre">AstroGlobe â DodĂ©caĂšdre</p>')
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">2.2</span> â Tu Sais Introspecter</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">C = ÎI / ÎE â Quand lâEntropie monte, la CohĂ©rence sâeffondre.\nLe Sol dâabord â toujours.</p>")
assert 'đź Le sceau' in pied and 'AstroGlobe â' in pied and 'Volume <span' in pied and 'Le Sol' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong> : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â six onglets, la MĂ©taMatrice, le dodĂ©caĂšdre, lecture continue, contraste, recherche. <em>Les trois widgets (Cockpit, Arborescence, GĂ©omĂ©tries) sont embarquĂ©s et se chargent au clic ; le Cockpit demande une connexion pour ses vues 3D.</em></p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V22</span> â arrĂȘtĂ©e au <span data-ami="date">18 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
mt = re.search(r'<!-- ⊠FORMULE-THEORIE-DU-TOUT.*?<!-- ⊠/FORMULE-THEORIE-DU-TOUT ⊠-->\s*<div style="[^"]*">\s*La Donnée = Le Sens à Le Temps\s*\xa0·\xa0\s*D = S à T\s*</div>', h, re.S); assert mt, 'bandeau Théorie du Tout'
theorie = mt.group(0); h = h[:mt.start()] + h[mt.end():]
rep('<!--ami:emport-->', '<div class="ami-theorie">' + theorie + '</div>\n' + emport + '\n</div><!-- /#' + RID + ' -->\n' + pied + '\n', 1, 'bandeau ThĂ©orie du Tout rentrĂ© dans la racine · â Emporter · racine fermĂ©e · pied V16')
# ââ 3 · la feuille du composant (nettoyĂ©e comme pour 0.3/0.1) ââââââââââââââââââââââââââââââââââââââââââââ
css = d['css']
blocs = []; k = 0; n = len(css)
while k < n:
j = css.find('{', k)
if j < 0: break
sel = css[k:j]; depth = 1; e = j + 1
while e < n and depth:
if css[e] == '{': depth += 1
elif css[e] == '}': depth -= 1
e += 1
blocs.append((sel, css[j:e])); k = e
out = []; otees = 0
for sel, corps in blocs:
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts): otees += 1; continue
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
sel = sel[:len(sel) - len(sel.lstrip())] + ',\n'.join(keep)
out.append(sel + corps)
css = ''.join(out)
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', ROOT + r' \1', css)
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items() if not v.startswith('4px}'))
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5, nettoyĂ©s (P5 de 0.3) ââ */\n'
':root{' + rootvals + ';--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
f'{ROOT} .emporter{{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);border:1px solid var(--or-somb);border-radius:14px;text-align:center}}\n'
f"{ROOT} .emporter h2{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none;border:0;padding:0}}\n"
f"{ROOT} .emporter p{{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}}\n"
f"{ROOT} .emporter p strong{{color:var(--nacre);font-weight:500;font-size:inherit}}\n"
f"{ROOT} .emporter .emp-b{{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}}\n"
f"{ROOT} .emporter .emp-b button{{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:44px}}\n"
f"{ROOT} .emporter .emp-b button:hover{{background:rgba(255,255,255,.1);color:var(--or-pale)}}\n"
f"{ROOT} .emporter .emp-d{{font-size:.78rem;color:var(--gris)}}\n{ROOT} .emporter [data-ami]{{color:var(--or-pale);font-size:inherit}}\n"
f'{ROOT} .bande-registres{{margin:1.2rem auto 0}}\n'
f'{ROOT} .tab-nav{{top:var(--nav-h,0px)}}\n'
+ css + '\n</style>\n')
assert '*{margin:0' not in feuille
rep('</head>', feuille + '</head>', 1, f'feuille du composant ({otees} rÚgles #tv/.tvx écartées)')
# ââ 4 · les scripts du composant âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
js = d['js_barre'].replace("document.getElementById('ami-charte')||document.body", f"document.getElementById('{RID}')||document.body")
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('.tab-btn').forEach(function(o,i){ o.classList.toggle('active', i===0); });
d.querySelectorAll('.tab-panel').forEach(function(p,i){ p.classList.toggle('active', i===0); });
d.querySelectorAll('iframe.unified-frame').forEach(function(f){ f.removeAttribute('src'); f.removeAttribute('srcdoc'); f.style.display=''; f.style.height=''; });""")
assert "'.tab-panel').forEach" in js
js = js.replace(""" if(a){ a.classList.add('ici');
deplie(true,false);
var p=N.querySelector('.porte[data-c="foret"]'); if(p) pose(p,true);
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}""", """ if(a){ a.classList.add('ici');
/* la page se marque, mais n'ouvre plus le tiroir toute seule (Diane, 17 sept) : un clic sur đł la ForĂȘt l'ouvre, Ă la bonne rangĂ©e */
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}"""); assert "n'ouvre plus le tiroir" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='2.2_ASTROGLOBE_Dodecaedre_aMi_'+(window.__amiV||'')+'.html';"); assert '2.2_ASTROGLOBE' in js
shim = f"""
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue ââ */
(function(){{'use strict';
var R=document.getElementById('{RID}'); if(!R) return; var memo=null;
document.addEventListener('ami:lecture',function(e){{
var on=e.detail&&e.detail.actif;
if(on){{ memo=R.querySelector('.tab-btn.active'); R.querySelectorAll('.tab-panel').forEach(function(p){{ p.classList.add('active'); p.querySelectorAll('.reveal').forEach(function(x){{ x.classList.add('visible'); }}); }}); R.classList.add('lecture'); }}
else {{ R.classList.remove('lecture'); if(memo&&window.switchDodTab) switchDodTab(memo.dataset.tab, memo); }}
}});
}})();
/* ââ LA BARRE DE PROGRESSION â elle mesure l'onglet ouvert ââ */
(function(){{'use strict';
var prog=document.getElementById('prog'), R=document.getElementById('{RID}'); if(!prog||!R) return;
function pile(){{ var n=document.querySelector('.ami-barre'); return n?Math.min(n.getBoundingClientRect().height,144):0; }}
function volet(){{ if(R.classList.contains('lecture')) return R; return R.querySelector('.tab-panel.active')||R; }}
function avance(){{ var v=volet(), r=v.getBoundingClientRect(), y=window.pageYOffset, debut=r.top+y-pile(), fin=r.bottom+y-window.innerHeight, course=fin-debut,
p=course>4 ? (y-debut)/course : (r.bottom<=window.innerHeight?1:0); prog.style.width=(Math.max(0,Math.min(1,p))*100).toFixed(2)+'%'; }}
window.addEventListener('scroll',avance,{{passive:true}}); window.addEventListener('resize',avance);
document.addEventListener('click',function(e){{ if(e.target.closest&&e.target.closest('.tab-btn,#amiLecture,#amiPlein,[data-act]')) setTimeout(avance,90); }},true);
document.addEventListener('ami:lecture',function(){{ setTimeout(avance,90); }}); avance();
}})();
/* ââ LES CADRES DES WIDGETS : Arborescence et GĂ©omĂ©tries suivent leur hauteur (srcdoc, mĂȘme origine) ; le Cockpit garde sa lucarne ââ */
(function(){{'use strict';
var SUIT={{arbo:1,vibrations:1}};
var orig=window.loadUnifiedFrame; if(!orig) return;
function suit(f){{ if(f.__suit) return; f.__suit=1; f.setAttribute('scrolling','no');
function mesure(){{ try{{ var d=f.contentDocument; if(!d||!d.documentElement) return; var hh=d.documentElement.scrollHeight; if(hh>200) f.style.height=hh+'px'; }}catch(x){{}} }}
f.addEventListener('load',function(){{ mesure(); setTimeout(mesure,400); setTimeout(mesure,1500); }}); setInterval(mesure,900); }}
window.loadUnifiedFrame=function(kind,btn){{
var ids={{cockpit:'unified-cockpit-frame',arbo:'unified-arbo-frame',vibrations:'unified-vibrations-frame'}};
var f=document.getElementById(ids[kind]); if(!f) return orig(kind,btn);
if(SUIT[kind]){{ if(!f.getAttribute('srcdoc')){{ var u=window.__amiPayload&&window.__amiPayload(kind); if(u){{ f.classList.add('suit'); suit(f); f.srcdoc=u; }} else return orig(kind,btn); }}
f.style.display='block'; if(btn) btn.textContent='â '+(kind==='arbo'?'Arborescence chargĂ©e':'GĂ©omĂ©tries chargĂ©es'); setTimeout(function(){{ f.scrollIntoView({{behavior:'smooth',block:'start'}}); }},120); return; }}
return orig(kind,btn);
}};
}})();
"""
# le dĂ©codeur des payloads devient joignable (une fonction, pas le texte) â sans toucher aux payloads
rep(" window.openUnifiedFrame=function(kind){window.open(urlFor(kind),'_blank','noopener')};",
" window.openUnifiedFrame=function(kind){window.open(urlFor(kind),'_blank','noopener')};\n window.__amiPayload=function(kind){ return payloads[kind] ? decodeBase64Utf8(payloads[kind]) : null; };", 1, '__amiPayload')
contraste = f"""
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct ââ */
body.ami-contraste {ROOT},body.ami-contraste{{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;--or-somb:#b09030;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;--violet-cl:#c9bcff;--cyan:#7fe3e0;--fuchsia:var(--grenat-ct)}}
body.ami-contraste {ROOT}{{font-weight:400}}
body.ami-contraste {ROOT} em{{font-style:normal}}
</style>
"""
# ââ 5 · la boĂźte Ă outils : â â° đ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
coins = '''
<button class="ami-coin h" id="amiHaut" type="button" hidden aria-label="Revenir en haut" title="â Revenir en haut">â</button>
<button class="ami-coin d3" id="ami-toc-toggle" type="button" aria-expanded="false" aria-controls="ami-toc-panel" aria-label="Sommaire de la page" title="â° Le sommaire de la page">â°</button>
<div id="ami-toc-panel" class="ami-toc" hidden role="dialog" aria-label="Sommaire"></div>
<button class="ami-coin r" id="amiRech" type="button" aria-label="Rechercher" title="Chercher dans le volume (Ctrl+F)"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></button>
<div id="ami-rech" role="dialog" aria-modal="true" aria-label="Rechercher dans le volume">
<div class="rc-b">
<div class="rc-h"><span class="rc-loupe"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></span>
<input type="search" id="rc-q" placeholder="Chercher dans les six ongletsâŠ" autocomplete="off" spellcheck="false" aria-label="Chercher">
<span class="rc-n" id="rc-n"></span>
<button class="rc-x" type="button" id="rc-x" aria-label="Fermer">â</button></div>
<div class="rc-l" id="rc-l" role="listbox"></div>
</div>
</div>
'''
boite_js = r'''
<script id="ami-boite-js">
/* ââ LA BOĂTE Ă OUTILS : â Haut · â° Sommaire (bĂąti depuis les onglets et leurs sections) · đ Recherche (indexe le texte de la racine, dit oĂč il est, ouvre l'onglet) ââ */
(function(){'use strict';
var R=document.getElementById('ami-astroglobe'); if(!R) return;
var H=document.getElementById('amiHaut');
if(H){ function suit(){ H.hidden = window.pageYOffset < 600; } window.addEventListener('scroll',suit,{passive:true}); suit();
H.addEventListener('click',function(){ window.scrollTo({top:0,behavior:'smooth'}); }); }
function nomOnglet(t){ var b=R.querySelector('.tab-btn[data-tab="'+t+'"]'); if(!b) return t; var s=b.querySelector('span:nth-child(2)'); var txt=(s?s.childNodes[0].textContent:b.textContent).trim(); var e=b.querySelector('span:first-child'); return (e?e.textContent.trim()+' ':'')+txt; }
/* â° */
var T=document.getElementById('ami-toc-toggle'), P=document.getElementById('ami-toc-panel');
function batiToc(){ var html='<div class="toc-h"><span>â° Sommaire</span><button type="button" class="toc-x" aria-label="Fermer">â</button></div>';
R.querySelectorAll('.tab-panel').forEach(function(pan){ var t=pan.id.replace('tab-','');
html+='<button type="button" class="toc-o" data-t="'+t+'">'+nomOnglet(t)+'</button>';
var secs=pan.querySelectorAll('.sec-header h2'); if(secs.length){ html+='<div class="toc-s">'; secs.forEach(function(x,i){ var id=x.closest('section,[id]')&&x.closest('section,[id]').id; if(!id){ id='ami-s-'+t+'-'+i; x.closest('.sec-header').id=id; } html+='<a href="#'+id+'" data-t="'+t+'">'+x.textContent.trim().replace(/</g,'<')+'</a>'; }); html+='</div>'; } });
P.innerHTML=html;
P.querySelector('.toc-x').addEventListener('click',fermeToc);
P.querySelectorAll('.toc-o').forEach(function(b){ b.addEventListener('click',function(){ va(b.dataset.t,null); }); });
P.querySelectorAll('.toc-s a').forEach(function(a){ a.addEventListener('click',function(e){ e.preventDefault(); va(a.dataset.t, a.getAttribute('href').slice(1)); }); }); }
function va(t,id){ fermeToc(); var b=R.querySelector('.tab-btn[data-tab="'+t+'"]'); if(b&&window.switchDodTab) switchDodTab(t,b);
setTimeout(function(){ var el=id?document.getElementById(id):R.querySelector('.tab-nav'); if(el){ var pile=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0; var y=el.getBoundingClientRect().top+window.pageYOffset-pile-56; window.scrollTo({top:Math.max(0,y),behavior:'smooth'}); } },160); }
function ouvreToc(){ if(!P.innerHTML) batiToc(); P.hidden=false; T.setAttribute('aria-expanded','true'); }
function fermeToc(){ P.hidden=true; T.setAttribute('aria-expanded','false'); }
if(T&&P){ T.addEventListener('click',function(){ P.hidden?ouvreToc():fermeToc(); }); document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&!P.hidden) fermeToc(); }); }
/* đ */
var B=document.getElementById('amiRech'), D=document.getElementById('ami-rech'), Q=document.getElementById('rc-q'), L=document.getElementById('rc-l'), N=document.getElementById('rc-n'), X=document.getElementById('rc-x');
if(!B||!D) return;
var IDX=null, RES=[], SEL=-1;
function bati(){ IDX=[];
var w=document.createTreeWalker(R,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT), n, chap='';
while((n=w.nextNode())){
if(n.nodeType===1){ if(n.tagName==='H2'||n.tagName==='H3'){ chap=n.textContent.trim(); } continue; }
var s=n.textContent.replace(/\s+/g,' ').trim(); if(s.length<3) continue;
var p=n.parentElement; if(!p||p.closest('script,style,svg,#ami-rech,#ami-toc-panel,.tab-nav,.ami-barre,.ami-pied,.emporter')) continue;
var pane=p.closest('.tab-panel'); IDX.push({t:s, tl:s.toLowerCase(), el:p, t2:pane?pane.id.replace('tab-',''):null, chap:chap}); } }
function esc(s){ return s.replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function cherche(q){ q=q.trim().toLowerCase(); L.innerHTML=''; RES=[]; SEL=-1;
if(q.length<2){ N.textContent=''; L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; return; }
if(!IDX) bati();
var vus={}, par={}, ordre=[];
IDX.forEach(function(e){ var i=e.tl.indexOf(q); if(i<0) return; var k=(e.t2||'')+'|'+e.chap+'|'+e.t.slice(0,60); if(vus[k]) return; vus[k]=1;
var g=e.t2?nomOnglet(e.t2):'â'; if(!par[g]){par[g]=[];ordre.push(g);} if(par[g].length<40) par[g].push({e:e,i:i}); });
var tot=0; ordre.forEach(function(g){ tot+=par[g].length; });
N.textContent = tot? tot+(tot>1?' passages':' passage') : 'rien';
if(!tot){ L.innerHTML='<div class="rc-v">Rien pour « '+esc(q)+' ».</div>'; return; }
ordre.forEach(function(g){ var d=document.createElement('div'); d.className='rc-g'; d.textContent=g; L.appendChild(d);
par[g].forEach(function(r){ var b=document.createElement('button'); b.type='button'; b.className='rc-r'; b.setAttribute('role','option');
var a=Math.max(0,r.i-46), z=Math.min(r.e.t.length,r.i+q.length+70);
b.innerHTML=(a?'âŠ':'')+esc(r.e.t.slice(a,r.i))+'<mark>'+esc(r.e.t.slice(r.i,r.i+q.length))+'</mark>'+esc(r.e.t.slice(r.i+q.length,z))+(z<r.e.t.length?'âŠ':'')+(r.e.chap?'<span class="rc-c">'+esc(r.e.chap)+'</span>':'');
b.dataset.k=RES.length; RES.push(r.e); b.addEventListener('click',function(){ saute(+b.dataset.k); }); L.appendChild(b); }); }); }
function saute(k){ var e=RES[k]; if(!e) return; ferme();
if(e.t2){ var b=R.querySelector('.tab-btn[data-tab="'+e.t2+'"]'); if(b&&!b.classList.contains('active')&&window.switchDodTab) switchDodTab(e.t2,b); }
setTimeout(function(){ var cible=e.el.closest('p,li,td,th,h1,h2,h3,h4,blockquote,figure,div')||e.el; var pile=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0;
var y=cible.getBoundingClientRect().top+window.pageYOffset-pile-64; window.scrollTo({top:Math.max(0,y),behavior:'smooth'});
cible.classList.remove('ami-trouve'); void cible.offsetWidth; cible.classList.add('ami-trouve'); setTimeout(function(){cible.classList.remove('ami-trouve');},2600); },220); }
function ouvre(){ D.classList.add('on'); B.setAttribute('aria-pressed','true'); Q.value=''; Q.focus(); L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; N.textContent=''; }
function ferme(){ D.classList.remove('on'); B.setAttribute('aria-pressed','false'); }
X.addEventListener('click',ferme); D.addEventListener('click',function(e){ if(e.target===D) ferme(); });
var tm=null; Q.addEventListener('input',function(){ clearTimeout(tm); tm=setTimeout(function(){ cherche(Q.value); },140); });
Q.addEventListener('keydown',function(e){ var bs=L.querySelectorAll('.rc-r');
if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); if(!bs.length) return; SEL=Math.max(0,Math.min(bs.length-1,SEL+(e.key==='ArrowDown'?1:-1))); bs.forEach(function(b2,i){b2.classList.toggle('sel',i===SEL);}); bs[SEL].scrollIntoView({block:'nearest'}); }
else if(e.key==='Enter'){ e.preventDefault(); saute(SEL>=0?SEL:0); } else if(e.key==='Escape'){ ferme(); } });
addEventListener('keydown',function(e){ if((e.ctrlKey||e.metaKey)&&(e.key==='f'||e.key==='F')){ e.preventDefault(); ouvre(); } else if(e.key==='Escape'&&D.classList.contains('on')) ferme(); });
B.addEventListener('click',ouvre);
})();
</script>
'''
boite_css = '''
<style id="ami-boite">
''' + BOITE_CSS + '''
/* les places du 18/09 : de bas en haut đ â° â */
#amiRech{bottom:1rem!important;right:1rem!important}#ami-toc-toggle{bottom:4rem!important;right:1rem!important}#amiHaut{bottom:7rem!important;right:1rem!important}
.ami-toc{position:fixed;right:1rem;bottom:6.6rem;z-index:61;width:min(22rem,calc(100vw - 2rem));max-height:min(70vh,34rem);overflow:auto;background:var(--encre);border:1px solid rgba(var(--or-rgb),.34);border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.6);font-family:'Jost',sans-serif}
.ami-toc .toc-h{display:flex;justify-content:space-between;align-items:center;padding:.6rem .9rem;border-bottom:1px solid rgba(var(--or-rgb),.18);color:var(--or-pale);font-size:.86rem;letter-spacing:.08em}
.ami-toc .toc-x{background:none;border:0;color:var(--gris);cursor:pointer;font-size:1rem;min-width:32px;min-height:32px}
.ami-toc .toc-o{display:block;width:100%;text-align:left;background:none;border:0;border-top:1px solid rgba(255,255,255,.05);color:var(--or-pale);font:500 .9rem 'Jost',sans-serif;padding:.55rem .9rem;cursor:pointer;min-height:36px}
.ami-toc .toc-o:hover{background:rgba(var(--or-rgb),.08)}
.ami-toc .toc-s{padding:0 .4rem .4rem .9rem}
.ami-toc .toc-s a{display:block;color:var(--gris-cl);text-decoration:none;font-size:.82rem;padding:.3rem .5rem;border-radius:6px;min-height:28px}
.ami-toc .toc-s a:hover{background:rgba(var(--or-rgb),.08);color:var(--nacre)}
.ami-trouve{outline:2px solid var(--or);outline-offset:4px;border-radius:4px;transition:outline-color 2s}
#ami-rech .rc-n,#ami-rech .rc-g,#ami-rech .rc-c{font-size:.72rem}
</style>
'''
rep('</head>', boite_css + contraste + '</head>', 1, 'feuilles boĂźte + contraste')
rep('</body>', coins + '\n<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n' + boite_js + '</body>', 1, 'coins + scripts du composant + boĂźte')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p5_astro.py python3 p5_astro.py entree.html sortie.html58 lignes
AstroGlobe P5 : encres translucides â pleines, plancher des cellules construites par script et des tailles en ligne.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · 2.2 AstroGlobe â les encres et ce que le plancher n'a pas vu (audit A·8 A·9 ; mesures aprĂšs P4).
1. Chaque encre Ă l'opacitĂ© 1 : « color:rgba(var(--x-rgb),a) » avec a < .75 â « color:var(--x-encre) », dans les feuilles et en ligne
(hors scripts, hors SVG). L'or Ă .3â.5 sur onyx faisait 1,7â2,9:1 ; le gris Ă .45 faisait 2,5:1. Les fonds/bordures gardent leur voile.
--or-somb n'est pas une encre (3,4:1) : en texte, il devient --or. Le rubis (4,3:1) et le grenat (3,7:1) passent en -ct pour le texte.
2. Le plancher pour ce que le script construit aprÚs coup (la table de la MétaMatrice, les cartes) et pour les tailles écrites en ligne.
3. Ămojis droits dans .mm-diag-horm ; cibles des liens du pied â„ 24 px ; la table des pĂŽles.
Usage : python3 p5_astro.py entree.html sortie.html"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · les encres ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ENCRE = {'--or':'var(--or)','--or-pale':'var(--or-pale)','--gris-cl':'var(--gris-cl)','--gris':'var(--gris)','--bleu-cl':'var(--bleu-cl)','--vert':'var(--vert-cl)',
'--fuchsia':'var(--grenat-ct)','--violet-cl':'var(--violet-cl)','--cyan':'var(--amazonite-ct)','--nacre':'var(--nacre)'}
def encre(nom):
if nom in ENCRE: return ENCRE[nom]
if nom.startswith('--anc-'): return f'var({nom})' # une pierre déjà nommée (rapatriée) : pleine
return f'var({nom})'
n1 = n2 = 0
def sub_css(m):
global n1
nom, a = m.group(1), float(m.group(2))
if a >= .75: return m.group(0)
n1 += 1; return 'color:' + encre(nom)
parts = re.split(r'(<script\b.*?</script>|<svg\b.*?</svg>)', h, flags=re.S)
for i in range(0, len(parts), 2):
t = parts[i]
t = re.sub(r'color:\s*rgba\(var\((--[a-z0-9-]+)-rgb\),\s*(\.?\d*\.?\d+)\)', sub_css, t)
parts[i] = t
h = ''.join(parts); J.append(f'encres translucides â pleines Ă{n1} (feuilles + en ligne, hors scripts/SVG)')
ENCRES = '''
<style id="ami-encres">
/* ââ P5 · les encres qui tenaient sous 4,5:1 mĂȘme pleines ââ */
#ami-astroglobe .sec-num,#ami-astroglobe .hero-vol,#ami-astroglobe .geo-section-title,#ami-astroglobe .geo-card-champ,#ami-astroglobe .mm-plan-champs,#ami-astroglobe .nectar-kicker,#ami-astroglobe .unified-kicker{color:var(--or)}
#ami-astroglobe .mm-card-vice,#ami-astroglobe p.geo-prop,#ami-astroglobe em.vice,#ami-astroglobe .mm-cell-bad{color:var(--rubis-ct)}
#ami-astroglobe .mm-card-sub,#ami-astroglobe .mm-topbar-sub,#ami-astroglobe em,#ami-astroglobe .cosmo-card-desc,#ami-astroglobe .geo-card-desc{color:var(--gris-cl)}
#ami-astroglobe .registre-block span,#ami-astroglobe .registre-block em{opacity:1}
#ami-astroglobe [style*="color:var(--or-somb)"],#ami-astroglobe [style*="color:var(--anc-orsomb)"]{color:var(--or)!important}
#ami-astroglobe .btn-or{background:var(--or);color:var(--onyx)}
#ami-astroglobe [style*="color:var(--rubis)"],#ami-astroglobe [style*="color:var(--anc-rubis)"]{color:var(--rubis-ct)!important}
#ami-astroglobe .ami-theorie{margin-top:2rem}
/* le plancher pour ce que le script construit aprÚs coup, et pour les tailles écrites en ligne */
#ami-astroglobe td,#ami-astroglobe th,#ami-astroglobe .mm-table *,#ami-astroglobe .mm-poles-dim,#ami-astroglobe .mm-card *{font-size:max(.72rem,1em)!important}
#ami-astroglobe [style*="font-size:.5"],#ami-astroglobe [style*="font-size:.6"],#ami-astroglobe [style*="font-size: .5"],#ami-astroglobe [style*="font-size: .6"]{font-size:max(.72rem,1em)!important}
#ami-astroglobe .mm-diag-horm .e,#ami-astroglobe em .e{font-style:normal}
/* cibles : les liens du pied et de la bande */
.ami-pied a,.bande-registres a{min-height:24px;display:inline-block}
</style>
'''
rep('</head>', ENCRES + '</head>', 1, 'feuille ami-encres')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p6_astro_mutations.py python3 p6_astro_mutations.py page.html121 lignes
AstroGlobe : 118 mutations (onglets, porte, faces, MĂ©taMatrice, widgets, đ§ â â¶ zoom, â â° đ, plein Ă©cran, 4 largeurs, reduced-motion).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 2.2 AstroGlobe â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis.
Six onglets, la porte (dodĂ©caĂšdre â Cockpit), les faces, les vues de la MĂ©taMatrice, les cerveaux, les trois widgets (hauteur qui suit),
đ§ â â¶ + zoom, â â° đ, #prog, export, metas, reduced-motion, barre .ici, plein Ă©cran (vrai + repli), 320/390/768/1280."""
import os, sys, datetime
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'astro_p6.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
MOIS=['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1200)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
# ââ identitĂ©
ok('meta ami:volume 2.2 · ami:version V23 · ami:date', E("()=>['2.2','V23'].every((v,i)=>document.querySelector('meta[name=\"ami:'+['volume','version'][i]+'\"]').content===v) && /2026/.test(document.querySelector('meta[name=\"ami:date\"]').content)"))
ok('titre : 2.2 AstroGlobe · DodĂ©caĂšdre â Tu Sais Introspecter', E("()=>document.title.startsWith('2.2 AstroGlobe · DodĂ©caĂšdre')"))
ok('la une en casse mixte, đź droit, kicker « Volume 2.2 · Tu Sais Introspecter »', E("()=>{const h=document.querySelector('.hero-h1');return getComputedStyle(h).textTransform==='none' && h.textContent.includes('DodĂ©caĂšdre') && !h.textContent.includes('DODĂCAĂDRE') && document.querySelector('.hero-vol').textContent.includes('Tu Sais Introspecter')}"))
ok('plus dâimage PNG en base64 dans la une', E("()=>document.querySelectorAll('img[src^=\"data:image\"]').length===0"))
ok('aucune var(--x) sans déclaration', E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>v!=='--cc').every(v=>decl.has(v));}"))
# ââ la barre, le pied, la bande
ok('barre V16 en tĂȘte, 2.2 marquĂ©e .ici, tiroir fermĂ©', E("()=>!!document.querySelector('.ami-barre .v.ici[data-v=\"ami-astroglobe\"]') && !document.querySelector('.ami-barre .tiroir.on')"))
ok('bande des registres aprĂšs la une', E("()=>{const b=document.querySelector('.bande-registres');return b && b.previousElementSibling.classList.contains('hero')}"))
ok('pied V16 aprĂšs la racine, sceau đź, volume 2.2', E("()=>{const f=document.querySelector('.ami-pied');return f && f.previousElementSibling.id==='ami-astroglobe' && f.querySelector('.ap-sceau').textContent.trim()==='đź' && f.textContent.includes('2.2')}"))
ok('ancienne topnav et ancien pied absents', E("()=>!document.querySelector('.topnav') && !document.querySelector('.nav-footer')"))
ok('â Emporter avant le pied, export nommĂ©', E("()=>{const e=document.querySelector('#ami-astroglobe .emporter');return e && !!document.getElementById('dlHtml') && e.parentElement.id==='ami-astroglobe'}"))
ok('bandeau Théorie du Tout dans la racine', E("()=>!!document.querySelector('#ami-astroglobe .ami-theorie a[href*=\"theorie-du-tout\"]')"))
# ââ les six onglets
ok('6 onglets, aucun ne roule (pas de rouleau horizontal)', E("()=>{const t=document.querySelector('.tab-nav');return document.querySelectorAll('.tab-btn').length===6 && t.scrollWidth<=t.clientWidth+1}"))
ok('sous-titres dâonglets gris â„ 11,5 px, libellĂ©s en couleur', E("()=>[...document.querySelectorAll('.tab-btn .tb-step')].every(s=>parseFloat(getComputedStyle(s).fontSize)>=11.5) && new Set([...document.querySelectorAll('.tab-btn')].map(b=>getComputedStyle(b).color)).size>=4"))
for i, tid in enumerate(['geo','matrice','arbo','astro','vibrations','overview']):
E(f"()=>document.querySelectorAll('.tab-btn')[{['overview','geo','matrice','arbo','astro','vibrations'].index(tid)}].click()"); pg.wait_for_timeout(300)
ok(f'onglet {tid} par clic', E("()=>document.querySelector('.tab-panel.active').id")=='tab-'+tid and E("()=>document.querySelector('.tab-btn.active').dataset.tab")==tid)
ok('les onglets collent sous la barre (top = --nav-h)', E("()=>getComputedStyle(document.querySelector('.tab-nav')).top===getComputedStyle(document.documentElement).getPropertyValue('--nav-h').trim()"))
# ââ la porte et les faces
E("()=>document.querySelector('.tab-btn[data-tab=overview]').click()"); pg.wait_for_timeout(200)
ok('le dodécaÚdre 3D est dans la une (13 faces), cliquable', E("()=>{const d=document.querySelector('.hero .ami-porte .dodec-wrapper');return d && d.querySelectorAll('.face').length===12 && !!d.querySelector('.face-13-core') && !!document.querySelector('.ami-porte-dodec[data-act]')}"))
E("()=>document.querySelector('.ami-porte-b .btn-or').click()"); pg.wait_for_timeout(2500)
ok('« Charger le cockpit ici » ouvre lâonglet Cockpit et charge le cadre', E("()=>document.querySelector('.tab-panel.active').id==='tab-astro' && !!document.getElementById('unified-cockpit-frame').src && document.getElementById('unified-cockpit-frame').getBoundingClientRect().height>300"))
E("()=>document.querySelector('.tab-btn[data-tab=overview]').click()"); pg.wait_for_timeout(200)
E("()=>document.querySelector('.hero .face-7').click()"); pg.wait_for_timeout(800)
ok('une face (C7) ouvre le Cockpit ici sur #champ-7 â plus de lien local mort', E("()=>document.querySelector('.tab-panel.active').id==='tab-astro' && /#champ-7$/.test(document.getElementById('unified-cockpit-frame').src) && location.href.indexOf('COCKPIT_STABILISE')<0"))
ok('la rotation du dodĂ©caĂšdre : 22 s, en pause au survol (code vivant, plus dâerreur #dodec)', E("()=>{const d=document.getElementById('dodec2');const a=getComputedStyle(d).animationName;d.dispatchEvent(new Event('mouseenter'));const p=d.style.animationPlayState;d.dispatchEvent(new Event('mouseleave'));return a==='rotateDodec' && p==='paused'}"))
# ââ MĂ©taMatrice
E("()=>document.querySelector('.tab-btn[data-tab=matrice]').click()"); pg.wait_for_timeout(600)
ok('MétaMatrice : 13 cartes construites', E("()=>document.querySelectorAll('#tab-matrice .mm-card').length>=13"))
for v in ('plans','cerveaux','diagonales','poles','tableau','cartes'):
E(f"()=>document.querySelector('.mm-view-btn[data-view={v}]').click()"); pg.wait_for_timeout(350)
ok(f'vue MétaMatrice « {v} »', E(f"()=>{{const b=document.querySelector('.mm-view-btn[data-view={v}]');const p=document.getElementById('mm-view-{v}');return b.classList.contains('active') && p && p.offsetParent!==null && p.getBoundingClientRect().height>40}}"))
E("()=>document.querySelector('.mm-view-btn[data-view=tableau]').click()"); pg.wait_for_timeout(400)
ok('table de la MétaMatrice : cellules ℠11,5 px, en rouleau', E("()=>{const t=document.querySelector('#mm-view-tableau table');if(!t)return false;const td=t.querySelector('td');return parseFloat(getComputedStyle(td).fontSize)>=11.5 && !!t.closest('.tw,[style*=overflow]')}"))
E("()=>document.querySelector('.mm-view-btn[data-view=cerveaux]').click()"); pg.wait_for_timeout(300)
E("()=>{const b=document.querySelector('[data-act^=\"buildMMCerveaux(\\'phi\"]'); if(b) b.click();}"); pg.wait_for_timeout(300)
ok('cerveau Ί construit par data-act', E("()=>document.querySelector('#mm-view-cerveaux').textContent.length>200"))
# ââ widgets
for kind, tid in (('arbo','arbo'),('vibrations','vibrations')):
E(f"()=>document.querySelector('.tab-btn[data-tab={tid}]').click()"); pg.wait_for_timeout(200)
E(f"()=>loadUnifiedFrame('{kind}', document.querySelector('[data-act^=\"loadUnifiedFrame(\\'{kind}\\'\"]'))"); pg.wait_for_timeout(3500)
ok(f'widget {kind} : cadre srcdoc, hauteur qui suit le contenu', E(f"()=>{{const f=document.getElementById('unified-{kind}-frame');try{{const sh=f.contentDocument.documentElement.scrollHeight;return !!f.getAttribute('srcdoc') && Math.abs(f.getBoundingClientRect().height-sh)<40 && sh>1000}}catch(e){{return false}}}}"))
ok('« â Ouvrir en plein Ă©cran » prĂ©sent pour les trois widgets', E("()=>document.querySelectorAll('[data-act^=\"openUnifiedFrame(\"]').length>=3"))
# ââ â¶ Cockpit plein Ă©cran (19/09) : la lucarne prend l'Ă©cran, â dedans, invitation au paysage en portrait seulement
ok('Brouillon : le kicker est parti (validé le 19/09)', E("()=>!document.body.textContent.includes('Brouillon · reconstruction')"))
E("()=>switchDodTab('astro',document.querySelector('.tab-btn[data-tab=astro]'))"); pg.wait_for_timeout(200)
pg.click('#amiCockpitPlein'); pg.wait_for_timeout(2200)
ok('â¶ Cockpit plein Ă©cran : la lucarne est lâĂ©lĂ©ment plein Ă©cran, le cadre chargĂ© et visible, â dedans', E("()=>{const L=document.getElementById('ami-lucarne'),F=document.getElementById('unified-cockpit-frame');return document.fullscreenElement===L && !!F.getAttribute('src') && getComputedStyle(F).display==='block' && F.getBoundingClientRect().height>innerHeight-80 && getComputedStyle(document.getElementById('ami-lucarne-x')).display==='flex' && document.getElementById('amiCockpitPlein').getAttribute('aria-pressed')==='true'}"))
ok('invitation « tourne ton téléphone » : en portrait seulement', E("()=>{const P=document.getElementById('ami-paysage');const port=matchMedia('(orientation:portrait)').matches;return (getComputedStyle(P).display!=='none')===(port && Math.min(innerWidth,innerHeight)<=768)}"))
E("()=>document.getElementById('ami-lucarne-x').click()"); pg.wait_for_timeout(700)
ok('â de la lucarne : sortie, bouton revenu Ă â¶', E("()=>!document.fullscreenElement && document.getElementById('amiCockpitPlein').textContent.startsWith('â¶')"))
E("()=>switchDodTab('vibrations',document.querySelector('.tab-btn[data-tab=vibrations]'))"); pg.wait_for_timeout(200)
# ââ đ§ â â¶ zoom
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
ok('đ§ lecture continue : 6 panneaux dĂ©roulĂ©s, goutte pleine', E("()=>document.querySelectorAll('.tab-panel.active').length===6 && document.getElementById('amiLecture').getAttribute('aria-pressed')==='true' && getComputedStyle(document.querySelector('#amiLecture svg')).fill!=='none'"))
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : un seul panneau', E("()=>document.querySelectorAll('.tab-panel.active').length===1"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
ok('â contraste : body.ami-contraste, cercle plein', E("()=>document.body.classList.contains('ami-contraste') && getComputedStyle(document.querySelector('#amiContraste svg circle')).fill!=='none'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('â au repos : demi-cercle (path plein, cercle vide)', E("()=>getComputedStyle(document.querySelector('#amiContraste svg circle')).fill==='none' && getComputedStyle(document.querySelector('#amiContraste svg path')).fill!=='none'"))
ok('đ§ Ă gauche (.5rem) puis â (2.9rem) ; â¶ Ă droite', E("()=>{const l=document.getElementById('amiLecture').getBoundingClientRect(),c=document.getElementById('amiContraste').getBoundingClientRect(),p=document.getElementById('amiPlein').getBoundingClientRect();return l.left<c.left && c.left<60 && p.right>innerWidth-60}"))
pg.click('#amiPlein'); pg.wait_for_timeout(700)
ok('â¶ plein Ă©cran : la racine dĂ©file, la barre est dedans, zoom visible, â', E("()=>{const fe=document.fullscreenElement;return fe && fe.id==='ami-astroglobe' && getComputedStyle(fe).overflowY==='auto' && fe.contains(document.getElementById('amiBarre')) && getComputedStyle(document.querySelector('.coin-zoom')).display==='flex' && document.getElementById('amiPlein').textContent==='â'}"))
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
ok('zoom + : 115 %', E("()=>document.getElementById('amiZoomV').textContent==='115%'"))
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(100)
pg.click('#amiPlein'); pg.wait_for_timeout(600)
ok('sortie du plein écran : la barre revient avant la racine', E("()=>!document.fullscreenElement && document.getElementById('amiBarre').nextElementSibling.id==='ami-astroglobe'"))
# ââ â â° đ
E("()=>{const v=document.querySelector('.tab-panel.active');window.scrollTo({top:v.getBoundingClientRect().top+window.pageYOffset+1200,behavior:'instant'})}"); pg.wait_for_timeout(500) # instant : le défilement doux du test était coupé par le resize des widgets à la sortie du plein écran (19/09)
ok('â paraĂźt aprĂšs 600 px, â° et đ en bas Ă droite (de bas en haut đ â° â)', E("()=>{const g=id=>document.getElementById(id).getBoundingClientRect();const r=g('amiRech'),t=g('ami-toc-toggle'),h=g('amiHaut');return !document.getElementById('amiHaut').hidden && r.bottom>t.bottom && t.bottom>h.bottom && r.right>innerWidth-40}"))
ok('#prog avance en descendant', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>document.getElementById('amiHaut').click()"); pg.wait_for_timeout(1600)
ok('â ramĂšne en haut', E("()=>window.pageYOffset<40"))
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(300)
ok('â° sommaire : 6 onglets, â„ 10 sections', E("()=>!document.getElementById('ami-toc-panel').hidden && document.querySelectorAll('#ami-toc-panel .toc-o').length===6 && document.querySelectorAll('#ami-toc-panel .toc-s a').length>=10"))
E("()=>document.querySelectorAll('#ami-toc-panel .toc-s a')[3].click()"); pg.wait_for_timeout(900)
ok('un lien du sommaire ouvre lâonglet et descend', E("()=>document.getElementById('ami-toc-panel').hidden && window.pageYOffset>200"))
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', 'juste mesure'); pg.wait_for_timeout(500)
ok('đ « juste mesure » trouve des passages, groupĂ©s par onglet', E("()=>document.querySelectorAll('#rc-l .rc-r').length>0 && document.querySelectorAll('#rc-l .rc-g').length>0"))
E("()=>document.querySelector('#rc-l .rc-r').click()"); pg.wait_for_timeout(900)
ok('un rĂ©sultat ouvre lâonglet et marque le passage', E("()=>!document.getElementById('ami-rech').classList.contains('on') && !!document.querySelector('.ami-trouve')"))
# ââ forme
ok('reduced-motion écrit ; pulsations ℠7 s ; rotation 22 s', E("()=>{const css=[...document.querySelectorAll('style')].map(s=>s.textContent).join(' ');return /prefers-reduced-motion/.test(css) && !/pulseCenter 3s|glowPulse 4s|c13Pulse 4s/.test(css) && /rotateDodec 22s/.test(css)}"))
ok('aucun émoji en italique dans la page', E("()=>[...document.querySelectorAll('#ami-astroglobe .e')].every(e=>getComputedStyle(e).fontStyle!=='italic')"))
ok('tous les tableaux en rouleau (.tw ou conteneur overflow-x:auto)', E("()=>[...document.querySelectorAll('#ami-astroglobe table')].every(t=>{let e=t.parentElement;while(e&&e.id!=='ami-astroglobe'){if(e.classList.contains('tw')||/auto|scroll/.test(getComputedStyle(e).overflowX))return true;e=e.parentElement}return false})"))
ok('plus dâonclick= Ă©crit dans le HTML (restent ceux construits par script : showGeoPreview, openMMCard)', E("()=>[...document.querySelectorAll('[onclick]')].every(e=>/^(showGeoPreview|openMMCard)\\(/.test(e.getAttribute('onclick')))"))
ok('orthographes : ConScience · TransMission · CosmoLogie', E("()=>{const t=document.getElementById('ami-astroglobe').innerText;return !/\\bConscience\\b/.test(t) && !/\\bTransmission\\b/.test(t) && !/\\bCosmologie\\b/.test(t)}"))
ok('aucun débordement horizontal', E("()=>document.documentElement.scrollWidth<=innerWidth"))
pg.close()
for W in (320, 768):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(900)
ok(f'aucun débordement à {W} px', pg.evaluate("()=>document.documentElement.scrollWidth<=innerWidth"), str(pg.evaluate("document.documentElement.scrollWidth"))); pg.close()
pg = b.new_page(viewport={'width': 390, 'height': 844}); pg.emulate_media(reduced_motion='reduce'); pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(700)
ok('reduced-motion : aucune animation en cours', pg.evaluate("()=>document.getAnimations().filter(a=>a.playState==='running').length")==0); pg.close()
errs = [e for e in errs if 'THREE is not defined' not in e] # le Cockpit (widget, intouché) attend three.js du réseau
ok('0 erreur JS sur les deux largeurs (hors THREE du Cockpit)', not errs, '; '.join(errs[:3]))
b.close()
n = sum(1 for _, c, _ in R if c); print(f'\n{n} / {len(R)} réussis')
if n < len(R): print('â ratĂ©s :', [x for x, c, _ in R if not c])
chaine_astro.sh bash chaine_astro.sh (astro.html à cÎté)25 lignes
Rejoue la chaĂźne AstroGlobe p1 â p6 d'un trait.
â§ Copier le script
set -e -o pipefail
python3 p1_astro.py astro.html astro_p1.html >/dev/null
python3 p2_astro.py astro_p1.html astro_p2.html >/dev/null
python3 - <<'PY'
import re
h=open('astro_p2.html',encoding='utf-8').read()
m=re.search(r':root \{\n(.*?)\n\}', h, re.S); root=m.group(1)
props=[re.sub(r'\s+','',l.strip()) for l in root.split('\n') if l.strip()]
h=h.replace(m.group(0), ':root{\n '+' '.join(props)+'\n}', 1); open('astro_p3.html','w',encoding='utf-8').write(h)
PY
python3 p3_palette.py rapatrier astro_p3.html ':root' >/dev/null
python3 p3_palette.py repeindre astro_p3.html ':root' >/dev/null
python3 p4_astro.py astro_p3.html astro_p4.html | tail -2
python3 ../acc/p19_plein_ecran_v16.py astro_p4.html astro_p4b.html ami-astroglobe >/dev/null
cp astro_p4b.html astro_p5.html
python3 p2_plancher.py astro_p5.html 390 ami-astroglobe 2>&1 | tail -1
python3 p2_plancher.py astro_p5.html 1280 ami-astroglobe 2>&1 | tail -1
python3 - <<'PY'
h=open('astro_p5.html',encoding='utf-8').read()
if '#ami-astroglobe span.e{font-size:.72rem}' in h:
h=h.replace('#ami-astroglobe span.e{font-size:.72rem}','#ami-astroglobe span.e{font-size:max(.72rem,1em)} /* un Ă©moji hĂ©rite : jamais de taille absolue (leçon dâApokalypse) */')
open('astro_p5.html','w',encoding='utf-8').write(h)
PY
python3 p5_astro.py astro_p5.html astro_p6.html | tail -1
p20_boite_volumes.py python3 p20_boite_volumes.py entree.html sortie.html union|psy101 lignes
0.3 et 0.2 : đ Recherche de la Charte (gĂ©nĂ©rique), â° â aux places du 18/09.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P20 · 0.3 L'Union du TouT et 0.2 PsychaTrice â la boĂźte Ă outils de la Charte complĂšte : đ Recherche arrive (gĂ©nĂ©rique : indexe le
texte de la racine, dit dans quel onglet/chapitre, ouvre l'onglet, marque le passage), â° et â (dĂ©jĂ lĂ ) prennent la forme et les places
du 18/09 : de bas en haut đ 1 rem · â° 4 rem · â 7 rem. Demande de Diane, 18/09 17h40 : « tout ce qu'on a fait niveau accessibilitĂ© pour
l'Initiation, à faire pour les autres ».
Usage : python3 p20_boite_volumes.py entree.html sortie.html union|psy"""
import re, sys
SRC, OUT, PAGE = sys.argv[1], sys.argv[2], sys.argv[3]
h = open(SRC, encoding='utf-8').read(); J = []
BOITE_CSS = open('../spec/boite_css.txt', encoding='utf-8').read().replace('#ami-rech{display:none!important}\n', '')
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
P = {
'union': dict(RID='ami-union', TOC='#tocBtn', HAUT='.ami-top', PANE='.tab-content', PANE_ON='.tab-content.active', TAB="'.tab-btn-full[data-c=\"'+k+'\"]'", KEY="p.dataset.c", NOM="(b.querySelector('.tb-l,.tt-l')||b).textContent.trim()", EXCL='.toc-panel,.ami-barre,.ami-pied,.emporter'),
'psy': dict(RID='ami-psychatrice', TOC='.ami-toc', HAUT='.ami-top', PANE='.pt-pane', PANE_ON='.pt-pane.on', TAB="'.pt-tab[data-t=\"'+k+'\"]'", KEY="p.id.replace('pane-','')", NOM="(b.querySelector('.tt-l')||b).textContent.trim()", EXCL='.ami-barre,.ami-pied,.emporter'),
}[PAGE]
RID = P['RID']
coins = '''
<button class="ami-coin r" id="amiRech" type="button" aria-label="Rechercher" title="Chercher dans le volume (Ctrl+F)"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></button>
<div id="ami-rech" role="dialog" aria-modal="true" aria-label="Rechercher dans le volume">
<div class="rc-b">
<div class="rc-h"><span class="rc-loupe"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></span>
<input type="search" id="rc-q" placeholder="Chercher dans le volumeâŠ" autocomplete="off" spellcheck="false" aria-label="Chercher">
<span class="rc-n" id="rc-n"></span>
<button class="rc-x" type="button" id="rc-x" aria-label="Fermer">â</button></div>
<div class="rc-l" id="rc-l" role="listbox"></div>
</div>
</div>
'''
js = r'''
<script id="ami-boite-js">
/* ââ đ LA RECHERCHE de la Charte (gĂ©nĂ©rique) : indexe le texte de la racine, dit oĂč il est, ouvre l'onglet, marque le passage ââ */
(function(){'use strict';
var R=document.getElementById('%RID%'); if(!R) return;
var B=document.getElementById('amiRech'), D=document.getElementById('ami-rech'), Q=document.getElementById('rc-q'), L=document.getElementById('rc-l'), N=document.getElementById('rc-n'), X=document.getElementById('rc-x');
if(!B||!D) return;
var IDX=null, RES=[], SEL=-1;
function cle(p){ return %KEY%; }
function nom(k){ var b=R.querySelector(%TAB%); if(!b) return k; return %NOM%; }
function bati(){ IDX=[];
var w=document.createTreeWalker(R,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT), n, chap='';
while((n=w.nextNode())){
if(n.nodeType===1){ if(n.tagName==='H2'||n.tagName==='H3'){ chap=n.textContent.trim(); } continue; }
var s=n.textContent.replace(/\s+/g,' ').trim(); if(s.length<3) continue;
var p=n.parentElement; if(!p||p.closest('script,style,svg,#ami-rech,%EXCL%')) continue;
var pane=p.closest('%PANE%'); IDX.push({t:s, tl:s.toLowerCase(), el:p, k:pane?cle(pane):null, chap:chap}); } }
function esc(s){ return s.replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function cherche(q){ q=q.trim().toLowerCase(); L.innerHTML=''; RES=[]; SEL=-1;
if(q.length<2){ N.textContent=''; L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; return; }
if(!IDX) bati();
var vus={}, par={}, ordre=[];
IDX.forEach(function(e){ var i=e.tl.indexOf(q); if(i<0) return; var kk=(e.k||'')+'|'+e.chap+'|'+e.t.slice(0,60); if(vus[kk]) return; vus[kk]=1;
var g=e.k?nom(e.k):'â'; if(!par[g]){par[g]=[];ordre.push(g);} if(par[g].length<40) par[g].push({e:e,i:i}); });
var tot=0; ordre.forEach(function(g){ tot+=par[g].length; });
N.textContent = tot? tot+(tot>1?' passages':' passage') : 'rien';
if(!tot){ L.innerHTML='<div class="rc-v">Rien pour « '+esc(q)+' ».</div>'; return; }
ordre.forEach(function(g){ var d=document.createElement('div'); d.className='rc-g'; d.textContent=g; L.appendChild(d);
par[g].forEach(function(r){ var b=document.createElement('button'); b.type='button'; b.className='rc-r'; b.setAttribute('role','option');
var a=Math.max(0,r.i-46), z=Math.min(r.e.t.length,r.i+q.length+70);
b.innerHTML=(a?'âŠ':'')+esc(r.e.t.slice(a,r.i))+'<mark>'+esc(r.e.t.slice(r.i,r.i+q.length))+'</mark>'+esc(r.e.t.slice(r.i+q.length,z))+(z<r.e.t.length?'âŠ':'')+(r.e.chap?'<span class="rc-c">'+esc(r.e.chap)+'</span>':'');
b.dataset.k=RES.length; RES.push(r.e); b.addEventListener('click',function(){ saute(+b.dataset.k); }); L.appendChild(b); }); }); }
function saute(k){ var e=RES[k]; if(!e) return; ferme();
if(e.k){ var b=R.querySelector((function(k){ return %TAB%; })(e.k)); if(b) b.click(); }
var d=e.el.closest('details'); while(d){ d.open=true; d=d.parentElement&&d.parentElement.closest('details'); }
setTimeout(function(){ var cible=e.el.closest('p,li,td,th,h1,h2,h3,h4,blockquote,figure,div')||e.el; var pile=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0;
var y=cible.getBoundingClientRect().top+window.pageYOffset-pile-64; window.scrollTo({top:Math.max(0,y),behavior:'smooth'});
cible.classList.remove('ami-trouve'); void cible.offsetWidth; cible.classList.add('ami-trouve'); setTimeout(function(){cible.classList.remove('ami-trouve');},2600); },260); }
function ouvre(){ D.classList.add('on'); B.setAttribute('aria-pressed','true'); Q.value=''; Q.focus(); L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; N.textContent=''; }
function ferme(){ D.classList.remove('on'); B.setAttribute('aria-pressed','false'); }
X.addEventListener('click',ferme); D.addEventListener('click',function(e){ if(e.target===D) ferme(); });
var tm=null; Q.addEventListener('input',function(){ clearTimeout(tm); tm=setTimeout(function(){ cherche(Q.value); },140); });
Q.addEventListener('keydown',function(e){ var bs=L.querySelectorAll('.rc-r');
if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); if(!bs.length) return; SEL=Math.max(0,Math.min(bs.length-1,SEL+(e.key==='ArrowDown'?1:-1))); bs.forEach(function(b2,i){b2.classList.toggle('sel',i===SEL);}); bs[SEL].scrollIntoView({block:'nearest'}); }
else if(e.key==='Enter'){ e.preventDefault(); saute(SEL>=0?SEL:0); } else if(e.key==='Escape'){ ferme(); } });
addEventListener('keydown',function(e){ if((e.ctrlKey||e.metaKey)&&(e.key==='f'||e.key==='F')){ e.preventDefault(); ouvre(); } else if(e.key==='Escape'&&D.classList.contains('on')) ferme(); });
B.addEventListener('click',ouvre);
})();
</script>
'''
for k, v in (('%RID%', RID), ('%KEY%', P['KEY']), ('%TAB%', P['TAB']), ('%NOM%', P['NOM']), ('%EXCL%', P['EXCL']), ('%PANE%', P['PANE'])):
js = js.replace(k, v)
css = '''
<style id="ami-boite">
''' + BOITE_CSS + f'''
/* â° et â (dĂ©jĂ lĂ ) prennent la forme des coins de la Charte, aux places du 18/09 : de bas en haut đ 1 rem · â° 4 rem · â 7 rem */
#{RID} {P['TOC']},#{RID} {P['HAUT']},{P['TOC']},{P['HAUT']}{{position:fixed!important;right:1rem!important;left:auto!important;width:2.1rem!important;height:2.1rem!important;min-width:0!important;padding:0!important;border-radius:50%!important;border:1px solid var(--or-somb)!important;background:rgba(9,9,14,.92)!important;color:var(--or)!important;font-size:.92rem!important;font-family:'Jost',sans-serif!important;line-height:1!important;box-shadow:none!important;transform:none!important;z-index:60!important;display:flex!important;align-items:center;justify-content:center}}
#{RID} {P['TOC']},{P['TOC']}{{bottom:4rem!important;top:auto!important}}
#{RID} {P['HAUT']},{P['HAUT']}{{bottom:7rem!important;top:auto!important}}
#amiRech{{bottom:1rem!important;right:1rem!important}}
#{RID} {P['TOC']}:hover,#{RID} {P['HAUT']}:hover{{background:rgba(255,255,255,.09)!important;color:var(--or-pale)!important}}
.ami-trouve{{outline:2px solid var(--or);outline-offset:4px;border-radius:4px;transition:outline-color 2s}}
#ami-rech .rc-n,#ami-rech .rc-g,#ami-rech .rc-c{{font-size:.72rem}}
</style>
'''
rep('</head>', css + '</head>', 1, 'feuille de la boĂźte')
rep('</body>', coins + js + '</body>', 1, 'đ + recherche gĂ©nĂ©rique')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p7_astro.py python3 p7_astro.py entree.html sortie.html17 lignes
AstroGlobe P7 : le kicker « Brouillon · reconstruction Ă valider » s'en va (validĂ© le 19/09), cadre pointillĂ© â plein.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P7 · 2.2 AstroGlobe â le kicker « đ§Ź Brouillon · reconstruction Ă valider avec Agdistys » s'en va : Diane l'a validĂ©
(19/09 00h15 : « ah oui validé :)) »). Le cadre en pointillé (l'air de brouillon) devient plein.
Usage : python3 p7_astro.py entree.html sortie.html"""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
rep(' <div style="font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;color:var(--or);text-align:center;margin-bottom:.4rem;"><span class="e">đ§Ź</span> Brouillon · reconstruction Ă valider avec Agdistys</div>\n', '', 1, 'kicker Brouillon ĂŽtĂ© (validĂ©)')
rep('border:1px dashed rgba(var(--or-rgb),.35);border-radius:16px;font-family:\'Jost\',sans-serif;color:var(--anc-nacre3);">\n <h3', 'border:1px solid rgba(var(--or-rgb),.35);border-radius:16px;font-family:\'Jost\',sans-serif;color:var(--anc-nacre3);">\n <h3', 1, 'cadre pointillĂ© â plein')
assert 'Brouillon' not in h
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p8_astro.py python3 p8_astro.py entree.html sortie.html69 lignes
AstroGlobe P8 : « â¶ Cockpit plein Ă©cran » (lucarne en plein Ă©cran, paysage demandĂ©, « tourne ton tĂ©lĂ©phone »), « â Ouvrir dans une page Ă part ».
â§ Copier le script
# -*- coding: utf-8 -*-
"""P8 · 2.2 AstroGlobe â le Cockpit sur tĂ©lĂ©phone (question de Diane, 19/09 00h15 : « faudrait le mettre en vue paysage direct non ? »).
Une page ne peut pas forcer l'orientation ; ce qu'elle PEUT : mettre la lucarne du Cockpit en vrai plein écran et, là seulement,
demander le paysage (screen.orientation.lock â Android/Chrome ; iOS refuse). Donc : un bouton « â¶ Cockpit plein Ă©cran » Ă cĂŽtĂ©
de « Charger ici » / « Ouvrir en plein écran » ; en plein écran on tente le paysage, et si l'appareil refuse, un mot :
« tourne ton tĂ©lĂ©phone ». Sous 768 px en portrait, la mĂȘme invitation est Ă©crite sous la lucarne.
Usage : python3 p8_astro.py entree.html sortie.html"""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
rep('<button data-act="openUnifiedFrame(\'cockpit\')" type="button">â Ouvrir en plein Ă©cran</button></div>\n<iframe class="unified-frame" id="unified-cockpit-frame" title="Cockpit AstroGlobe aMi V75"></iframe>',
'<button data-act="openUnifiedFrame(\'cockpit\')" type="button">â Ouvrir dans une page Ă part</button>'
'<button data-act="amiCockpitPlein()" type="button" id="amiCockpitPlein" aria-pressed="false">ⶠCockpit plein écran</button></div>\n'
'<div class="ami-lucarne" id="ami-lucarne"><iframe class="unified-frame" id="unified-cockpit-frame" title="Cockpit AstroGlobe aMi V75" allowfullscreen allow="fullscreen"></iframe>'
'<button type="button" class="ami-lucarne-x" id="ami-lucarne-x" aria-label="Quitter le plein Ă©cran" title="Quitter le plein Ă©cran (Ăchap)">â</button><p class="ami-paysage" id="ami-paysage" hidden><span class="e">đ±</span> Le Cockpit se lit mieux en paysage : tourne ton tĂ©lĂ©phone.</p></div>',
1, 'bouton ⶠCockpit plein écran + lucarne + invitation au paysage')
CSS = '''
<style id="ami-cockpit-paysage">
/* 19 sept 2026 â le Cockpit sur tĂ©lĂ©phone : plein Ă©cran de la lucarne, paysage demandĂ© quand l'appareil l'accepte */
.ami-lucarne{position:relative}
.ami-lucarne:fullscreen,.ami-lucarne:-webkit-full-screen{background:var(--onyx);display:flex;flex-direction:column}
.ami-lucarne:fullscreen .unified-frame,.ami-lucarne:-webkit-full-screen .unified-frame{display:block!important;flex:1 1 auto;height:auto!important;min-height:0;border:0;border-radius:0}
.ami-lucarne:fullscreen .ami-paysage,.ami-lucarne:-webkit-full-screen .ami-paysage{position:absolute;left:50%;bottom:.8rem;transform:translateX(-50%);margin:0;padding:.45rem .9rem;border-radius:999px;background:rgba(9,9,14,.9);border:1px solid var(--or-somb);z-index:2}
.ami-paysage{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or-pale);text-align:center;margin:.6rem 0 0}
.ami-paysage .e{font-style:normal}
@media(max-width:768px) and (orientation:portrait){.ami-lucarne:not(:fullscreen) .unified-frame[src]+.ami-paysage{display:block}}
@media(orientation:landscape){.ami-lucarne:fullscreen .ami-paysage{display:none}}
.ami-lucarne-x{display:none;position:absolute;top:.6rem;right:.6rem;z-index:3;width:2.1rem;height:2.1rem;border-radius:50%;border:1px solid var(--or-somb);background:rgba(9,9,14,.92);color:var(--or);font-family:'Jost',sans-serif;font-size:.95rem;line-height:1;cursor:pointer;align-items:center;justify-content:center;padding:0}
.ami-lucarne:fullscreen .ami-lucarne-x,.ami-lucarne:-webkit-full-screen .ami-lucarne-x{display:flex}
.ami-lucarne-x:hover{background:rgba(255,255,255,.09);color:var(--or-pale)}
#amiCockpitPlein[aria-pressed="true"]{background:rgba(255,255,255,.09)}
</style>
'''
rep('<button type="button" class="btn" data-act="openUnifiedFrame(\'cockpit\')">â Ouvrir en plein Ă©cran</button>', '<button type="button" class="btn" data-act="openUnifiedFrame(\'cockpit\')">â Ouvrir dans une page Ă part</button>', 1, 'la porte : « dans une page Ă part » (les mots de Diane : « dans le module ou dans une 2e page »)')
rep('</head>', CSS + '</head>', 1, 'feuille ami-cockpit-paysage')
JS = r'''
<script id="ami-cockpit-paysage-js">
/* ⶠCockpit plein écran : la lucarne prend l'écran ; on demande le paysage (Android/Chrome l'accorde en plein écran, iOS non) ;
sinon on invite Ă tourner le tĂ©lĂ©phone. Ăchap ou â (le mĂȘme bouton) rend la page. */
(function(){'use strict';
var L=document.getElementById('ami-lucarne'), B=document.getElementById('amiCockpitPlein'), P=document.getElementById('ami-paysage'), F=document.getElementById('unified-cockpit-frame');
if(!L||!B||!F) return;
var TEL=function(){ return Math.min(innerWidth,innerHeight)<=768; };
function portrait(){ return matchMedia('(orientation:portrait)').matches; }
function invite(){ if(!P) return; P.hidden = !( F.getAttribute('src') && TEL() && portrait() ); }
function pleinOn(){ return document.fullscreenElement===L || document.webkitFullscreenElement===L; }
function maj(){ var on=pleinOn(); B.textContent = on ? 'â Quitter le plein Ă©cran' : 'â¶ Cockpit plein Ă©cran'; B.setAttribute('aria-pressed', on?'true':'false'); invite();
if(!on){ try{ if(screen.orientation&&screen.orientation.unlock) screen.orientation.unlock(); }catch(x){} } }
window.amiCockpitPlein=function(){
if(pleinOn()){ (document.exitFullscreen||document.webkitExitFullscreen).call(document); return; }
if(!F.getAttribute('src')&&window.loadUnifiedFrame) loadUnifiedFrame('cockpit', document.querySelector('[data-act="loadUnifiedFrame(\'cockpit\',this)"]'));
var req=L.requestFullscreen||L.webkitRequestFullscreen; if(!req){ if(P){ P.hidden=false; } return; }
var r=req.call(L,{navigationUI:'hide'}); if(r&&r.then) r.then(paysage,function(){ if(P) P.hidden=false; }); else setTimeout(paysage,80); };
function paysage(){ maj(); try{ if(screen.orientation&&screen.orientation.lock){ screen.orientation.lock('landscape').then(function(){ setTimeout(function(){ if(P) P.hidden=!portrait(); },300); }, function(){ if(P) P.hidden=!portrait(); }); } else if(P) P.hidden=!portrait(); }catch(x){ if(P) P.hidden=!portrait(); } }
var X=document.getElementById('ami-lucarne-x'); if(X) X.addEventListener('click',function(){ if(pleinOn()) (document.exitFullscreen||document.webkitExitFullscreen).call(document); });
document.addEventListener('fullscreenchange',maj); document.addEventListener('webkitfullscreenchange',maj);
matchMedia('(orientation:portrait)').addEventListener('change',function(){ invite(); if(pleinOn()&&P) P.hidden=!portrait(); });
var ob=new MutationObserver(invite); ob.observe(F,{attributes:true,attributeFilter:['src','style']}); invite();
})();
</script>
'''
rep('<script id="ami-boite-js">', JS + '<script id="ami-boite-js">', 1, 'script du plein écran du Cockpit')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p21_loi_union.py python3 p21_loi_union.py union.html loi.html sortie.html248 lignes
0.3 : La Loi & l'Instrument V9 remplace l'ancienne Charte MathĂ©matique sous la Fractale (5 sous-onglets), palette M2, scoping, đ Ă tous les niveaux.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P21 · 0.3 L'Union du TouT â « La Loi & l'Instrument » V9 (Alignement RepĂšre), 19 sept 2026.
Demande de Diane (19/09 00h15) : « la charte mathématique à implémenter dans l'union du tout s'il te plait,
et en plus son pied de page est hors cadre ».
OĂč : t3 đ± Chartes du Vivant â đ« Fractale · Charte Physique Structurelle (le canton qui portait dĂ©jĂ l'ancienne
Charte Mathématique, plus courte). L'ancienne s'en va (markup, feuille, script) ; la V9 prend sa place :
sub-nav : â La Charte · đ Les Outils de Mesure · đ L'Outil de Falsification · đ± Les Trois Instruments
· đč L'Instrument (VĂ©rification de Ί, les trois verbes, Pont instrumental, RepĂšre de la PsychĂ©)
La coque du document (une, bande des deux volets, pied) ne vient pas : l'Union a sa une et son pied V16 â
c'est la réponse au « pied hors cadre ». Palette : les coques cessent de redéclarer --or & co (M2), les
couleurs écrites en dur reprennent le nom de leur pierre, les --uni-* deviennent des alias.
Usage : python3 p21_loi_union.py union_entree.html loi.html sortie.html"""
import re, sys
SRC, LOI, OUT = sys.argv[1], sys.argv[2], sys.argv[3]
U = open(SRC, encoding='utf-8').read(); L = open(LOI, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global U
c = U.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
U = U.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
def bloc(h, start, tag):
"""de l'ouverture <tag âŠ> Ă l'index start jusqu'Ă sa fermeture Ă©quilibrĂ©e."""
assert h.startswith('<' + tag, start)
rx = re.compile(r'<(/?)' + tag + r'\b[^>]*>'); d = 0
for m in rx.finditer(h, start):
d += -1 if m.group(1) else 1
if d == 0: return h[start:m.end()]
raise AssertionError('bloc non fermé ' + tag)
# ââ 1 · ce qu'on prend dans La Loi & l'Instrument âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
i = L.find('<header class="cm2-hero">'); hero = bloc(L, i, 'header')
panels = []
for m in re.finditer(r'<section class="cm2-panel[^"]*" id="cm2-([a-z]+)">', L):
panels.append((m.group(1), bloc(L, m.start(), 'section')))
assert [p[0] for p in panels] == ['charte', 'outils', 'falsif', 'instruments'], [p[0] for p in panels]
tabs = re.findall(r'<button class="cm2-tab[^"]*" data-p="([a-z]+)" type="button">(.*?)</button>', L)
assert len(tabs) == 4
i = L.find('<div class="phi-shell" id="phiShell">'); phi = bloc(L, i, 'div')
i = L.find('<div class="uni-statut">'); statut = bloc(L, i, 'div')
cards = [bloc(L, m.start(), 'section') for m in re.finditer(r'<section class="ti-card">', L)]; assert len(cards) == 2
dek = re.search(r'<p class="uni-dek">(.*?)</p>', L, re.S).group(1)
scripts = [m.group(1) for m in re.finditer(r'<script\b[^>]*>(.*?)</script>', L, re.S)]; assert len(scripts) == 3
styles = [m.group(1) for m in re.finditer(r'<style\b[^>]*>(.*?)</style>', L, re.S)]; assert len(styles) == 6
J.append('pris dans la V9 : hero, 4 panneaux, phi-shell, statut (trois verbes), 2 ti-cards, 2 scripts, 5 feuilles')
# le pied interne de l'instrument : la licence est au pied de l'Union
phi = phi.replace('<div class="phi-foot">Protocole de ConScience aMi · Agdistys © CC BY-SA 4.0 · Données stockées uniquement dans ce navigateur</div>',
'<div class="phi-foot">Données stockées uniquement dans ce navigateur.</div>'); assert 'uniquement dans ce navigateur.' in phi
# un seul h1 par page : le titre de la Charte Mathématique devient h2
hero = hero.replace('<h1>La Charte Mathématique</h1>', '<h2 class="cm2-h1">La Charte Mathématique</h2>'); assert 'cm2-h1' in hero
hero = hero.replace('<div class="cm2-kicker">Protocole de ConScience aMi · TroisiÚme Langue</div>',
'<div class="cm2-kicker">Charte Mathématique V9 · Alignement RepÚre · TroisiÚme Langue</div>'); assert 'V9 · Alignement' in hero
for k in range(len(cards)): cards[k] = cards[k].replace('<h2>', '<h3>').replace('</h2>', '</h3>')
phi = phi.replace('<h1>đč VĂ©rification de Ί</h1>', '<h2 class="phi-h1">đč VĂ©rification de Ί</h2>'); assert 'phi-h1' in phi
# les tableaux des Trois Instruments n'avaient pas de rouleau : on les enveloppe comme les autres (.cm2-tw)
n_ti = 0
for k in range(len(panels)):
kk, pnl = panels[k]
pnl, a = re.subn(r'<table class="ti-table">', '<div class="cm2-tw"><table class="ti-table">', pnl)
pnl, b2 = re.subn(r'</table>', '</table></div>', pnl) if a else (pnl, 0)
if a:
assert a == b2 == pnl.count('<div class="cm2-tw"><table class="ti-table">'), (a, b2)
n_ti += a; panels[k] = (kk, pnl)
assert n_ti == 3, n_ti
J.append('3 tableaux ti-table enveloppés .cm2-tw (rouleau)')
# ââ 2 · la palette : les valeurs d'avant â les pierres de l'Union âââââââââââââââââââââââââââââââââââââ
HEX = {'#c8a84a':'--or', '#e6d090':'--or-pale', '#7a611a':'--or-somb', '#f8c870':'--or-pale', '#c89a2a':'--or', '#e2c06a':'--or-pale', '#d9c17d':'--or-pale',
'#f2ede2':'--nacre', '#eee':'--nacre', '#a0a0b8':'--gris', '#c8c8dc':'--gris-cl',
'#07070c':'--noir', '#08070d':'--noir', '#09090e':'--noir', '#0b0a10':'--noir', '#0c0a10':'--noir', '#111118':'--encre', '#11111a':'--encre', '#181826':'--voile',
'#90d8a8':'--vert-cl', '#bfe6cf':'--vert-cl', '#c0e8d0':'--vert-cl', '#3a7a52':'--vert',
'#8a3030':'--rubis', '#f03030':'--rubis', '#f0a0a0':'--rubis-cl', '#e8c4cc':'--rubis-cl', '#e8c0c8':'--rubis-cl', '#fe6969':'--rubis-cl',
'#2a4870':'--bleu', '#80a8d8':'--bleu-cl', '#9b65d5':'--violet', '#4ec8b8':'--turq', '#ff93b5':'--rose',
'#ff7800':'--ambre', '#ffd06a':'--or-pale', '#80c027':'--jade-cl'}
RGB = {'200,168,74':'--or', '7,7,12':'--noir', '6,6,12':'--noir', '9,9,16':'--noir', '16,16,26':'--encre', '255,208,106':'--or-pale', '128,192,39':'--jade-cl',
'155,101,213':'--violet', '42,72,112':'--bleu', '58,122,82':'--vert', '138,48,48':'--rubis', '242,237,226':'--nacre', '240,160,160':'--rubis-cl',
'144,216,168':'--vert-cl', '200,154,42':'--or'}
def peint(css):
css = re.sub(r'rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,', lambda m: ('rgba(var(%s-rgb),' % RGB[','.join(m.group(1,2,3))]) if ','.join(m.group(1,2,3)) in RGB else m.group(0), css)
css = re.sub(r'#[0-9a-fA-F]{3,6}\b', lambda m: ('var(%s)' % HEX[m.group(0).lower()]) if m.group(0).lower() in HEX else m.group(0), css)
return css
# M2 · les coques cessent de redéclarer la palette
def sans_palette(css):
return re.sub(r'\n\s*--(?:or|or-pale|or-somb|nacre|gris|gris-cl|vert|vert-cl|rubis|rubis-cl|bleu|bleu-cl|violet|turq|rose|noir|encre|voile)\s*:\s*#[0-9a-fA-F]{3,6}\s*;', '', css)
# la feuille de la coque : on ne garde que le bandeau de statut, le code, et le bloc V5 (lex / eco / alignement)
c0 = styles[0]
keep = []
for sel in ['.cm2-card code', '.uni-statut', '.uni-statut-t', '.uni-verbes', '@media(min-width:760px){.uni-verbes', '.uni-v{', '@media(min-width:760px){.uni-v{', '.uni-v .e', '.uni-v .s', '.uni-v.ok', '.uni-v p', '.uni-v b', '.uni-jal', '.uni-j{', '.uni-j b']:
j = c0.find(sel); assert j >= 0, sel
k = c0.find('}', j);
if c0.startswith('@media', j): k = c0.find('}}', j) + 1
keep.append(c0[j:k+1])
v5 = c0[c0.find('/* V5 ·'):]; assert v5.startswith('/* V5')
c0k = '\n'.join(keep) + '\n' + v5
c0k = c0k.replace('var(--txt2)', 'var(--gris-cl)').replace('var(--or2)', 'var(--or-pale)').replace('var(--mut)', 'var(--gris)').replace('var(--line)', 'rgba(var(--or-rgb),.16)').replace('var(--mono)', "'Space Mono',monospace")
c0k = c0k.replace('var(--uni-trait)', 'rgba(255,255,255,.075)').replace('var(--uni-a16)', 'rgba(var(--or-rgb),.16)').replace('var(--uni-a26)', 'rgba(var(--or-rgb),.26)').replace('var(--uni-a32)', 'rgba(var(--or-rgb),.32)')
for a, b in (('--uni-onyx','--noir'),('--uni-encre','--encre'),('--uni-voile','--voile'),('--uni-nacre','--nacre'),('--uni-gris-cl','--gris-cl'),('--uni-gris','--gris'),('--uni-or-pale','--or-pale'),('--uni-or-somb','--or-somb'),('--uni-or','--or'),('--uni-emeraude','--vert-cl'),('--uni-ambre','--or-pale'),('--uni-violet','--violet')):
c0k = c0k.replace('var(%s)' % a, 'var(%s)' % b)
c2 = sans_palette(styles[2]).replace('.cm2-hero h1', '.cm2-hero .cm2-h1')
c3 = sans_palette(styles[3]).replace('.phi-shell h1', '.phi-shell .phi-h1').replace('.phi-hero h1', '.phi-hero .phi-h1'); c4 = sans_palette(styles[4]); c5 = styles[5]
css_all = peint(c0k + '\n' + c2 + '\n' + c3 + '\n' + c4 + '\n' + c5)
assert not re.search(r'--(?:or|nacre|gris)\s*:\s*#', css_all), 'palette redéclarée'
reste = sorted(set(h.lower() for h in re.findall(r'#[0-9a-fA-F]{3,6}\b', css_all)))
J.append(f'feuilles repeintes : hex restants {reste}')
# ââ 3 · scoper chaque sĂ©lecteur sous #ami-union .src-tri ââââââââââââââââââââââââââââââââââââââââââââââ
SC = '#ami-union .src-tri '
def scope(css):
out = []; i = 0; n = len(css)
# découpe grossiÚre : on marche rÚgle par rÚgle, en gérant les blocs @media (un seul niveau)
def sels(s):
return ','.join(SC + x.strip() for x in s.split(',') if x.strip())
pos = 0; res = ''
while pos < n:
j = css.find('{', pos)
if j < 0: res += css[pos:]; break
head = css[pos:j]
cm = head.rfind('*/'); pre, sel = (head[:cm+2], head[cm+2:]) if cm >= 0 else ('', head)
if sel.strip().startswith('@media'):
k = j + 1; d = 1
while d:
c = css[k]; d += (c == '{') - (c == '}'); k += 1
inner = css[j+1:k-1]
res += pre + sel + '{' + scope(inner) + '}'; pos = k; continue
if sel.strip().startswith('@'): # @keyframes, @font-face : on laisse
k = j + 1; d = 1
while d:
c = css[k]; d += (c == '{') - (c == '}'); k += 1
res += css[pos:k]; pos = k; continue
k = css.find('}', j)
res += pre + sels(sel) + css[j:k+1]; pos = k + 1
return res
css_sc = scope(css_all)
assert css_sc.count(SC) > 200, css_sc.count(SC)
# le panneau et la nav de la V9 ne servent plus (la sub-nav de l'Union les remplace) : les 4 panneaux restent visibles dans leur sub-p
css_sc = css_sc.replace(SC + '.cm2-panel{display:none;', SC + '.cm2-panel{display:block;')
assert '.cm2-panel{display:block;' in css_sc
FEUILLE = '''
<style id="ami-loi-instrument">
/* ââ 19 sept 2026 â La Charte MathĂ©matique V9 (Alignement RepĂšre) et l'Instrument, sous la Fractale ââ */
''' + css_sc + '''
/* ââ les rĂšgles de l'Union, aprĂšs la V9 (Ă spĂ©cificitĂ© Ă©gale, la derniĂšre gagne) ââ */
#ami-union .src-tri .tri-p[data-c="phy"]{--c:var(--or)}
#ami-union .src-tri .cm2-shell,#ami-union .src-tri .phi-shell{width:100%;max-width:1000px;font-family:'Jost',system-ui,sans-serif}
#ami-union .src-tri .cm2-shell .sub-p{display:none}#ami-union .src-tri .cm2-shell .sub-p.on{display:block}
#ami-union .src-tri .ami-instrument{max-width:1000px;margin:0 auto}
#ami-union .src-tri .ami-instrument .uni-statut{margin:0 0 1.2rem}
#ami-union .src-tri .ami-instrument .ti-card{--c:var(--or)}
#ami-union .src-tri .cm2-hero{margin:0 auto 1rem;max-width:1000px}
#ami-union .src-tri .cm2-hero .cm2-h1{margin:0}
#ami-union .src-tri .cm2-hero p.cm2-sub{margin:.5rem auto 0}
/* téléphone : les coques et les cartes rendent leurs marges au texte (lecture au téléphone d'abord) */
@media(max-width:620px){
#ami-union .src-tri .cm2-shell,#ami-union .src-tri .phi-shell{padding:.6rem .35rem;border-left:0;border-right:0;border-radius:0}
#ami-union .src-tri .cm2-card,#ami-union .src-tri .phi-card,#ami-union .src-tri .ti-card{padding:.8rem .65rem}
#ami-union .src-tri .cm2-sim{padding:.75rem .6rem}
#ami-union .src-tri .cm2-hero{padding:.9rem .3rem 1rem}
#ami-union .src-tri .tri-p[data-c="phy"]{padding-left:.3rem;padding-right:.3rem}
#ami-union .src-tri .ami-instrument .uni-v{padding:.85rem .7rem}
}
</style>
'''
# ââ 4 · l'ancienne Charte MathĂ©matique s'en va âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 4a · la feuille : de « .cm2-shell{ » jusqu'au @media 620 qui ferme le bloc
a = U.find('#ami-union .src-tri .cm2-shell, #ami-union .src-tri .cm2-shell *{box-sizing:border-box}'); assert a > 0
b = U.find("@media(max-width:620px){.cm2-row2{grid-template-columns:1fr}.cm2-tab{flex:1 1 100%;text-align:center}\n #ami-union .src-tri .cm2-palier{grid-template-columns:4.4rem 1fr;gap:.6rem}}", a); assert b > a
b = U.find('}}', b) + 2
vieille_css = U[a:b]; assert vieille_css.count('cm2') > 100
U = U[:a] + '/* (lâancienne feuille cm2 vivait ici â remplacĂ©e par #ami-loi-instrument, 19/09) */' + U[b:]; J.append(f'ancienne feuille cm2 ĂŽtĂ©e ({len(vieille_css)} car.)')
# 4b · le script (sceau, simulateurâŠ)
m = re.search(r"<script>\ntry\{\n\n\(function\(\)\{\n var R=document\.querySelector\('#ami-union \.src-tri'\); if\(!R\)return;\n var \$=function\(id\)\{return R\.querySelector\('#'\+id\)\};.*?</script>", U, re.S); assert m and m.group(0).count('cm2') > 40
U = U[:m.start()] + U[m.end():]; J.append('ancien script cm2 Îté')
# 4c · le markup de la Fractale : de la sub-nav à la fin du cm2-shell
a = U.find('<div class="tri-p" data-c="phy" id="tp-phy">'); assert a > 0
band = bloc(U, U.find('<div class="tri-band">', a), 'div')
s1 = U.find('<div class="sub-nav">', a); s2 = U.find('<!-- ââââââââââ PSYCHIQUE ââââââââââ -->', a); assert s1 > a and s2 > s1
vieux = U[s1:s2]; assert vieux.count('cm2-sec') == 15 and vieux.rstrip().endswith('</div>')
# la fermeture du tri-p est le dernier </div> du morceau
fin = vieux.rstrip()[:-6]
NAV = ('<div class="sub-nav">\n' + '\n'.join(f' <button class="sub-b{" on" if i == 0 else ""}" type="button" data-s="phy{i+1}">{lab}</button>' for i, (k, lab) in enumerate(tabs))
+ '\n <button class="sub-b" type="button" data-s="phy5">đč L\'Instrument</button>\n </div>\n')
SHELL = ' <div class="cm2-shell" id="cm2Shell">\n' + ''.join(f'<div class="sub-p{" on" if i == 0 else ""}" id="sp-phy{i+1}">\n{p}\n</div>\n' for i, (k, p) in enumerate(panels)) + ' </div>\n'
INST = (' <div class="sub-p ami-instrument" id="sp-phy5">\n'
' <p class="cm2-sub" style="text-align:center;margin:.4rem auto 1rem;max-width:640px">' + dek + '</p>\n'
+ statut + '\n' + phi + '\n' + cards[0] + '\n' + cards[1] + '\n </div>\n')
neuf = ' ' + hero + '\n ' + NAV + SHELL + INST + '</div>'
U = U[:s1] + neuf + U[s2:]; J.append('Fractale : sub-nav 5 entrĂ©es, 4 panneaux V9 dans le cm2-shell, lâInstrument en 5e')
assert U.count('id="cm2Shell"') == 1 and U.count('id="phiShell"') == 1
# ââ 5 · feuille et scripts âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('</head>', FEUILLE + '</head>', 1, 'feuille ami-loi-instrument')
MIROIR = r'''
<script id="ami-loi-miroir">
try{ (function(){ /* les compteurs des trois verbes recopient ceux de l'instrument (phiStatN, phiStatP) */
function miroir(){ var m={uniJalN:'phiStatN', uniJalP:'phiStatP'}; Object.keys(m).forEach(function(k){ var s=document.getElementById(m[k]), d=document.getElementById(k); if(s&&d){ var v=(s.textContent||'').trim(); if(v) d.textContent=v; } }); }
miroir(); ['phiStatN','phiStatP'].forEach(function(id){ var el=document.getElementById(id); if(el&&window.MutationObserver) new MutationObserver(miroir).observe(el,{childList:true,characterData:true,subtree:true}); });
/* la sub-nav de la Fractale réveille le canvas et les piles collantes */
document.querySelectorAll('#tp-phy .sub-b').forEach(function(b){ b.addEventListener('click',function(){ try{ window.dispatchEvent(new Event('resize')); }catch(e){} }); });
})(); }catch(e){}
</script>
'''
SCR = ('\n<script id="ami-cm2-v9">\ntry{\n' + scripts[0] + '\n}catch(e){ /* un volume absorbĂ© ne doit jamais empĂȘcher les autres de fonctionner */ }\n</script>\n'
'<script id="ami-phi-v9">\ntry{\n' + scripts[1] + '\n}catch(e){ /* idem */ }\n</script>\n' + MIROIR)
# juste aprĂšs le script de la tri (sub-b), avant le canton suivant
anc = "}catch(e){ /* un volume absorbĂ© ne doit jamais empĂȘcher les autres de fonctionner */ }\n</script>\n</div>\n</div>\n\n<!-- âââ T4"
assert U.count(anc) == 1, U.count(anc)
U = U.replace(anc, "}catch(e){ /* un volume absorbĂ© ne doit jamais empĂȘcher les autres de fonctionner */ }\n</script>\n" + SCR + "</div>\n</div>\n\n<!-- âââ T4"); J.append('scripts cm2 + phi V9 + miroir posĂ©s en fin de t3')
# les couleurs en ligne du markup V9 (paliers, rĂ©gimes) â pierres
a = U.find('<div class="tri-p" data-c="phy" id="tp-phy">'); b = U.find('<!-- ââââââââââ PSYCHIQUE ââââââââââ -->', a)
morceau = U[a:b]
parts = re.split(r'(<script\b.*?</script>)', morceau, flags=re.S); n = 0
for i in range(0, len(parts), 2):
def inl(m):
global n
s = m.group(1)
def h2(mm):
global n
hx = mm.group(0).lower()
if hx in HEX: n += 1; return 'var(%s)' % HEX[hx]
return mm.group(0)
return 'style="' + re.sub(r'#[0-9a-fA-F]{3,6}\b', h2, s) + '"'
parts[i] = re.sub(r'style="([^"]*)"', inl, parts[i])
morceau2 = ''.join(parts); U = U[:a] + morceau2 + U[b:]; J.append(f'couleurs en ligne â pierres Ă{n}')
# version
rep('<meta name="ami:version" content="V24">', '<meta name="ami:version" content="V27">', 1, 'meta ami:version V27 (la meta disait encore V24)')
U = re.sub(r'(data-ami="version">)V24<', r'\1V27<', U)
rep("body{margin:0;padding:22px;background:var(--noir);min-height:100vh}", "/* (style de démo body{padding:22px} Îté le 19/09) */", 1, 'style de démo body{padding:22px} de la tri Îté')
U = re.sub(r'(<meta name="ami:date" content=")[^"]*"', r'\g<1>2026-09-19"', U); assert 'content="2026-09-19"' in U
# ââ 6 · đ : un rĂ©sultat ouvre TOUS les niveaux d'onglets (onglet â canton tri â sous-onglet ; volets .pane des Lois/Contrats) ââ
OLD_SAUTE = """ if(e.k){ var b=R.querySelector((function(k){ return '.tab-btn-full[data-c="'+k+'"]'; })(e.k)); if(b) b.click(); }
var d=e.el.closest('details');"""
NEW_SAUTE = """ if(e.k){ var b=R.querySelector((function(k){ return '.tab-btn-full[data-c="'+k+'"]'; })(e.k)); if(b) b.click(); }
/* 19/09 : les niveaux en dessous â volet .pane (Lois, Contrats), canton .tri-p (Chartes), sous-onglet .sub-p (FractaleâŠ) */
(function(){ var anc=e.el.parentElement, clics=[], b2;
while(anc && anc!==R){
if(anc.classList.contains('pane')&&anc.dataset.c){ var tc=anc.closest('.tab-content'); b2=(tc&&tc.querySelector('.tb[data-c="'+anc.dataset.c+'"]'))||(tc&&R.querySelector('#subHost .sub-slot[data-c="'+tc.dataset.c+'"] .tb[data-c="'+anc.dataset.c+'"]'))||R.querySelector('.tb[data-c="'+anc.dataset.c+'"]'); if(b2) clics.push(b2); }
if(anc.classList.contains('tri-p')&&anc.dataset.c){ b2=R.querySelector('.tri-b[data-c="'+anc.dataset.c+'"]'); if(b2) clics.push(b2); }
if(anc.classList.contains('sub-p')&&anc.id){ var tp=anc.closest('.tri-p'); b2=tp&&tp.querySelector('.sub-b[data-s="'+anc.id.replace('sp-','')+'"]'); if(b2) clics.push(b2); }
anc=anc.parentElement; }
clics.reverse().forEach(function(b3){ if(!b3.classList.contains('on')) b3.click(); }); })();
var d=e.el.closest('details');"""
rep(OLD_SAUTE, NEW_SAUTE, 1, 'đ ouvre volet / canton / sous-onglet')
rep("p.closest('script,style,svg,#ami-rech,.toc-panel,.ami-barre,.ami-pied,.emporter')", "p.closest('script,style,svg,#ami-rech,.toc-panel,.ami-barre,.ami-pied,.emporter,.tabs,.tri-nav,.sub-nav,.sub-bar-host,.tab-btn-full,button')", 1, 'đ nâindexe plus les barres dâonglets ni les boutons (les onglets des Lois vivaient dans un hĂŽte cachĂ© â rĂ©sultat invisible)')
open(OUT, 'w', encoding='utf-8').write(U)
print('\n'.join('â ' + x for x in J))
p22_loi_mutations.py python3 p22_loi_mutations.py page.html48 lignes
0.3 : 23 mutations Ă 3 largeurs sur la Fractale (sceau, tore, curseurs, miroir, đ sur 3 niveaux, plancher, dĂ©bordement, palette).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P22 · 0.3 L'Union du TouT V27 â la mutation du canton Fractale (Charte MathĂ©matique V9 + l'Instrument) : on presse chaque
commande et on regarde si la page a changé comme promis. Usage : python3 p22_loi_mutations.py uni.html"""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'uni_p22.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (320, 390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1200); E = lambda js: pg.evaluate(js)
print(f'=== {W} px ===')
ok('meta ami:version V27, ami:date ISO 2026-09-19', E("()=>document.querySelector('meta[name=\"ami:version\"]').content==='V27' && document.querySelector('meta[name=\"ami:date\"]').content==='2026-09-19'"))
ok('body sans padding (style de démo Îté)', E("()=>getComputedStyle(document.body).padding==='0px'"))
ok('un seul h1', E("()=>document.querySelectorAll('h1').length===1"))
ok('lâancienne Charte MathĂ©matique est partie (15 accordĂ©ons cm2-sec)', E("()=>document.querySelectorAll('#tp-phy .cm2-sec').length===0 && !document.body.textContent.includes('â
· Fractale & Constante Génératrice')"))
E("()=>document.querySelector('.tab-btn-full[data-c=t3]').click()"); pg.wait_for_timeout(250)
E("()=>document.querySelector('.tri-b[data-c=phy]').click()"); pg.wait_for_timeout(250)
ok('Fractale : 5 sous-onglets (â đ đ đ± đč), le premier ouvert', E("()=>{const b=[...document.querySelectorAll('#tp-phy .sub-b')];return b.length===5 && b[0].classList.contains('on') && b[4].textContent.includes('L\\'Instrument') && document.querySelectorAll('#tp-phy .sub-p').length===5 && getComputedStyle(document.getElementById('sp-phy1')).display==='block' && getComputedStyle(document.getElementById('sp-phy5')).display==='none'}"))
ok('hero : « La Charte Mathématique » en h2, kicker V9 · Alignement RepÚre', E("()=>{const h=document.querySelector('#tp-phy .cm2-hero h2.cm2-h1');return h && h.textContent==='La Charte Mathématique' && document.querySelector('#tp-phy .cm2-kicker').textContent.includes('V9')}"))
ok('la V9 est lĂ : Ίâ = ÎI / (ÎI + ÎE), les 4 panneaux cm2', E("()=>document.querySelectorAll('#cm2Shell .cm2-panel').length===4 && /ÎI \\/ \\(ÎI \\+ ÎE\\)/.test(document.getElementById('cm2Shell').textContent)"))
ok('pas de pied ni de bande de la coque V9 (uni-pied, uni-bande, cm2-foot)', E("()=>!document.querySelector('.uni-pied,.uni-bande,.uni-couv,.cm2-foot,.cm2-tabs')"))
E("()=>document.querySelector('#tp-phy .sub-b[data-s=phy3]').click()"); pg.wait_for_timeout(200)
ok('đ sceau : refuse sans critĂšre de rĂ©futation', E("()=>{document.getElementById('cm2Objet').value='test';document.getElementById('cm2Val').value='1';document.getElementById('cm2Pred').value='p';document.getElementById('cm2Refut').value='';document.getElementById('cm2Gen').click();return /rĂ©futation/.test(document.getElementById('cm2Status').textContent)}"))
ok('đ sceau : scelle avec un critĂšre (FNV-1a)', E("()=>{document.getElementById('cm2Refut').value='si la valeur descend sous 0,5 avant Ă©chĂ©ance';document.getElementById('cm2Gen').click();return /ScellĂ©/.test(document.getElementById('cm2Status').textContent) && /SCEAU\\s+: [0-9A-F]{8}/.test(document.getElementById('cm2Out').textContent)}"))
E("()=>document.querySelector('#tp-phy .sub-b[data-s=phy2]').click()"); pg.wait_for_timeout(300)
ok('đ le tore se dessine (canvas peint) et tient dans sa colonne', E("()=>{const c=document.getElementById('cm2Cv');const d=c.getContext('2d').getImageData(0,0,c.width,c.height).data;let n=0;for(let i=3;i<d.length;i+=4) if(d[i]) n++;const r=c.getBoundingClientRect();return n>20000 && r.right<=innerWidth && r.width>150}"))
ok('đ un curseur change la lecture', E("()=>{const s=document.getElementById('cm2TC');const a=document.getElementById('cm2TCv').textContent;s.value=String(Math.min(parseFloat(s.max),parseFloat(s.value)+0.5));s.dispatchEvent(new Event('input',{bubbles:true}));return document.getElementById('cm2TCv').textContent!==a}"))
E("()=>document.querySelector('#tp-phy .sub-b[data-s=phy5]').click()"); pg.wait_for_timeout(400)
ok('đč lâInstrument : 21 curseurs de Ί, les trois verbes, 2 cartes (Pont, RepĂšre)', E("()=>document.querySelectorAll('#phiShell input[type=range]').length===21 && document.querySelectorAll('#sp-phy5 .uni-v').length===3 && document.querySelectorAll('#sp-phy5 .ti-card').length===2"))
ok('đč miroir : les jalons recopient les compteurs de lâinstrument', E("()=>document.getElementById('uniJalN').textContent===document.getElementById('phiStatN').textContent"))
ok('đč le pied interne ne porte plus la licence (elle est au pied de lâUnion)', E("()=>{const f=document.querySelector('#phiShell .phi-foot');return f && !/CC BY-SA/.test(f.textContent) && /navigateur/.test(f.textContent) && document.querySelector('.ami-pied')}"))
ok('pas de débordement dans la Fractale (tous sous-onglets)', E("()=>{const w=document.documentElement.clientWidth;let bad=0;document.querySelectorAll('#tp-phy .sub-p').forEach(p=>p.style.display='block');document.querySelectorAll('#tp-phy *').forEach(el=>{if(el.closest('svg'))return;let a=el.parentElement,roule=false;while(a&&a!==document.body){const o=getComputedStyle(a).overflowX;if(o==='auto'||o==='scroll'){roule=true;break}a=a.parentElement}if(roule)return;const r=el.getBoundingClientRect();if(r.width<1)return;if(r.right>w+1||r.left<-1)bad++});document.querySelectorAll('#tp-phy .sub-p').forEach(p=>p.style.display='');return bad===0 && document.documentElement.scrollWidth<=w}"))
ok('plancher 11,5 px tenu dans la Fractale', E("()=>{let n=0;document.querySelectorAll('#tp-phy .sub-p').forEach(p=>p.style.display='block');document.querySelectorAll('#tp-phy *').forEach(el=>{if(el.closest('script,style,svg'))return;let t=false;for(const c of el.childNodes) if(c.nodeType===3&&c.textContent.trim()) t=true;if(t&&parseFloat(getComputedStyle(el).fontSize)<11.5) n++});document.querySelectorAll('#tp-phy .sub-p').forEach(p=>p.style.display='');return n===0}"))
ok('aucune var(--x) sans déclaration', E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}for(const el of document.querySelectorAll('[style]')){for(const m of el.getAttribute('style').matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>!decl.has(v)&&!['--rc','--vc','--cc'].includes(v)).length===0}"))
ok('palette : plus de #c8a84a (vieil or) dans la feuille de la Loi', E("()=>!/#c8a84a|rgba\\(200,168,74|#e6d090|#9b65d5/i.test(document.getElementById('ami-loi-instrument').textContent)"))
# đ : trois niveaux
for q, i, attendu in (('dĂ©nominateur ÎE', 1, "t2|t2"), ('Pont instrumental', 0, "t3|phy|sp-phy5"), ('Charte Psychique', 0, "t3")):
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', q); pg.wait_for_timeout(450)
E(f"()=>document.querySelectorAll('#rc-l .rc-r')[{i}].click()"); pg.wait_for_timeout(1100)
ok(f'đ « {q} » ouvre tous les niveaux et montre le passage', E("()=>{const t=document.querySelector('.ami-trouve');if(!t)return false;const r=t.getBoundingClientRect();return r.width>0&&r.height>0&&r.top>=0&&r.top<innerHeight}"))
ok('0 erreur JS', not errs, str(errs[:3])); pg.close()
b.close()
n = sum(1 for r in R if r[1]); print(f'\n{n} / {len(R)} réussis')
if n < len(R): print('â ratĂ©s :', [r[0] for r in R if not r[1]])
p1_cosmo.py python3 p1_cosmo.py entree.html sortie.html106 lignes
CosmoLogie P1 : title/metas, une (Volume 3.3 · le miroir), un seul h1, onclick â data-act + dĂ©lĂ©guĂ©, renvoi Piratage, U+F105 â âș, orthographes, C9 ê© C12 đȘ, MapMonde â ami-karma.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · 3.3 CosmoLogie â l'identitĂ©, le texte, le vivant (audit du 19/09 : A·1 A·9 A·10 A·11 A·12 ; arbitrages B·1 B·2 B·5 C·1).
- <title>, meta ami:volume 3.3 · ami:version V9 · ami:date ; la une : kicker « Volume 3.3 · Tu Sais Lire la Culture », le miroir gardé
en une ligne (« la science mesure, le mythe raconte »), đ pour le volume ; l'ancienne mention du bas s'en va avec le pied.
- Un seul h1 : les 40 h1 des chapitres â h2.ami-h1 (mĂȘme apparence, posĂ©e en P2).
- 96 onclick= â data-act= + UN Ă©couteur dĂ©lĂ©guĂ© (Charte § LI) ; les onclick construits par script restent.
- « voir l'Ă©pisode transversal Ă venir » â lien vers đŽââ ïž l'Algorithme du Piratage (#pir-intro, onglet Emprise) : le chapitre existe.
- U+F105 (glyphe privĂ© d'une police absente, 556Ă) â « âș ». ⥠en une graphie. Orthographes arrĂȘtĂ©es (texte seulement).
- C·1 : les 13 Champs syncrĂ©tiques au canon : C9 ê©, C12 đȘ.
- A·12 : l'iframe MapMonde (fichier local mort) â renvoi vers agdistys.fr/ami-karma (+ la source agdistys.fr/histoire).
Usage : python3 p1_cosmo.py entree.html sortie.html"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · identitĂ© ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<title>đ CosmoLogie â La Bible 2.0 · La MyThoLogie Historique</title>',
'<title>3.3 CosmoLogie â Tu Sais Lire la Culture · Protocole de ConScience aMi</title>\n<meta name="ami:volume" content="3.3"><meta name="ami:version" content="V10"><meta name="ami:date" content="2026-09-19">', 1, 'title + metas')
rep('<div class="vol-label">Protocole de Conscience · 7.0 â 4.3 · Page miroir de la CosmoGonie 6.0 â 3.3</div>',
'<div class="vol-label">Protocole de ConScience aMi · Volume 3.3 · Tu Sais Lire la Culture</div>', 1, 'kicker de la une (B·1)')
rep('<div class="couv-attrib">Méthode Ouroboros · Tomographie Documentaire · ChronoLogique</div>',
'<div class="couv-attrib">MĂ©thode Ouroboros · Tomographie Documentaire · ChronoLogique</div>\n <p class="couv-miroir">đ Page miroir de <strong>4.3 CosmoGonie</strong> â la science mesure, le mythe raconte ce qui s\'est dit avant qu\'on puisse mesurer.</p>', 1, 'le miroir, en une ligne (B·1)')
# ââ 2 · un seul h1 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
n0 = len(re.findall(r'<h1\b', h))
h, n = re.subn(r'<h1 id="([^"]+)" class="anchor"( style="[^"]*")?>(.*?)</h1>', r'<h2 id="\1" class="anchor ami-h1"\2>\3</h2>', h, flags=re.S)
J.append(f'h1 des chapitres â h2.ami-h1 Ă{n} (reste {len(re.findall(chr(60)+"h1", h))} h1 : la une)')
assert len(re.findall(r'<h1\b', h)) == 1, len(re.findall(r'<h1\b', h))
# ââ 3 · onclick â data-act + dĂ©lĂ©guĂ© âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
parts = re.split(r'(<script\b.*?</script>)', h, flags=re.S); n = 0
for i in range(0, len(parts), 2):
parts[i], k = re.subn(r'\sonclick="(\w+\([^"]*\))"', r' data-act="\1"', parts[i]); n += k
h = ''.join(parts); J.append(f'onclick â data-act Ă{n} (hors scripts)'); assert n == 93, n # 96 dans le fichier brut, dont 3 dans les scripts (construits)
DELEGUE = r'''
<script id="ami-delegue">
/* ââ UN SEUL ĂCOUTEUR : data-act="fn('a',this)" â plus de fonction globale exigĂ©e par l'attribut (Charte § LI) ââ */
(function(){'use strict';
function arg(s, el){ s=s.trim();
if(s==='this') return el; if(s==='null') return null; if(/^'.*'$/.test(s)||/^".*"$/.test(s)) return s.slice(1,-1);
var m=s.match(/^document\.querySelectorAll\(\s*'([^']*)'\s*\)\s*\[(\d+)\]$/); if(m) return document.querySelectorAll(m[1])[+m[2]]||null;
m=s.match(/^document\.querySelector\(\s*'([^']*)'\s*\)$/); if(m) return document.querySelector(m[1]);
if(/^-?\d+(\.\d+)?$/.test(s)) return +s; return s; }
function args(txt, el){ var out=[], cur='', q=null, d=0; for(var i=0;i<txt.length;i++){ var c=txt[i];
if(q){ cur+=c; if(c===q) q=null; continue; } if(c==="'"||c==='"'){ q=c; cur+=c; continue; }
if(c==='('||c==='[') d++; if(c===')'||c===']') d--; if(c===','&&d===0){ out.push(cur); cur=''; continue; } cur+=c; }
if(cur.trim()) out.push(cur); return out.map(function(a){ return arg(a, el); }); }
function agit(el, e){ var m=(el.getAttribute('data-act')||'').match(/^(\w+)\((.*)\)\s*;?\s*(return false;?)?\s*$/); if(!m) return;
var fn=window[m[1]]; if(typeof fn!=='function') return; if(m[3]||el.tagName==='A') e.preventDefault(); fn.apply(el, args(m[2], el)); }
document.addEventListener('click', function(e){ var el=e.target.closest&&e.target.closest('[data-act]'); if(el) agit(el, e); });
document.addEventListener('keydown', function(e){ if(e.key!=='Enter'&&e.key!==' ') return; var el=e.target.closest&&e.target.closest('[data-act][role=button],[data-act][tabindex]'); if(el&&el.tagName!=='BUTTON'&&el.tagName!=='A'){ e.preventDefault(); agit(el, e); } });
})();
</script>
<script id="ami-va-js">
/* amiVa(chapitre, ancre) : ouvre l'onglet et le chapitre, puis descend Ă l'ancre â pour les renvois internes */
window.amiVa=function(id, anchor){ if(window.switchTab) switchTab(null, id); setTimeout(function(){ var el=anchor&&document.getElementById(anchor); if(!el) return; var pile=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--pile'))||parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0; var tb=document.getElementById('tabBar'); var y=el.getBoundingClientRect().top+window.pageYOffset-pile-(tb?tb.offsetHeight:0)-12; window.scrollTo({top:Math.max(0,y),behavior:'smooth'}); },120); };
</script>
'''
rep('<script>\n/* ===== TABS ===== */', DELEGUE + '<script>\n/* ===== TABS ===== */', 1, 'écouteur délégué + amiVa')
# ââ 4 · le renvoi « Ă venir » vers un chapitre qui existe âââââââââââââââââââââââââââââââââââââââââââââââââ
rep("la parole vivante en loi pénale (voir l'épisode transversal à venir).",
"la parole vivante en loi pĂ©nale (voir l'Ă©pisode <a href=\"#pir-intro\" data-act=\"amiVa('spirale','pir-intro')\">đŽââ ïž l'Algorithme du Piratage Religieux</a>).", 1, 'renvoi « Ă venir » â lien vers le chapitre (B·5)')
# ââ 5 · le glyphe privĂ©, âĄ, les orthographes (texte seulement) ââââââââââââââââââââââââââââââââââââââââââ
n = h.count('ï
'); h = h.replace('ï
', 'âș'); J.append(f'U+F105 â âș Ă{n}')
ORTH = [(r'\bconscience\b', 'ConScience'), (r'\bConscience\b', 'ConScience'), (r'\btransmission\b', 'TransMission'), (r'\bTransmission\b', 'TransMission'),
(r'\bcosmogonie\b', 'CosmoGonie'), (r'\bCosmogonie\b', 'CosmoGonie'), (r'\bcosmologie\b', 'CosmoLogie'), (r'\bCosmologie\b', 'CosmoLogie'),
(r'\bPardonnance\b', 'ParDonnance'), (r'\bAlgorythme\b', 'AlgoRythme'), (r'\bréflexion\b', 'RéfleXion'), (r'\bRéflexion\b', 'RéfleXion')]
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<[^>]+>)', h, flags=re.S); tot = {}
for i in range(0, len(parts), 2):
t = parts[i]
for rx, to in ORTH:
t, k = re.subn(rx, to, t)
if k: tot[to] = tot.get(to, 0) + k
t, k = re.subn('âĄ(?!ïž)', 'âĄïž', t)
if k: tot['âĄïž'] = tot.get('âĄïž', 0) + k
parts[i] = t
h = ''.join(parts); J.append(f'orthographes arrĂȘtĂ©es (texte seulement) {tot}')
# ââ 6 · C·1 : les 13 Champs syncrĂ©tiques au canon âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep("9:'đ±',10:'đ',11:'đ„',12:'âš',13:'đ'", "9:'ê©',10:'đ',11:'đ„',12:'đȘ',13:'đ'", 2, 'C9 ê© Â· C12 đȘ (canon du 9/09)')
# ââ 7 · A·12 : la MapMonde vit dans le volume Karma âââââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<div style="border:1px solid rgba\(200,168,74,\.25\);border-radius:10px;overflow:hidden;margin:1\.4rem 0;box-shadow:0 8px 40px rgba\(0,0,0,\.4\)">\s*<iframe src="MapMonde_Diagnostic_aMi\.html"[^>]*></iframe>\s*</div>\s*<p style="text-align:center"><a href="MapMonde_Diagnostic_aMi\.html"[^>]*>.*?</a></p>', h, re.S)
assert m, 'cadre MapMonde'
h = h[:m.start()] + '''<div class="ami-renvoi-carte">
<p>đ§ <strong>La MapMonde â Diagnostic du Vivant</strong> vit dans son volume : <strong>6.0 Karma</strong>.</p>
<p><a class="btn-ami" href="https://agdistys.fr/ami-karma" target="_blank" rel="noopener">đ Ouvrir la MapMonde (6.0 Karma) â</a> <a class="btn-ami" href="https://agdistys.fr/histoire" target="_blank" rel="noopener">đ L'article-source : Histoire â</a></p>
</div>''' + h[m.end():]; J.append('iframe MapMonde (fichier local) â renvois agdistys.fr/ami-karma + /histoire')
rep("La carte s'ouvre dans son propre cadre ci-dessous. Si elle met un instant Ă charger, c'est qu'elle appelle son fond cartographique â laisse-lui une seconde. Version prototype : les points s'enrichiront au fil des relectures sourcĂ©es.",
"La carte vit dans le volume 6.0 Karma (lien ci-dessous). Version prototype : les points s'enrichiront au fil des relectures sourcées.", 1, 'texte du callout MapMonde')
# ââ 8 · un onglet ou un sous-onglet cliquĂ© ramenait « tout en haut » (Diane, 19/09 15h) : la page lisait offsetTop de la barre
# COLLĂE â c'est sa place rendue, pas sa place dans le flux (et dans le cadre Webnode, celle du pont). On lit la place NATURELLE
# (la bande des registres, juste avant la pile) et on se pose juste dessous la barre du site.
rep(" setTimeout(function(){ var tb=document.getElementById('tabBar'); if(tb) window.scrollTo(0, tb.offsetTop - 1); }, 30);",
" setTimeout(function(){ var b=document.querySelector('#ami-cosmologie .bande-registres'), y=0, e=b; if(!b||window.__amiSansSaut) return; while(e){ y+=e.offsetTop; e=e.offsetParent; } y+=b.offsetHeight; var nav=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0; if(window.pageYOffset > y-nav) window.scrollTo(0, Math.max(0, y-nav)); }, 30);", 1, 'onglet cliqué : on reste sur la pile (place naturelle), plus de saut en haut')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p2_cosmo.py python3 p2_cosmo.py entree.html sortie.html158 lignes
CosmoLogie P2 : variables aux noms de la Charte, racine normalisée, reduced-motion, onglets (forme gardée, 4 par rangée, tirets conditionnels), émojis .e, tableaux .tw, grilles.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · 3.3 CosmoLogie â la forme (audit A·6 A·7 A·8 ; arbitrage : les onglets gardent leur forme).
- Les onglets replient dĂ©jĂ (2 rangĂ©es Ă 390, Ă©moji au-dessus) : libellĂ© au plancher (.62 â .72 rem), plus de chevauchement
(« LECTURE & INTERPRĂTATIONS » mordait sur « SOURCES ») : mots coupables coupĂ©s proprement ; cibles â„ 44 px ; pastilles .74 â .78.
- reduced-motion en tĂȘte (ambFlux/albedoBloom/couv/fadeIn se taisent) ; la une n'impose plus 88vh.
- Ămojis droits (jamais en italique) : enveloppĂ©s .e (texte, hors scripts/styles) ; 26 tableaux en .tw (rouleau par rĂšgle) ;
grilles : le dernier prend la colonne pleine s'il est seul ; h2.ami-h1 = l'apparence du h1 de chapitre ; .couv-miroir ; renvoi MapMonde.
- La palette : les noms propres Ă la page â les noms de la Charte (--bg â --encre, --text â --nacre, --x-pale â --x-clâŠ) et la racine
normalisĂ©e `:root{\\nâŠ\\n}` pour P3.
Usage : python3 p2_cosmo.py entree.html sortie.html"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · la palette : les noms de la Charte ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
REN = [('--text-dim', '--gris'), ('--text', '--nacre2'), ('--bg2', '--voile'), ('--bg3', '--voile'), ('--bg', '--encre'),
('--vert-pale', '--vert-cl'), ('--rubis-pale', '--rubis-cl'), ('--bleu-pale', '--bleu-cl'), ('--fuchsia-pale', '--fuchsia-cl'), ('--turquoise-pale', '--turquoise-cl')]
tot = {}
for a, b in REN:
h, k = re.subn(re.escape(a) + r'(?![a-z0-9-])', b, h); tot[a] = k
J.append(f'variables renommées {tot}')
# --text (#e8e4d8) et --nacre (#f2ede2) : une seule nacre
rep('--nacre2', '--nacre', h.count('--nacre2'), '--text â --nacre (une seule nacre)')
m = re.search(r':root \{\n(.*?)\n\}', h, re.S); assert m
root = m.group(1)
root = re.sub(r'--nacre:#[0-9a-f]{6};', '', root, count=1) # deux dĂ©clarations â une (celle de --text, renommĂ©e, reste)
root = root.replace('--voile:#14141f;--voile:#1a1a2e;', '--voile:#14141f;')
props = [re.sub(r'\s+', '', l.strip()) for l in root.split('\n') if l.strip()]
assert root.count('--nacre:') == 1 and root.count('--voile:') == 1, root
h = h.replace(m.group(0), ':root{\n ' + ' '.join(props) + '\n}', 1); J.append('racine normalisée pour P3')
# ââ 2 · reduced-motion, la une ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
CSS = '''
<style id="ami-forme-p2">
/* ââ P2 · 19 sept 2026 â la forme Ă la Charte ââ */
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
.couverture{min-height:0;padding:3.2rem 1.2rem 2.4rem}
.couv-miroir{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--gris);max-width:36rem;margin:.2rem auto 1rem;position:relative;z-index:2;line-height:1.6}
.couv-miroir strong{color:var(--or-pale);font-weight:500}
/* les onglets : mĂȘme forme, libellĂ© au plancher, plus de chevauchement */
.tab-bar-full{justify-content:center}
.tab-btn{font-size:.72rem;min-width:0;padding:.7rem .25rem;letter-spacing:.02em;overflow-wrap:break-word;hyphens:auto;-webkit-hyphens:auto;min-height:44px;line-height:1.15}
/* 7 & 3 : Avatars · Arbre Culturel · Amour & Union â « le monde d'aprĂšs » â sur la seconde rangĂ©e (Diane, 19/09) */
@media(min-width:900px){.tab-btn{flex:0 0 14.28%;max-width:14.28%}}
@media(min-width:521px) and (max-width:899px){.tab-btn{flex:0 0 20%;max-width:20%}}
@media(max-width:520px){.tab-btn{flex:1 1 25%;max-width:25%}}
@media(max-width:360px){.tab-btn{flex:1 1 33.33%;max-width:33.33%}}
.subtab-btn{font-size:.78rem;min-height:36px}
.subrow-lbl{font-size:.72rem}
/* le titre de chapitre (était un h1) */
h2.ami-h1{font-size:2.3rem;color:var(--or-pale);margin:0 0 1rem;font-style:normal}
/* émojis droits, jamais en italique */
.e,em .e,i .e,.episteme .e,.formule .e,.couv-axiome .e,h3 .e{font-style:normal}
/* tableaux : rouleau par rĂšgle */
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:1rem 0}
.tw table{margin:0}
/* grilles : le dernier prend la colonne pleine s'il est seul sur son rang */
.niveau-grid,.gen-grid,.atl-grid,.syn-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr))}
@media(min-width:480px) and (max-width:760px){.niveau-grid>*:last-child:nth-child(odd){grid-column:1/-1}} /* seulement quand il n'y a que deux colonnes (19/09 : Ă trois colonnes, la 3e carte prenait toute la largeur) */
/* cibles â„ 24 px */
button,.btn-ami,.som-secs a,.lect-switch button{min-height:32px}
.som-secs a{display:inline-block;padding:.2rem 0}
/* le renvoi vers la MapMonde (6.0 Karma) */
.ami-renvoi-carte{border:1px solid rgba(200,168,74,.25);border-radius:10px;padding:1.2rem 1rem;margin:1.4rem 0;text-align:center;background:rgba(255,255,255,.02)}
.ami-renvoi-carte p{margin:.4rem 0}
.ami-renvoi-carte .btn-ami{margin:.3rem .3rem;letter-spacing:.06em;text-transform:none;font-size:.86rem;font-weight:500}
</style>
'''
# les deux libellés longs des onglets se coupent à la syllabe (tirets conditionnels), jamais au hasard
rep('<span class="tab-icon">đ</span>Lecture & InterprĂ©tations</button>', '<span class="tab-icon">đ</span>Lecture & Inter­prĂ©­tations</button>', 1, 'InterprĂ©tations : coupe Ă la syllabe')
rep('<span class="tab-icon">đžïž</span>SyncrĂ©tismes ArchĂ©typaux</button>', '<span class="tab-icon">đžïž</span>SyncrĂ©­tismes ArchĂ©­typaux</button>', 1, 'SyncrĂ©tismes ArchĂ©typaux : coupe Ă la syllabe')
rep('</head>', CSS + '</head>', 1, 'feuille ami-forme-p2')
# ââ 3 · Ă©mojis en italique : on les enveloppe (texte seulement) ; tableaux en .tw ââââââââââââââââââââââââ
EMO = re.compile('([\U0001F300-\U0001FAFFâ-âżâŹ-⯿\U0001F900-\U0001F9FFêŠ -ê§ż]ïž?)')
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>|<svg\b.*?</svg>|<[^>]+>)', h, flags=re.S); n = 0
for i in range(0, len(parts), 2):
t2 = EMO.sub(r'<span class="e">\1</span>', parts[i]); n += t2.count('<span class="e">'); parts[i] = t2
h = ''.join(parts); J.append(f'Ă©mojis enveloppĂ©s .e Ă{n}')
parts = re.split(r'(<script\b.*?</script>|<style\b.*?</style>)', h, flags=re.S); n = 0
for i in range(0, len(parts), 2):
t2, a = re.subn(r'(<table\b)', r'<div class="tw">\1', parts[i]); t2, b = re.subn(r'(</table>)', r'\1</div>', t2); assert a == b; n += a; parts[i] = t2
h = ''.join(parts); J.append(f'tableaux en .tw Ă{n} (hors scripts)')
# ââ 4 · aucun sĂ©lecteur nu : la page ne repeint plus le site qui l'accueille (Diane, 19/09 : « il me change mes couleurs de texte ») ââ
# :where(#ami-cosmologie) garde la spécificité à zéro : la cascade de la page ne bouge pas.
m = re.search(r'<style\b[^>]*>(.*?)</style>', h, re.S); css = m.group(1); c0 = css
css = css.replace('html{scroll-behavior:smooth}', 'html:has(> body > #ami-cosmologie){scroll-behavior:smooth}', 1)
# le corps du DOCUMENT seul (la page entiĂšre ouverte telle quelle) : dans un bloc Webnode, la racine n\'est pas enfant direct du body â la rĂšgle se tait
css = re.sub(r'(?m)^body\{([^}]*)\}', lambda mm: 'body:has(> #ami-cosmologie){' + mm.group(1) + '}\n:where(#ami-cosmologie){' + mm.group(1).replace('min-height:100vh;','') + '}', css, count=1)
for sel in ['h1,h2,h3', 'h1', 'h2', 'h3', 'p', 'strong', 'em']:
scoped = ','.join(':where(#ami-cosmologie) ' + x for x in sel.split(','))
css, k = re.subn(r'(?m)^' + re.escape(sel) + r'\{', scoped + '{', css, count=1); assert k == 1, sel
assert css != c0
h = h.replace(m.group(0), m.group(0).replace(c0, css), 1)
rep('h4+.stree{margin-top:.2rem}', ':where(#ami-cosmologie) h4+.stree{margin-top:.2rem}', 1, 'h4+.stree scopé')
J.append('sĂ©lecteurs nus html/body/h1/h2/h3/p/strong/em â :where(#ami-cosmologie) (le site hĂŽte garde ses couleurs)')
# ââ 5 · les deux grandes images (l'Arbre Culturel, le Discernement) se ZOOMENT : un tap â plein Ă©cran, molette / pincement / glisser
ZOOM_CSS = """
/* ââ le zoom des grandes images (19/09) ââ */
.ami-zoomable{cursor:zoom-in}
.ami-zoom-b{display:inline-block;margin:.5rem auto 0;padding:.45rem 1rem;border:1px solid var(--or-somb);border-radius:999px;background:rgba(255,255,255,.04);color:var(--or);font-family:'Jost',sans-serif;font-size:.86rem;cursor:pointer;min-height:36px}
.ami-zoom-b:hover{background:rgba(255,255,255,.09);color:var(--or-pale)}
#ami-zoom{position:fixed;inset:0;z-index:2147483000;background:rgba(9,9,14,.97);display:none;overflow:hidden;touch-action:none;cursor:grab}
#ami-zoom.on{display:block}
#ami-zoom img{position:absolute;left:0;top:0;transform-origin:0 0;max-width:none;user-select:none;-webkit-user-drag:none}
#ami-zoom .zb{position:fixed;top:.8rem;right:.8rem;z-index:2;display:flex;gap:.5rem}
#ami-zoom .zb button{width:2.4rem;height:2.4rem;border-radius:50%;border:1px solid var(--or-somb);background:rgba(9,9,14,.92);color:var(--or);font-family:'Jost',sans-serif;font-size:1.1rem;cursor:pointer}
#ami-zoom .zb button:hover{color:var(--or-pale);background:rgba(255,255,255,.09)}
#ami-zoom .zi{position:fixed;left:50%;bottom:1rem;transform:translateX(-50%);color:var(--gris);font-family:'Jost',sans-serif;font-size:.8rem;background:rgba(9,9,14,.85);padding:.3rem .8rem;border-radius:999px;pointer-events:none}
"""
ZOOM_JS = r"""
<script id="ami-zoom-js">
/* ââ le zoom : une grande image se regarde de prĂšs â molette, pincement Ă deux doigts, glisser ; â ou Ăchap ââ */
(function(){'use strict';
var Z=document.createElement('div'); Z.id='ami-zoom'; Z.setAttribute('role','dialog'); Z.setAttribute('aria-label','Image en grand');
Z.innerHTML='<img alt=""><div class="zb"><button type="button" data-z="-" aria-label="RĂ©duire">â</button><button type="button" data-z="+" aria-label="Agrandir">+</button><button type="button" data-z="0" aria-label="Taille de dĂ©part">‹</button><button type="button" data-z="x" aria-label="Fermer">â</button></div><div class="zi">molette ou deux doigts pour zoomer · glisser pour se dĂ©placer · Ăchap pour fermer</div>';
document.body.appendChild(Z); var I=Z.querySelector('img'), s=1, x=0, y=0, base=1;
function pose(){ I.style.transform='translate('+x+'px,'+y+'px) scale('+s+')'; }
/* dans un cadre Webnode, la fenĂȘtre du document est tout le cadre : on mesure la boĂźte du zoom elle-mĂȘme (le pont la pose sur la fenĂȘtre visible) */
function dims(){ var r=Z.getBoundingClientRect(); return {W:r.width||innerWidth, H:r.height||innerHeight, L:Z.offsetLeft, T:Z.offsetTop}; } /* offsetTop : la place brute de la boßte (les événements sont en coordonnées brutes) */
function loc(e){ var d=dims(); return {x:e.clientX-d.L, y:e.clientY-d.T}; }
function cadre(){ var d=dims(), W=d.W, H=d.H; base=Math.min(W/I.naturalWidth,H/I.naturalHeight); s=base; x=(W-I.naturalWidth*s)/2; y=(H-I.naturalHeight*s)/2; pose(); }
function zoomA(f,cx,cy){ var ns=Math.max(base*0.5,Math.min(base*12,s*f)); x=cx-(cx-x)*(ns/s); y=cy-(cy-y)*(ns/s); s=ns; pose(); }
function ouvre(src,alt){ I.src=src; I.alt=alt||''; Z.classList.add('on'); document.documentElement.style.overflow='hidden'; if(I.complete&&I.naturalWidth) cadre(); else I.onload=cadre; setTimeout(cadre,150); setTimeout(cadre,500); /* dans un cadre Webnode, le pont pose la boĂźte sur la fenĂȘtre visible un instant plus tard : on recadre */ }
function ferme(){ Z.classList.remove('on'); document.documentElement.style.overflow=''; }
Z.querySelector('.zb').addEventListener('click',function(e){ var b=e.target.closest('button'); if(!b) return; var k=b.dataset.z; if(k==='x') ferme(); else if(k==='0') cadre(); else { var d=dims(); zoomA(k==='+'?1.5:1/1.5, d.W/2, d.H/2); } });
Z.addEventListener('wheel',function(e){ e.preventDefault(); var l=loc(e); zoomA(e.deltaY<0?1.15:1/1.15, l.x, l.y); },{passive:false});
var pts=new Map(), d0=0, s0=1, px=0, py=0;
Z.addEventListener('pointerdown',function(e){ if(e.target.closest('.zb')) return; Z.setPointerCapture(e.pointerId); var l=loc(e); pts.set(e.pointerId,l); if(pts.size===1){ px=l.x-x; py=l.y-y; Z.style.cursor='grabbing'; } if(pts.size===2){ var a=[...pts.values()]; d0=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y); s0=s; } });
Z.addEventListener('pointermove',function(e){ if(!pts.has(e.pointerId)) return; var l=loc(e); pts.set(e.pointerId,l);
if(pts.size===1){ x=l.x-px; y=l.y-py; pose(); }
else if(pts.size===2){ var a=[...pts.values()]; var d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y); var cx=(a[0].x+a[1].x)/2, cy=(a[0].y+a[1].y)/2; var f=(d/d0)*s0/s; zoomA(f,cx,cy); } });
function fin(e){ pts.delete(e.pointerId); if(!pts.size) Z.style.cursor='grab'; }
Z.addEventListener('pointerup',fin); Z.addEventListener('pointercancel',fin);
Z.addEventListener('dblclick',function(e){ if(!e.target.closest('.zb')){ var l=loc(e); zoomA(2,l.x,l.y); } });
document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&Z.classList.contains('on')) ferme(); });
window.addEventListener('resize',function(){ if(Z.classList.contains('on')) cadre(); });
document.querySelectorAll('#ami-cosmologie img[src^="data:image"]').forEach(function(img){ img.classList.add('ami-zoomable'); img.title='Cliquer pour zoomer';
var b=document.createElement('button'); b.type='button'; b.className='ami-zoom-b'; b.textContent='‹ Voir en grand · zoomer';
img.insertAdjacentElement('afterend', b); var go=function(){ ouvre(img.currentSrc||img.src, img.alt); }; img.addEventListener('click',go); b.addEventListener('click',go); });
})();
</script>
"""
h = h.replace('/* le renvoi vers la MapMonde (6.0 Karma) */', ZOOM_CSS + '\n/* le renvoi vers la MapMonde (6.0 Karma) */', 1)
rep('</body>', ZOOM_JS + '</body>', 1, 'zoom des grandes images (visualiseur plein écran)')
J.append('CSS du zoom posée')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p4_cosmo.py python3 p4_cosmo.py entree.html sortie.html314 lignes
CosmoLogie P4 : racine, barre/pied V16, bande, Emporter, boĂźte đ â° â, contraste, lecture continue, progression, LA PILE QUI SE REPLIE.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · 3.3 CosmoLogie â l'anatomie de Page (audit A·1 A·2 A·3 A·4 ; arbitrages B·2 B·3 C·3).
Racine #ami-cosmologie autour de la page ; barre V16 + #prog en tĂȘte (data-ici=ami-cosmologie, tiroir fermĂ©) ; bande des registres
aprĂšs la une ; â Emporter avant le pied ; pied V16 (sceau đ) Ă la place de la nav-footer (2 liens vers des fichiers locaux, morts en
ligne) ; scripts du composant (đ§ â â¶ zoom Emporter) ; progression sur le chapitre ouvert ; la boĂźte Ă outils đ â° â (recherche
générique qui ouvre l'onglet ET le chapitre ; sommaire bùti depuis les 10 onglets et leurs 33 chapitres) ; contraste élevé ;
LA PILE QUI SE REPLIE (C·3) : quand on descend dans un chapitre, les deux rangées d'onglets collées se replient en une ligne de 44 px
qui dit oĂč on est (« đș Mythes Religieux âș đ Le DĂ©luge ⎠») ; un tap la redĂ©ploie et remonte aux onglets ; remonter la redĂ©ploie.
Le â Sommaire dĂ©roulant par chapitre reste (B·3). Les onglets gardent leur forme (Diane, 19/09).
Usage : python3 p4_cosmo.py entree.html sortie.html"""
import re, sys, json
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open('../spec/barre_extrait.json'))
BOITE_CSS = open('../spec/boite_css.txt', encoding='utf-8').read().replace('#ami-rech{display:none!important}\n', '')
h = open(SRC, encoding='utf-8').read(); J = []
ROOT = '#ami-cosmologie'; RID = 'ami-cosmologie'
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · l'ancien pied (nav-footer + mention) s'en va âââââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<!-- NAV FOOTER -->\s*<div style="[^"]*">\s*<nav class="nav-footer">.*?</nav>\s*<p [^>]*>.*?</p>\s*</div>', h, re.S); assert m
h = h[:m.start()] + '<!--ami:emport-->' + h[m.end():]; J.append('nav-footer dâaoĂ»t ĂŽtĂ©e (0_Index_aMi.html, 4.2.DECRYPTAGE⊠: fichiers locaux morts ; la ForĂȘt de la barre porte les volumes)')
# ââ 2 · la racine, la barre, #prog, la bande, le pied âââââââââââââââââââââââââââââââââââââââââââââââââââââ
mb = re.search(r'<body[^>]*>\n?', h)
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
f'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="{RID}">')
assert nav.count('data-ici') == 1
h = h[:mb.end()] + d['prog'] + '\n' + nav + '\n<div id="' + RID + '">\n' + h[mb.end():]; J.append('barre V16 + #prog + racine ouverte')
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="https://agdistys.fr/ami-charte" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('</div>\n\n<!-- TAB BAR : UN MYTHE = UN ONGLET -->', '</div>\n' + bande + '\n<!-- TAB BAR : UN MYTHE = UN ONGLET -->', 1, 'bande des registres aprĂšs la une')
# les onglets ET les sous-onglets collent ensemble (Diane, 19/09 : « c'est sticky sauf les sous-onglets ») : une seule pile #amiPile
m2 = re.search(r'(<nav class="tab-bar-full" id="tabBar">.*?</nav>)\s*(<nav class="subtab-bar" id="subTabBar"></nav>)', h, re.S); assert m2
h = h[:m2.start()] + '<div id="amiPile">\n' + m2.group(1) + '\n' + m2.group(2) + '\n</div>' + h[m2.end():]; J.append('#amiPile : onglets + sous-onglets collés ensemble')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>', '<div class="ap-sceau" title="đ Le sceau du volume">đ</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', '<p class="ap-titre">CosmoLogie â dâHier Ă Aujourdâhui</p>')
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">3.3</span> â Tu Sais Lire la Culture</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">Le Mythe nâest pas un Mensonge.\nCâest une Trace Ă Savoir Lire.</p>")
assert 'đ Le sceau' in pied and 'CosmoLogie â' in pied and 'Volume <span' in pied and 'Trace Ă Savoir' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong> : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â dix onglets, trente-trois chapitres, lâAtlas retrouvĂ©, les 13 Champs syncrĂ©tiques, lecture continue, contraste, recherche.</p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V10</span> â arrĂȘtĂ©e au <span data-ami="date">19 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
rep('<!--ami:emport-->', emport + '\n</div><!-- /#' + RID + ' -->\n' + pied + '\n', 1, 'â Emporter · racine fermĂ©e · pied V16')
# ââ 3 · la feuille du composant (nettoyĂ©e comme pour 0.3/0.1/2.2) ââââââââââââââââââââââââââââââââââââââââ
css = d['css']
blocs = []; k = 0; n = len(css)
while k < n:
j = css.find('{', k)
if j < 0: break
sel = css[k:j]; depth = 1; e = j + 1
while e < n and depth:
if css[e] == '{': depth += 1
elif css[e] == '}': depth -= 1
e += 1
blocs.append((sel, css[j:e])); k = e
out = []; otees = 0
for sel, corps in blocs:
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts): otees += 1; continue
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
sel = sel[:len(sel) - len(sel.lstrip())] + ',\n'.join(keep)
out.append(sel + corps)
css = ''.join(out)
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', ROOT + r' \1', css)
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items() if not v.startswith('4px}'))
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5, nettoyĂ©s (P5 de 0.3) ââ */\n'
':root{' + rootvals + ';--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
f'{ROOT} .emporter{{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);border:1px solid var(--or-somb);border-radius:14px;text-align:center}}\n'
f"{ROOT} .emporter h2{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none;border:0;padding:0}}\n"
f"{ROOT} .emporter p{{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}}\n"
f"{ROOT} .emporter p strong{{color:var(--nacre);font-weight:500;font-size:inherit}}\n"
f"{ROOT} .emporter .emp-b{{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}}\n"
f"{ROOT} .emporter .emp-b button{{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:44px}}\n"
f"{ROOT} .emporter .emp-b button:hover{{background:rgba(255,255,255,.1);color:var(--or-pale)}}\n"
f"{ROOT} .emporter .emp-d{{font-size:.78rem;color:var(--gris)}}\n{ROOT} .emporter [data-ami]{{color:var(--or-pale);font-size:inherit}}\n"
f'{ROOT} .bande-registres{{margin:1.2rem auto 0}}\n'
f'{ROOT} #amiPile{{position:sticky;top:var(--nav-h,0px);z-index:100;background:var(--voile)}}\n{ROOT} .tab-bar-full{{position:static;top:auto}}\n'
+ css + '\n</style>\n')
assert '*{margin:0' not in feuille
rep('</head>', feuille + '</head>', 1, f'feuille du composant ({otees} rÚgles #tv/.tvx écartées)')
# ââ 4 · les scripts du composant âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
js = d['js_barre'].replace("document.getElementById('ami-charte')||document.body", f"document.getElementById('{RID}')||document.body")
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('#tabBar .tab-btn').forEach(function(o,i){ o.classList.toggle('active', i===0); });
d.querySelectorAll('.tab-section').forEach(function(p,i){ p.classList.toggle('active', i===0); });
var sb=d.querySelector('#subTabBar'); if(sb){ sb.innerHTML=''; sb.style.display='none'; }
var mini=d.querySelector('#ami-pile-mini'); if(mini) mini.remove(); d.querySelector('body').classList.remove('ami-repli');""")
assert "'.tab-section').forEach" in js
js = js.replace(""" if(a){ a.classList.add('ici');
deplie(true,false);
var p=N.querySelector('.porte[data-c="foret"]'); if(p) pose(p,true);
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}""", """ if(a){ a.classList.add('ici');
/* la page se marque, mais n'ouvre plus le tiroir toute seule (Diane, 17 sept) : un clic sur đł la ForĂȘt l'ouvre, Ă la bonne rangĂ©e */
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}"""); assert "n'ouvre plus le tiroir" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='3.3_COSMOLOGIE_aMi_'+(window.__amiV||'')+'.html';"); assert '3.3_COSMOLOGIE' in js
shim = f"""
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue (les 33 chapitres dĂ©roulĂ©s) ââ */
(function(){{'use strict';
var R=document.getElementById('{RID}'); if(!R) return; var memo=null;
/* 19/09 23h40 (Diane : « le bandeau freeze⊠ça me renvoie en haut ») : allumer OU Ă©teindre la lecture continue ne bouge pas ce qu'on lit â
on note le point sous les yeux (sous la pile collée : sondes à 40/55/70/85 %), et on lui rend sa hauteur aprÚs (scrollTo, que le pont du cadre connaßt) */
function ancre(){{ var cs=getComputedStyle(document.documentElement), vt=parseFloat(cs.getPropertyValue('--vue-top'))||0, vh=parseFloat(cs.getPropertyValue('--vue-h'))||window.innerHeight;
var pb=0, tb=document.getElementById('amiPile')||document.getElementById('ami-pile-mini'); if(tb){{ var rb=tb.getBoundingClientRect(); if(rb.bottom>0&&rb.bottom<vh*0.9) pb=rb.bottom; }} /* sous la pile collée (Mythes : 700 px) */
var el=null, sec=null, ks=[0.3,0.5,0.7,0.9,0.15];
for(var q=0;q<ks.length&&!sec;q++){{ el=document.elementFromPoint(window.innerWidth/2, vt+pb+(vh-pb)*ks[q]); sec=el&&el.closest('#ami-cosmologie .tab-section'); }}
return {{el:el, sec:sec, top:el?el.getBoundingClientRect().top:0, stop:sec?sec.getBoundingClientRect().top:0}}; }}
function rends(a){{ if(!a.el) return; function pose(){{ var d = a.el.isConnected ? a.el.getBoundingClientRect().top-a.top : (a.sec&&a.sec.isConnected ? a.sec.getBoundingClientRect().top-a.stop : 0); if(Math.abs(d)>1) window.scrollTo({{top:Math.max(0,window.pageYOffset+d), behavior:'instant'}}); }}
pose(); /* dans le cadre Webnode, l'hĂŽte ne remesure le cadre qu'aprĂšs coup (600 ms) : on repose le point lu quand il l'a fait */
if(document.documentElement.classList.contains('i-cadre-fil')) [200,700,1300,2000].forEach(function(t){{ setTimeout(pose,t); }}); }}
document.addEventListener('ami:lecture',function(e){{
var on=e.detail&&e.detail.actif;
if(on){{ memo=window.currentTab||'methode'; var a=ancre(); R.querySelectorAll('.tab-section').forEach(function(p){{ p.classList.add('active'); }}); R.classList.add('lecture'); rends(a); }}
else {{ R.classList.remove('lecture'); var a=ancre(); var cible=a.sec?a.sec.id.replace('tab-',''):memo;
window.__amiSansSaut=true; try{{ if(cible&&window.switchTab) switchTab(null, cible); }} finally {{ setTimeout(function(){{ window.__amiSansSaut=false; }},400); }}
rends(a); }}
}});
}})();
/* ââ LA BARRE DE PROGRESSION â elle mesure le chapitre ouvert ââ */
(function(){{'use strict';
var prog=document.getElementById('prog'), R=document.getElementById('{RID}'); if(!prog||!R) return;
function pile(){{ var n=document.querySelector('.ami-barre'); return n?Math.min(n.getBoundingClientRect().height,144):0; }}
function volet(){{ if(R.classList.contains('lecture')) return R; return R.querySelector('.tab-section.active')||R; }}
function avance(){{ var v=volet(), r=v.getBoundingClientRect(), y=window.pageYOffset, debut=r.top+y-pile(), fin=r.bottom+y-window.innerHeight, course=fin-debut,
p=course>4 ? (y-debut)/course : (r.bottom<=window.innerHeight?1:0); prog.style.width=(Math.max(0,Math.min(1,p))*100).toFixed(2)+'%'; }}
window.addEventListener('scroll',avance,{{passive:true}}); window.addEventListener('resize',avance);
document.addEventListener('click',function(e){{ if(e.target.closest&&e.target.closest('.tab-btn,.subtab-btn,#amiLecture,#amiPlein,[data-act]')) setTimeout(avance,90); }},true);
document.addEventListener('ami:lecture',function(){{ setTimeout(avance,90); }}); avance();
}})();
"""
contraste = f"""
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct ââ */
body.ami-contraste {ROOT},body.ami-contraste{{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;--or-somb:#b09030;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;--fuchsia:var(--grenat-ct);--fuchsia-cl:#ffb3dd;--turquoise:var(--amazonite-ct);--turquoise-cl:#9fe8e6}}
body.ami-contraste {ROOT}{{font-weight:400}}
body.ami-contraste {ROOT} em{{font-style:normal}}
</style>
"""
# ââ 5 · la boĂźte Ă outils : â â° đ + la pile qui se replie âââââââââââââââââââââââââââââââââââââââââââââââ
coins = '''
<button class="ami-coin h" id="amiHaut" type="button" hidden aria-label="Revenir en haut" title="â Revenir en haut">â</button>
<button class="ami-coin d3" id="ami-toc-toggle" type="button" aria-expanded="false" aria-controls="ami-toc-panel" aria-label="Sommaire de la page" title="â° Le sommaire de la page">â°</button>
<div id="ami-toc-panel" class="ami-toc" hidden role="dialog" aria-label="Sommaire"></div>
<button class="ami-coin r" id="amiRech" type="button" aria-label="Rechercher" title="Chercher dans le volume (Ctrl+F)"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></button>
<div id="ami-rech" role="dialog" aria-modal="true" aria-label="Rechercher dans le volume">
<div class="rc-b">
<div class="rc-h"><span class="rc-loupe"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></span>
<input type="search" id="rc-q" placeholder="Chercher dans les dix ongletsâŠ" autocomplete="off" spellcheck="false" aria-label="Chercher">
<span class="rc-n" id="rc-n"></span>
<button class="rc-x" type="button" id="rc-x" aria-label="Fermer">â</button></div>
<div class="rc-l" id="rc-l" role="listbox"></div>
</div>
</div>
'''
boite_js = r'''
<script id="ami-boite-js">
/* ââ LA BOĂTE Ă OUTILS : â Haut · â° Sommaire (10 onglets â 33 chapitres) · đ Recherche (indexe la racine, ouvre l'onglet ET le chapitre) ââ */
(function(){'use strict';
var R=document.getElementById('ami-cosmologie'); if(!R) return;
var H=document.getElementById('amiHaut');
if(H){ function suit(){ H.hidden = window.pageYOffset < 600; } window.addEventListener('scroll',suit,{passive:true}); suit();
H.addEventListener('click',function(){ window.scrollTo({top:0,behavior:'smooth'}); }); }
function groupeDe(id){ var G=window.GROUPS||{}; for(var k in G){ if(G[k].indexOf(id)>=0) return k; } return null; }
function nomGroupe(g){ var T=window.GROUP_TITLES||{}; return T[g]||g; }
function nomChap(id){ var M=window.TAB_META||{}; return M[id]?M[id][0]+' '+M[id][1]:id; }
/* la hauteur de ce qui restera collĂ© une fois arrivĂ© Ă yDoc : la barre + (la pile entiĂšre, ou la ligne « oĂč je suis » si la pile se sera repliĂ©e) */
function pile(yDoc){ var p=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0; var tb=document.getElementById('amiPile')||document.getElementById('tabBar'); if(!tb) return p;
var b=document.querySelector('#ami-cosmologie .bande-registres'), h0=0, e=b||tb; while(e){ h0+=e.offsetTop; e=e.offsetParent; } if(b) h0+=b.offsetHeight;
var repli = (yDoc!==undefined) ? (yDoc > h0 + tb.offsetHeight + 160) : document.body.classList.contains('ami-repli');
return p + (repli ? 44 : tb.offsetHeight); }
/* â° */
var T=document.getElementById('ami-toc-toggle'), P=document.getElementById('ami-toc-panel');
function batiToc(){ var html='<div class="toc-h"><span>â° Sommaire</span><button type="button" class="toc-x" aria-label="Fermer">â</button></div>';
(window.GROUP_ORDER||[]).forEach(function(g){ html+='<button type="button" class="toc-o" data-g="'+g+'" data-t="'+(window.GROUPS[g][0])+'">'+nomGroupe(g)+'</button><div class="toc-s">';
window.GROUPS[g].forEach(function(id){ html+='<a href="#tab-'+id+'" data-t="'+id+'">'+nomChap(id).replace(/</g,'<')+'</a>'; }); html+='</div>'; });
P.innerHTML=html;
P.querySelector('.toc-x').addEventListener('click',fermeToc);
P.querySelectorAll('.toc-o').forEach(function(b){ b.addEventListener('click',function(){ va(b.dataset.t); }); });
P.querySelectorAll('.toc-s a').forEach(function(a){ a.addEventListener('click',function(e){ e.preventDefault(); va(a.dataset.t); }); }); }
function va(id){ fermeToc(); if(window.switchTab) switchTab(null,id);
setTimeout(function(){ var el=document.getElementById('tab-'+id); if(el){ var yd=el.getBoundingClientRect().top+window.pageYOffset; var y=yd-pile(yd)-8; window.scrollTo({top:Math.max(0,y),behavior:'smooth'}); } },160); }
function ouvreToc(){ if(!P.innerHTML) batiToc(); P.hidden=false; T.setAttribute('aria-expanded','true'); }
function fermeToc(){ P.hidden=true; T.setAttribute('aria-expanded','false'); }
if(T&&P){ T.addEventListener('click',function(){ P.hidden?ouvreToc():fermeToc(); }); document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&!P.hidden) fermeToc(); }); }
/* đ */
var B=document.getElementById('amiRech'), D=document.getElementById('ami-rech'), Q=document.getElementById('rc-q'), L=document.getElementById('rc-l'), N=document.getElementById('rc-n'), X=document.getElementById('rc-x');
if(!B||!D) return;
var IDX=null, RES=[], SEL=-1;
function bati(){ IDX=[];
var w=document.createTreeWalker(R,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT), n, chap='';
while((n=w.nextNode())){
if(n.nodeType===1){ if(n.tagName==='H2'||n.tagName==='H3'){ chap=n.textContent.trim(); } continue; }
var s=n.textContent.replace(/\s+/g,' ').trim(); if(s.length<3) continue;
var p=n.parentElement; if(!p||p.closest('script,style,svg,#ami-rech,#ami-toc-panel,#tabBar,#subTabBar,#ami-pile-mini,.sommaire-wrap,.ami-barre,.ami-pied,.emporter,button')) continue;
var sec=p.closest('.tab-section'); IDX.push({t:s, tl:s.toLowerCase(), el:p, id:sec?sec.id.replace('tab-',''):null, chap:chap}); } }
function esc(s){ return s.replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function cherche(q){ q=q.trim().toLowerCase(); L.innerHTML=''; RES=[]; SEL=-1;
if(q.length<2){ N.textContent=''; L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; return; }
if(!IDX) bati();
var vus={}, par={}, ordre=[];
IDX.forEach(function(e){ var i=e.tl.indexOf(q); if(i<0) return; var k=(e.id||'')+'|'+e.chap+'|'+e.t.slice(0,60); if(vus[k]) return; vus[k]=1;
var g=e.id?(nomGroupe(groupeDe(e.id))+' âș '+nomChap(e.id)):'â'; if(!par[g]){par[g]=[];ordre.push(g);} if(par[g].length<40) par[g].push({e:e,i:i}); });
var tot=0; ordre.forEach(function(g){ tot+=par[g].length; });
N.textContent = tot? tot+(tot>1?' passages':' passage') : 'rien';
if(!tot){ L.innerHTML='<div class="rc-v">Rien pour « '+esc(q)+' ».</div>'; return; }
ordre.forEach(function(g){ var d=document.createElement('div'); d.className='rc-g'; d.textContent=g; L.appendChild(d);
par[g].forEach(function(r){ var b=document.createElement('button'); b.type='button'; b.className='rc-r'; b.setAttribute('role','option');
var a=Math.max(0,r.i-46), z=Math.min(r.e.t.length,r.i+q.length+70);
b.innerHTML=(a?'âŠ':'')+esc(r.e.t.slice(a,r.i))+'<mark>'+esc(r.e.t.slice(r.i,r.i+q.length))+'</mark>'+esc(r.e.t.slice(r.i+q.length,z))+(z<r.e.t.length?'âŠ':'')+(r.e.chap?'<span class="rc-c">'+esc(r.e.chap)+'</span>':'');
b.dataset.k=RES.length; RES.push(r.e); b.addEventListener('click',function(){ saute(+b.dataset.k); }); L.appendChild(b); }); }); }
function saute(k){ var e=RES[k]; if(!e) return; ferme();
if(e.id&&window.currentTab!==e.id&&window.switchTab) switchTab(null,e.id);
setTimeout(function(){ var cible=e.el.closest('p,li,td,th,h1,h2,h3,h4,blockquote,figure,div')||e.el;
var yd=cible.getBoundingClientRect().top+window.pageYOffset; var y=yd-pile(yd)-64; window.scrollTo({top:Math.max(0,y),behavior:'smooth'});
cible.classList.remove('ami-trouve'); void cible.offsetWidth; cible.classList.add('ami-trouve'); setTimeout(function(){cible.classList.remove('ami-trouve');},2600); },220); }
function ouvre(){ D.classList.add('on'); B.setAttribute('aria-pressed','true'); Q.value=''; Q.focus(); L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; N.textContent=''; }
function ferme(){ D.classList.remove('on'); B.setAttribute('aria-pressed','false'); }
X.addEventListener('click',ferme); D.addEventListener('click',function(e){ if(e.target===D) ferme(); });
var tm=null; Q.addEventListener('input',function(){ clearTimeout(tm); tm=setTimeout(function(){ cherche(Q.value); },140); });
Q.addEventListener('keydown',function(e){ var bs=L.querySelectorAll('.rc-r');
if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); if(!bs.length) return; SEL=Math.max(0,Math.min(bs.length-1,SEL+(e.key==='ArrowDown'?1:-1))); bs.forEach(function(b2,i){b2.classList.toggle('sel',i===SEL);}); bs[SEL].scrollIntoView({block:'nearest'}); }
else if(e.key==='Enter'){ e.preventDefault(); saute(SEL>=0?SEL:0); } else if(e.key==='Escape'){ ferme(); } });
addEventListener('keydown',function(e){ if((e.ctrlKey||e.metaKey)&&(e.key==='f'||e.key==='F')){ e.preventDefault(); ouvre(); } else if(e.key==='Escape'&&D.classList.contains('on')) ferme(); });
B.addEventListener('click',ouvre);
})();
</script>
<script id="ami-pile-js">
/* ââ LA PILE QUI SE REPLIE (19 sept 2026, C·3) : quand on descend dans un chapitre, les deux rangĂ©es d'onglets collĂ©es se replient en
une ligne de 44 px qui dit oĂč on est â « đș Mythes Religieux âș đ Le DĂ©luge ⎠». Un tap la redĂ©ploie et remonte aux onglets ;
remonter de 40 px la redĂ©ploie. Rien n'est perdu : tout reste Ă un tap. Se retire d'une ligne : body.ami-sans-repli. ââ */
(function(){'use strict';
var tb=document.getElementById('amiPile')||document.getElementById('tabBar'); if(!tb) return;
var mini=document.createElement('button'); mini.type='button'; mini.id='ami-pile-mini'; mini.setAttribute('aria-label','OĂč je suis â redĂ©ployer les onglets'); mini.setAttribute('data-ami-sous-pile','1');
tb.parentNode.insertBefore(mini, tb);
function nomGroupe(g){ var T=window.GROUP_TITLES||{}; return T[g]||g; }
function nomChap(id){ var M=window.TAB_META||{}; return M[id]?M[id][0]+' '+M[id][1]:id; }
function texte(){ var g=window.currentGroup, t=window.currentTab; var s=nomGroupe(g); if(t&&(window.GROUPS&&window.GROUPS[g]||[]).length>1) s+=' <span class="pm-sep">âș</span> '+nomChap(t); mini.innerHTML='<span class="pm-ou">'+s+'</span><span class="pm-fl">âŽ</span>'; }
/* offsetTop d'un élément collé suit le défilement : on lit la place NATURELLE de la barre (le bas de la bande des registres, juste avant) */
function haut(){ var b=document.querySelector('#ami-cosmologie .bande-registres')||document.querySelector('#ami-cosmologie .couverture'); return b?b.getBoundingClientRect().bottom+window.pageYOffset:tb.offsetTop; }
var last=window.pageYOffset, repli=false, monte=0;
function pose(on){ if(repli===on) return; repli=on; document.body.classList.toggle('ami-repli',on); if(on){ texte(); tb.setAttribute('data-ami-decolle','1'); } else tb.removeAttribute('data-ami-decolle'); }
function veille(){ var R=document.getElementById('ami-cosmologie'); if(document.body.classList.contains('ami-sans-repli')||(R&&R.classList.contains('lecture'))){ pose(false); return; }
var y=window.pageYOffset, h0=haut(), bas=h0+Math.min(tb.offsetHeight, innerHeight*0.35)+120; /* une pile haute (Mythes : 15 sous-onglets) se replie dÚs qu'elle mangerait plus d'un tiers de l'écran */
if(y<last) monte+=last-y; else monte=0; /* un défilement doux arrive par petits pas : on cumule la remontée */
if(y<h0+40){ pose(false); }
else if(y>last+2 && y>bas){ pose(true); }
else if(monte>40){ pose(false); }
last=y; }
window.addEventListener('scroll',veille,{passive:true});
mini.addEventListener('click',function(){ pose(false); var y=haut()-(parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0); window.scrollTo({top:Math.max(0,y),behavior:'smooth'}); });
document.addEventListener('click',function(e){ if(e.target.closest&&e.target.closest('.tab-btn,.subtab-btn')) setTimeout(texte,60); },true);
document.addEventListener('ami:lecture',function(){ setTimeout(veille,60); });
texte();
})();
</script>
'''
boite_css = '''
<style id="ami-boite">
''' + BOITE_CSS + '''
/* les places du 18/09 : de bas en haut đ â° â */
#amiRech{bottom:1rem!important;right:1rem!important}#ami-toc-toggle{bottom:4rem!important;right:1rem!important}#amiHaut{bottom:7rem!important;right:1rem!important}
.ami-toc{position:fixed;right:1rem;bottom:6.6rem;z-index:61;width:min(22rem,calc(100vw - 2rem));max-height:min(70vh,34rem);overflow:auto;background:var(--encre);border:1px solid rgba(var(--or-rgb),.34);border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.6);font-family:'Jost',sans-serif}
.ami-toc .toc-h{display:flex;justify-content:space-between;align-items:center;padding:.6rem .9rem;border-bottom:1px solid rgba(var(--or-rgb),.18);color:var(--or-pale);font-size:.86rem;letter-spacing:.08em}
.ami-toc .toc-x{background:none;border:0;color:var(--gris);cursor:pointer;font-size:1rem;min-width:32px;min-height:32px}
.ami-toc .toc-o{display:block;width:100%;text-align:left;background:none;border:0;border-top:1px solid rgba(255,255,255,.05);color:var(--or-pale);font:500 .9rem 'Jost',sans-serif;padding:.55rem .9rem;cursor:pointer;min-height:36px;text-transform:none;letter-spacing:0}
.ami-toc .toc-o:hover{background:rgba(var(--or-rgb),.08)}
.ami-toc .toc-s{padding:0 .4rem .4rem .9rem}
.ami-toc .toc-s a{display:block;color:var(--gris-cl);text-decoration:none;font-size:.82rem;padding:.3rem .5rem;border-radius:6px;min-height:28px}
.ami-toc .toc-s a:hover{background:rgba(var(--or-rgb),.08);color:var(--nacre)}
.ami-trouve{outline:2px solid var(--or);outline-offset:4px;border-radius:4px;transition:outline-color 2s}
#ami-rech .rc-n,#ami-rech .rc-g,#ami-rech .rc-c{font-size:.72rem}
/* ââ la pile qui se replie ââ */
#ami-pile-mini{display:none;position:fixed;left:0;right:0;top:var(--nav-h,0px);z-index:101;width:100%;box-sizing:border-box;min-height:44px;padding:.5rem 1rem;background:var(--voile);border:0;border-bottom:1px solid rgba(var(--or-rgb),.25);color:var(--or-pale);font-family:'Jost',sans-serif;font-size:.86rem;text-align:left;cursor:pointer;align-items:center;justify-content:space-between;gap:.6rem}
#ami-pile-mini .pm-ou{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
#ami-pile-mini .pm-sep{color:var(--gris);margin:0 .1rem}
#ami-pile-mini .pm-fl{color:var(--or);flex:0 0 auto}
body.ami-repli #ami-pile-mini{display:flex}
body.ami-repli #ami-cosmologie #amiPile{position:static}
</style>
'''
rep('</head>', boite_css + contraste + '</head>', 1, 'feuilles boĂźte + pile + contraste')
rep('</body>', coins + '\n<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n' + boite_js + '</body>', 1, 'coins + scripts du composant + boĂźte + pile')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p5_cosmo.py python3 p5_cosmo.py entree.html sortie.html46 lignes
CosmoLogie P5 : encres pleines, gris froids d'avant â --gris, --lc â rubis-ct, plancher du script (13 Champs, Atlas).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · 3.3 CosmoLogie â les encres (audit : 118 contrastes sous seuil).
1. Chaque encre Ă l'opacitĂ© 1 : « color:rgba(var(--x-rgb),a) » avec a < .75 â pleine (feuilles + en ligne, hors scripts/SVG).
2. Les gris froids d'avant (#555570, #7a7890, #9a98a8 â --anc-grisfroid*) servaient d'encre : ils passent Ă --gris ; --or-somb en texte â --or ;
--lc (#c85a4a, encre des lectures) â rubis-ct ; --vert-somb/--bleu-somb ne sont que des fonds (gardĂ©s).
3. Le plancher pour ce que le script construit aprÚs coup (sous-onglets, sommaire, 13 Champs, Atlas) et pour les tailles écrites en ligne.
Usage : python3 p5_cosmo.py entree.html sortie.html"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
n1 = 0
def sub_css(m):
global n1
nom, a = m.group(1), float(m.group(2))
if a >= .75: return m.group(0)
n1 += 1; return 'color:var(' + nom + ')'
parts = re.split(r'(<script\b.*?</script>|<svg\b.*?</svg>)', h, flags=re.S)
for i in range(0, len(parts), 2):
parts[i] = re.sub(r'color:\s*rgba\(var\((--[a-z0-9-]+)-rgb\),\s*(\.?\d*\.?\d+)\)', sub_css, parts[i])
h = ''.join(parts); J.append(f'encres translucides â pleines Ă{n1}')
ENCRES = '''
<style id="ami-encres">
/* ââ P5 · les encres qui tenaient sous 4,5:1 mĂȘme pleines ââ */
#ami-cosmologie{--anc-grisfroid:var(--gris);--anc-grisfroidsomb:var(--gris);--anc-grisfroidsomb2:var(--gris);--lc:var(--rubis-ct)}
#ami-cosmologie .vol-label,#ami-cosmologie .subrow-lbl,#ami-cosmologie .st-num,#ami-cosmologie .num{color:var(--or)}
#ami-cosmologie .couv-attrib,#ami-cosmologie .atl-tl-date{color:var(--gris)}
#ami-cosmologie [style*="color:var(--or-somb)"],#ami-cosmologie [style*="color:var(--anc-orsomb)"]{color:var(--or)!important}
#ami-cosmologie [style*="color:var(--anc-grisfroidsomb2)"],#ami-cosmologie [style*="color:var(--anc-grisfroidsomb)"]{color:var(--gris)!important}
#ami-cosmologie .tab-btn{color:var(--gris)}
#ami-cosmologie .reson,#ami-cosmologie .ft-loc{color:var(--or)}
#ami-cosmologie a:not([class]){color:var(--or);min-height:24px;display:inline-block}
/* le plancher pour ce que le script construit aprÚs coup, et pour les tailles écrites en ligne */
#ami-cosmologie .syn13-rep strong,#ami-cosmologie .atl-scan-status,#ami-cosmologie [class^="atl-"] strong,#ami-cosmologie [class^="atl-"] em,#ami-cosmologie .subtab-btn,#ami-cosmologie .som-secs a,#ami-cosmologie .syn13-btn,#ami-cosmologie .syn13-rep,#ami-cosmologie .atl-layer,#ami-cosmologie td,#ami-cosmologie th{font-size:max(.72rem,1em)!important}
#ami-cosmologie [style*="font-size:.5"],#ami-cosmologie [style*="font-size:.6"],#ami-cosmologie [style*="font-size: .5"],#ami-cosmologie [style*="font-size: .6"],#ami-cosmologie [style*="font-size:.7rem"]{font-size:max(.72rem,1em)!important}
/* cibles : les liens du pied et de la bande */
.ami-pied a,.bande-registres a{min-height:24px;display:inline-block}
</style>
'''
rep('</head>', ENCRES + '</head>', 1, 'feuille ami-encres')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
p6_cosmo_mutations.py python3 p6_cosmo_mutations.py page.html124 lignes
CosmoLogie : 90 mutations (identitĂ©, barre/pied, onglets sans chevauchement, sous-onglets, â Sommaire, Piratage, MapMonde, 13 Champs, đ§ â â¶ zoom, â â° đ, pile qui se replie, plancher, dĂ©bordement 4 largeurs).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 3.3 CosmoLogie â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis.
IdentitĂ©, barre/pied/bande/Emporter, 10 onglets â 33 chapitres (forme gardĂ©e), sous-onglets, â Sommaire du chapitre, le renvoi
« Piratage », la MapMonde, les 13 Champs au canon, đ§ â â¶ zoom, â â° đ, #prog, la pile qui se replie, plein Ă©cran, plancher,
dĂ©bordement, 0 var sans dĂ©claration, 0 erreur JS â Ă 390 et 1280 (+ 320 pour le dĂ©bordement)."""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'cosmo_p6.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1200)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
# ââ identitĂ©
ok('meta ami:volume 3.3 · ami:version V10 · ami:date 2026-09-19', E("()=>['3.3','V10','2026-09-19'].every((v,i)=>document.querySelector('meta[name=\"ami:'+['volume','version','date'][i]+'\"]').content===v)"))
ok('titre : 3.3 CosmoLogie â Tu Sais Lire la Culture', E("()=>document.title.startsWith('3.3 CosmoLogie â Tu Sais Lire la Culture')"))
ok('un seul h1 (la une) ; 40 chapitres en h2.ami-h1', E("()=>document.querySelectorAll('h1').length===1 && document.querySelectorAll('h2.ami-h1').length===40"))
ok('la une : kicker Volume 3.3 · Tu Sais Lire la Culture, le miroir en une ligne (4.3 CosmoGonie)', E("()=>/Volume 3.3/.test(document.querySelector('.vol-label').textContent) && /Tu Sais Lire la Culture/.test(document.querySelector('.vol-label').textContent) && /4.3 CosmoGonie/.test(document.querySelector('.couv-miroir').textContent) && !/7.0/.test(document.querySelector('.vol-label').textContent)"))
ok('aucune var(--x) sans déclaration', E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}for(const el of document.querySelectorAll('[style]')){for(const m of el.getAttribute('style').matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>!decl.has(v)&&!['--cc','--c','--lc','--fx','--fy'].includes(v)).length===0;}"))
ok('plus de glyphe privĂ© U+F105 ; C9 ê© et C12 đȘ dans le script des 13 Champs', E("()=>!document.body.textContent.includes('\\uf105') && [...document.scripts].some(s=>s.textContent.includes(\"9:'ê©'\") && s.textContent.includes(\"12:'đȘ'\"))"))
# ââ la barre, le pied, la bande
ok('barre V16 en tĂȘte, 3.3 marquĂ©e .ici, tiroir fermĂ©', E("()=>!!document.querySelector('.ami-barre .v.ici[data-v=\"ami-cosmologie\"]') && !document.querySelector('.ami-barre .tiroir.on')"))
ok('bande des registres aprĂšs la une', E("()=>{const b=document.querySelector('.bande-registres');return b && b.previousElementSibling.classList.contains('couverture')}"))
ok('pied V16 aprĂšs la racine, sceau đ, volume 3.3, devise « Le Mythe nâest pas un Mensonge »', E("()=>{const f=document.querySelector('.ami-pied');return f && f.previousElementSibling.id==='ami-cosmologie' && f.querySelector('.ap-sceau').textContent.trim()==='đ' && f.textContent.includes('3.3') && f.textContent.includes('Trace Ă Savoir Lire')}"))
ok('ancienne nav-footer absente ; aucun lien vers un fichier .html local', E("()=>!document.querySelector('.nav-footer') && [...document.querySelectorAll('a[href]')].every(a=>!/^[^:]*\\.html/.test(a.getAttribute('href')))"))
ok('â Emporter avant le pied, export nommĂ© 3.3_COSMOLOGIE', E("()=>{const e=document.querySelector('#ami-cosmologie .emporter');return e && !!document.getElementById('dlHtml') && e.parentElement.id==='ami-cosmologie' && [...document.scripts].some(s=>s.textContent.includes('3.3_COSMOLOGIE_aMi_'))}"))
ok('la MapMonde : plus dâiframe local, renvois vers agdistys.fr/ami-karma et /histoire', E("()=>!document.querySelector('iframe[src$=\"MapMonde_Diagnostic_aMi.html\"]') && !!document.querySelector('.ami-renvoi-carte a[href=\"https://agdistys.fr/ami-karma\"]') && !!document.querySelector('.ami-renvoi-carte a[href=\"https://agdistys.fr/histoire\"]')"))
# ââ les onglets (forme gardĂ©e) et les sous-onglets
ok('10 onglets, sans rouleau, aucun chevauchement entre voisins', E("()=>{const bs=[...document.querySelectorAll('#tabBar .tab-btn')];const tb=document.getElementById('tabBar');if(bs.length!==10||tb.scrollWidth>tb.clientWidth+1)return false;for(let i=0;i<bs.length;i++)for(let j=i+1;j<bs.length;j++){const a=bs[i].getBoundingClientRect(),b=bs[j].getBoundingClientRect();if(a.left<b.right-1&&b.left<a.right-1&&a.top<b.bottom-1&&b.top<a.bottom-1)return false;}return true}"))
ok('libellés des onglets ℠11,5 px ; cibles ℠44 px', E("()=>[...document.querySelectorAll('#tabBar .tab-btn')].every(b=>parseFloat(getComputedStyle(b).fontSize)>=11.5 && b.getBoundingClientRect().height>=44)"))
ok('sous-onglets collés avec les onglets (#amiPile sticky, contient #subTabBar) ; onglets centrés', E("()=>{const p=document.getElementById('amiPile');const bs=[...document.querySelectorAll('#tabBar .tab-btn')];const tb=document.getElementById('tabBar').getBoundingClientRect();const rows={};bs.forEach(b=>{const r=b.getBoundingClientRect();const k=Math.round(r.top);rows[k]=rows[k]||{l:1e9,r:0};rows[k].l=Math.min(rows[k].l,r.left);rows[k].r=Math.max(rows[k].r,r.right)});const centre=Object.values(rows).every(o=>Math.abs((o.l-tb.left)-(tb.right-o.r))<40);return getComputedStyle(p).position==='sticky' && p.contains(document.getElementById('subTabBar')) && centre}"))
E("()=>document.querySelector('#tabBar .tab-btn[data-group=mythes]').click()"); pg.wait_for_timeout(300)
ok('đș Mythes Religieux : 15 sous-onglets en 3 rangĂ©es, La CrĂ©ation ouverte', E("()=>document.querySelectorAll('#subTabBar .subtab-btn').length===15 && document.querySelectorAll('#subTabBar .subrow').length===3 && document.querySelector('.tab-section.active').id==='tab-creation' && document.querySelectorAll('.tab-section.active').length===1"))
E("()=>document.querySelector('#subTabBar .subtab-btn[data-id=deluge]').click()"); pg.wait_for_timeout(300)
ok('sous-onglet đ Le DĂ©luge : un seul chapitre ouvert, â Sommaire du chapitre reconstruit', E("()=>document.querySelector('.tab-section.active').id==='tab-deluge' && document.getElementById('somChapTitle').textContent.includes('DĂ©luge') && document.querySelectorAll('#sommaireBody a').length>=3"))
E("()=>document.querySelectorAll('#sommaireBody a')[1].click()"); pg.wait_for_timeout(900)
ok('une entrĂ©e du â Sommaire descend dans le chapitre', E("()=>window.pageYOffset>300"))
# ââ le renvoi « Piratage » (Ă©tait « Ă venir »)
E("()=>document.querySelector('#tabBar .tab-btn[data-group=avatars]').click()"); pg.wait_for_timeout(250)
E("()=>document.querySelector('a[href=\"#pir-intro\"]').click()"); pg.wait_for_timeout(900)
ok('le renvoi « Piratage » ouvre lâonglet Emprise · Conditionnement et descend Ă #pir-intro', E("()=>{const s=document.querySelector('.tab-section.active');const el=document.getElementById('pir-intro');const r=el.getBoundingClientRect();return s.id==='tab-spirale' && r.top>=0 && r.top<innerHeight*0.6}"))
# ââ 13 Champs syncrĂ©tiques (script) + Atlas
E("()=>switchTab(null,'syncretismes')"); pg.wait_for_timeout(300)
ok('les 13 Champs syncrĂ©tiques : 13 boutons, C9 ê©, C12 đȘ ; un clic ouvre la fiche avec « âș »', E("()=>{const bs=[...document.querySelectorAll('.syn13-btn')];if(bs.length!==13)return false;if(!bs[8].textContent.includes('ê©')||!bs[11].textContent.includes('đȘ'))return false;bs[2].click();const p=document.getElementById('syn13Panel');return p.classList.contains('visible') && p.textContent.includes('âș') && !p.textContent.includes('\\uf105')}"))
E("()=>switchTab(null,'atlantide')"); pg.wait_for_timeout(300)
ok('lâAtlas retrouvĂ© : choisir Santorin peint le scanner', E("()=>{const b=document.querySelector('[data-act^=\"atlSelect(\"]');if(!b)return false;b.click();return document.querySelectorAll('.atl-bar,.atl-cell,[class*=atl-]').length>8}"))
# ââ le zoom des grandes images + un onglet cliquĂ© ne remonte plus en haut
E("()=>switchTab(null,'arbre')"); pg.wait_for_timeout(300)
ok('lâArbre Culturel : un bouton « Voir en grand » ; le zoom sâouvre plein Ă©cran, + agrandit, â ferme', E("()=>{const b=[...document.querySelectorAll('#tab-arbre .ami-zoom-b')][0];if(!b)return false;b.click();const Z=document.getElementById('ami-zoom');if(!Z.classList.contains('on'))return false;const I=Z.querySelector('img');const t0=I.style.transform;Z.querySelector('[data-z=\"+\"]').click();const t1=I.style.transform;Z.querySelector('[data-z=x]').click();return t0!==t1 && !Z.classList.contains('on') && document.documentElement.style.overflow===''}"))
E("()=>switchTab(null,'deluge')"); pg.wait_for_timeout(300)
E("()=>{const p=document.getElementById('amiPile');window.scrollTo({top:p.offsetTop+p.offsetHeight+900,behavior:'instant'})}"); pg.wait_for_timeout(300)
E("()=>document.querySelector('#subTabBar .subtab-btn[data-id=babel]').click()"); pg.wait_for_timeout(1400)
ok('un sous-onglet cliqué depuis le bas ramÚne sur la pile, pas en haut de page', E("()=>{const b=document.querySelector('#ami-cosmologie .bande-registres').getBoundingClientRect();return window.pageYOffset>300 && b.bottom<=60 && b.bottom>-20}"))
# ââ đ§ â â¶ zoom
E("()=>switchTab(null,'methode')"); pg.wait_for_timeout(200)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
ok('đ§ lecture continue : 33 chapitres dĂ©roulĂ©s, goutte pleine', E("()=>document.querySelectorAll('.tab-section.active').length===33 && document.getElementById('amiLecture').getAttribute('aria-pressed')==='true' && getComputedStyle(document.querySelector('#amiLecture svg')).fill!=='none'"))
E("()=>{document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(300) # on éteint d'abord
E("()=>{switchTab(null,'deluge');}"); pg.wait_for_timeout(500)
E("()=>{const s=document.getElementById('tab-deluge');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+600,behavior:'instant'})}"); pg.wait_for_timeout(300)
_on=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-section'))break;}el.setAttribute('data-ami-on','1');return el.getBoundingClientRect().top}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(900)
ok('đ§ allumĂ©e depuis un volet du milieu : le point lu ne bouge pas (les volets dâavant sâouvrent au-dessus sans pousser la lecture) â Diane 19/09 23h40', pg.evaluate("(t)=>{const el=document.querySelector('[data-ami-on]');return Math.abs(el.getBoundingClientRect().top-t)<3}", _on), str(pg.evaluate("(t)=>[document.querySelector('[data-ami-on]').getBoundingClientRect().top,t]", _on)))
E("()=>{const s=document.getElementById('tab-deluge');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+900,behavior:'instant'})}"); pg.wait_for_timeout(300)
_anc=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-section'))break;}document.querySelectorAll('[data-ami-anc]').forEach(e=>e.removeAttribute('data-ami-anc'));el.setAttribute('data-ami-anc','1');return [el.getBoundingClientRect().top, window.pageYOffset, el.closest('.tab-section,.tab-panel').id, el.closest('.tab-section,.tab-panel').getBoundingClientRect().top]}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(700)
ok('đ§ recliquĂ©e depuis le bas : on reste oĂč on lisait (le chapitre sous les yeux sâouvre, le point lu ne bouge pas) â Diane 19/09 23h', pg.evaluate("(a)=>{const el=document.querySelector('[data-ami-anc]');const s=document.getElementById(a[2]);const r=(el||s).getBoundingClientRect();return Math.abs(r.top-(el?a[0]:a[3]))<3 && s.classList.contains('active')}", _anc))
E("()=>{switchTab(null,'methode');window.scrollTo(0,0);document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(400)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : un seul chapitre (celui sous les yeux)', E("()=>document.querySelectorAll('.tab-section.active').length===1"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
ok('â contraste : body.ami-contraste, cercle plein', E("()=>document.body.classList.contains('ami-contraste') && getComputedStyle(document.querySelector('#amiContraste svg circle')).fill!=='none'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('â au repos : demi-cercle', E("()=>getComputedStyle(document.querySelector('#amiContraste svg circle')).fill==='none' && getComputedStyle(document.querySelector('#amiContraste svg path')).fill!=='none'"))
ok('đ§ Ă gauche (.5rem) puis â (2.9rem) ; â¶ Ă droite', E("()=>{const l=document.getElementById('amiLecture').getBoundingClientRect(),c=document.getElementById('amiContraste').getBoundingClientRect(),p=document.getElementById('amiPlein').getBoundingClientRect();return l.left<c.left && c.left<60 && p.right>innerWidth-60}"))
pg.click('#amiPlein'); pg.wait_for_timeout(700)
ok('â¶ plein Ă©cran : la racine dĂ©file, la barre est dedans, zoom visible, â', E("()=>{const fe=document.fullscreenElement;return fe && fe.id==='ami-cosmologie' && getComputedStyle(fe).overflowY==='auto' && fe.contains(document.getElementById('amiBarre')) && getComputedStyle(document.querySelector('.coin-zoom')).display==='flex' && document.getElementById('amiPlein').textContent==='â'}"))
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
ok('zoom + : 115 %', E("()=>document.getElementById('amiZoomV').textContent==='115%'"))
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(100)
pg.click('#amiPlein'); pg.wait_for_timeout(600)
ok('sortie du plein écran : la barre revient avant la racine', E("()=>!document.fullscreenElement && document.getElementById('amiBarre').nextElementSibling.id==='ami-cosmologie'"))
# ââ â â° đ + #prog + la pile qui se replie
E("()=>{const v=document.querySelector('.tab-section.active');window.scrollTo({top:v.getBoundingClientRect().top+window.pageYOffset+1200,behavior:'instant'})}"); pg.wait_for_timeout(500)
ok('â paraĂźt aprĂšs 600 px, â° et đ en bas Ă droite (de bas en haut đ â° â)', E("()=>{const g=id=>document.getElementById(id).getBoundingClientRect();const r=g('amiRech'),t=g('ami-toc-toggle'),h=g('amiHaut');return !document.getElementById('amiHaut').hidden && r.bottom>t.bottom && t.bottom>h.bottom && r.right>innerWidth-40}"))
ok('#prog avance en descendant', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>switchTab(null,'deluge')"); pg.wait_for_timeout(300)
E("()=>{const tb=document.getElementById('amiPile');window.scrollTo({top:tb.offsetTop+tb.offsetHeight+400,behavior:'instant'})}"); pg.wait_for_timeout(200)
E("()=>window.scrollBy(0,60)"); pg.wait_for_timeout(300)
ok('la pile se replie en descendant : une ligne « oĂč je suis » sous la barre, les onglets ne collent plus', E("()=>{const m=document.getElementById('ami-pile-mini');const r=m.getBoundingClientRect();return document.body.classList.contains('ami-repli') && getComputedStyle(m).display==='flex' && r.height>=40 && r.top<80 && /DĂ©luge/.test(m.textContent) && getComputedStyle(document.getElementById('amiPile')).position==='static'}"))
E("()=>window.scrollBy(0,-60)"); pg.wait_for_timeout(250)
ok('remonter de 40 px redéploie les onglets', E("()=>!document.body.classList.contains('ami-repli') && getComputedStyle(document.getElementById('ami-pile-mini')).display==='none' && getComputedStyle(document.getElementById('amiPile')).position==='sticky'"))
E("()=>window.scrollBy(0,300)"); pg.wait_for_timeout(250)
E("()=>document.getElementById('ami-pile-mini').click()"); pg.wait_for_timeout(900)
ok('un tap sur la ligne remonte aux onglets et les redéploie', E("()=>{const tb=document.getElementById('amiPile').getBoundingClientRect();return !document.body.classList.contains('ami-repli') && tb.top>=-2 && tb.top<120}"))
E("()=>document.getElementById('amiHaut').click()"); pg.wait_for_timeout(1600)
ok('â ramĂšne en haut', E("()=>window.pageYOffset<40"))
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(300)
ok('â° sommaire : 10 onglets, 33 chapitres', E("()=>!document.getElementById('ami-toc-panel').hidden && document.querySelectorAll('#ami-toc-panel .toc-o').length===10 && document.querySelectorAll('#ami-toc-panel .toc-s a').length===33"))
E("()=>document.querySelectorAll('#ami-toc-panel .toc-s a')[9].click()"); pg.wait_for_timeout(900)
ok('un lien du sommaire ouvre lâonglet et le chapitre (Le DĂ©luge)', E("()=>document.getElementById('ami-toc-panel').hidden && document.querySelector('.tab-section.active').id==='tab-deluge' && document.querySelector('#tabBar .tab-btn.active').dataset.group==='mythes'"))
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', 'Léviathan'); pg.wait_for_timeout(500)
ok('đ « LĂ©viathan » trouve des passages, groupĂ©s par onglet âș chapitre', E("()=>document.querySelectorAll('#rc-l .rc-r').length>0 && [...document.querySelectorAll('#rc-l .rc-g')].some(g=>g.textContent.includes('âș'))"))
E("()=>document.querySelector('#rc-l .rc-r').click()"); pg.wait_for_timeout(900)
ok('un rĂ©sultat ouvre lâonglet et le chapitre, marque le passage visible', E("()=>{const t=document.querySelector('.ami-trouve');if(!t)return false;const r=t.getBoundingClientRect();return !document.getElementById('ami-rech').classList.contains('on') && r.width>0 && r.top>=0 && r.top<innerHeight}"))
# ââ forme
ok('reduced-motion écrit', E("()=>[...document.querySelectorAll('style')].some(s=>/prefers-reduced-motion/.test(s.textContent))"))
ok('aucun émoji en italique', E("()=>[...document.querySelectorAll('#ami-cosmologie .e')].every(e=>getComputedStyle(e).fontStyle!=='italic')"))
ok('26 tableaux en rouleau (.tw)', E("()=>document.querySelectorAll('#ami-cosmologie .tw>table').length>=25"))
_pl=E("()=>{document.querySelectorAll('.tab-section').forEach(s=>s.classList.add('active'));let n=0;const out={};const w=document.createTreeWalker(document.getElementById('ami-cosmologie'),NodeFilter.SHOW_TEXT);let t;while((t=w.nextNode())){if(t.textContent.trim().length<2)continue;const el=t.parentElement;if(!el||el.closest('script,style,svg,.ami-barre'))continue;const cs=getComputedStyle(el);if(cs.display==='none')continue;if(parseFloat(cs.fontSize)<11.5){n++;const k=el.tagName+'.'+String(el.className).slice(0,20)+' '+cs.fontSize;out[k]=(out[k]||0)+1}}document.querySelectorAll('.tab-section').forEach((s,i)=>s.classList.toggle('active',s.id==='tab-deluge'));return n+' '+JSON.stringify(out)}")
ok('plancher 11,5 px tenu dans la racine (tous chapitres dépliés ; reste la barre V16)', _pl.startswith('0 '), _pl[:300])
ok('0 erreur JS', not [e for e in errs if 'ERR_' not in e], str(errs[:2])); pg.close()
for W in (320, 390, 768, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(800)
E = lambda js: pg.evaluate(js)
ok(f'pas de débordement à {W} px (tous chapitres dépliés, hors rouleaux et halos coupés)', E("()=>{document.querySelectorAll('.tab-section').forEach(s=>s.classList.add('active'));const w=document.documentElement.clientWidth;let bad=[];document.querySelectorAll('#ami-cosmologie *').forEach(el=>{if(el.closest('svg,.cover-ambient'))return;let a=el.parentElement,roule=false;while(a&&a!==document.body){const o=getComputedStyle(a).overflowX;if(o==='auto'||o==='scroll'||o==='hidden'){roule=true;break}a=a.parentElement}if(roule)return;const r=el.getBoundingClientRect();if(r.width<1)return;if(r.right>w+1||r.left<-1)bad.push(el.tagName+'.'+String(el.className).slice(0,20))});return bad.length===0 && document.documentElement.scrollWidth<=w}"))
pg.close()
b.close()
n = sum(1 for r in R if r[1]); print(f'\n{n} / {len(R)} réussis')
if n < len(R): print('â ratĂ©s :', [r[0] for r in R if not r[1]])
chaine_cosmo.sh bash chaine_cosmo.sh (cosmo.html à cÎté)20 lignes
Rejoue la chaĂźne CosmoLogie p1 â p5 d'un trait (p3_palette, p19, p2_plancher inclus).
â§ Copier le script
set -e -o pipefail
python3 p1_cosmo.py cosmo.html cosmo_p1.html >/dev/null
python3 p2_cosmo.py cosmo_p1.html cosmo_p2.html >/dev/null
cp cosmo_p2.html cosmo_p3.html
python3 p3_palette.py rapatrier cosmo_p3.html ':root' >/dev/null
python3 p3_palette.py repeindre cosmo_p3.html ':root' >/dev/null
python3 p4_cosmo.py cosmo_p3.html cosmo_p4.html | tail -1
python3 ../acc/p19_plein_ecran_v16.py cosmo_p4.html cosmo_p4b.html ami-cosmologie >/dev/null
cp cosmo_p4b.html cosmo_p5.html
python3 p2_plancher.py cosmo_p5.html 390 ami-cosmologie 2>&1 | tail -1
python3 p2_plancher.py cosmo_p5.html 1280 ami-cosmologie 2>&1 | tail -1
python3 - <<'PY'
h=open('cosmo_p5.html',encoding='utf-8').read()
if '#ami-cosmologie span.e{font-size:.72rem}' in h:
h=h.replace('#ami-cosmologie span.e{font-size:.72rem}','#ami-cosmologie span.e{font-size:max(.72rem,1em)}')
open('cosmo_p5.html','w',encoding='utf-8').write(h)
PY
python3 p5_cosmo.py cosmo_p5.html cosmo_p6.html | tail -1
python3 p7_arbre_cosmo.py cosmo_p6.html cosmo_p7.html arbre/arbre_5200_q80.webp | tail -1
p1_decr.py · p2_decr.py · p4_decr.py · p5_decr.py · p7_folios_decr.py bash chaine_decr.sh (decr.html à cÎté)1 lignes
DĂ©cryptage : texte (liens cms.webnode ĂŽtĂ©s, Formule en encart, canon des Champs), forme (10 volets, loupe animĂ©e â„ 900 px / fixe en dessous, une colorĂ©e), anatomie V16 + pile qui se replie, encres, visualiseur des folios en tĂȘte et empilĂ© sous 900 px.
â§ Copier le script
(script absent du dossier outils/)
p6_decr_mutations.py python3 p6_decr_mutations.py page.html136 lignes
DĂ©cryptage : 109 mutations (identitĂ©, barre/pied/bande/Emporter/Formule, onglets â volets, â sommaire, Calculateur, visualiseur (liste, scans, zoom ‹, empilĂ©), loupe, đ§ â â¶ zoom, â â° đ, pile, plancher, dĂ©bordement).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 4.1 DĂ©cryptage â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis.
IdentitĂ©, barre/pied/bande/Emporter/Formule, 5 onglets â 10 volets, â sommaire-accordĂ©on, le Calculateur, le visualiseur (liste,
scans, zoom ‹, empilĂ© sous 900 px), la loupe de la une (animĂ©e â„ 900, fixe en dessous), le canon des Champs, đ§ â â¶ zoom,
â â° đ, #prog, la pile qui se replie, plein Ă©cran, plancher, dĂ©bordement, 0 var sans dĂ©claration, 0 erreur JS â Ă 390 et 1280."""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'decr_p6.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1200)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
# ââ identitĂ©
ok('meta ami:volume 4.1 · ami:version V6 · ami:date 2026-09-19', E("()=>['4.1','V6','2026-09-19'].every((v,i)=>document.querySelector('meta[name=\"ami:'+['volume','version','date'][i]+'\"]').content===v)"))
ok('titre : 4.1 DĂ©cryptage · Tuto Ouroboros â Tu Sais Accueillir le Vivant', E("()=>document.title.startsWith('4.1 DĂ©cryptage · Tuto Ouroboros â Tu Sais Accueillir le Vivant')"))
ok('h1 : la une (et sa couche loupe, inert) ; 9 volets en h2.ami-h1', E("()=>document.querySelectorAll('h1').length===2 && document.querySelector('.loupe-inner').hasAttribute('inert') && document.querySelectorAll('h2.ami-h1').length===9"))
ok('la une : DĂCRYPTAGE, kicker ConScience · Volume 4.1 · Tu Sais Accueillir le Vivant, 5 onglets annoncĂ©s', E("()=>/DĂCRYPTAGE/.test(document.querySelector('.couv-h1').textContent) && /ConScience aMi · Volume 4.1$/.test(document.querySelector('.vol-label').textContent.trim()) && ([...document.querySelector('.couverture').children].filter(e=>!e.classList.contains('loupe-layer')).map(e=>e.textContent).join(' ').match(/Tu Sais Accueillir Le Vivant/g)||[]).length===1 && /GĂ©oColoVoluMĂ©trique/.test(document.querySelector('.couv-question').textContent) && /Ăvidences Sciences/.test(document.querySelector('.couv-tabs-prev').textContent) && /ConScience Universelle/.test(document.querySelector('.couv-attrib').textContent)"))
ok('aucune var(--x) sans déclaration', E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}for(const el of document.querySelectorAll('[style]')){for(const m of el.getAttribute('style').matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>!decl.has(v)&&!['--cc','--c','--lc','--fx','--fy'].includes(v)).length===0;}"))
ok('plus aucun lien agdistys.cms.webnode.fr ni fichier .html local ; la Formule en encart', E("()=>!document.documentElement.outerHTML.includes('cms.webnode') && [...document.querySelectorAll('a[href]')].every(a=>!/^[^:]*\\.html/.test(a.getAttribute('href'))) && /ÎI \\/ ÎE/.test(document.querySelector('.ami-formule .af-f').textContent) && /D = S Ă T/.test(document.querySelector('.ami-formule .af-d').textContent)"))
ok('les couleurs des Champs au canon (âĄïž Vibration · đ« Flux · đ§ Structure · đ SouverainetĂ© · đ„ Offrande)', E("()=>{const t=[...document.querySelectorAll('.chip-fuchsia')].find(c=>/Offrande/.test(c.textContent)).parentElement.textContent;return /Vibration/.test(t)&&/Flux/.test(t)&&/Structure/.test(t)&&/SouverainetĂ©/.test(t)&&/Offrande/.test(t)&&/C11/.test(t)}"))
ok('orthographes : ConScience, TransMission, ThermoCognition ; « Cosmologie » (la science) gardé', E("()=>{const t=document.getElementById('ami-decryptage').innerText;return !/\\bConscience\\b/.test(t) && /ConScience/.test(t) && /ThermoCognition/.test(t) && /Cosmologie/.test(t)}"))
# ââ la barre, le pied, la bande
ok('barre V16 en tĂȘte, 4.1 marquĂ©e .ici, tiroir fermĂ© ; topnav maison absente', E("()=>!!document.querySelector('.ami-barre .v.ici[data-v=\"ami-decryptage\"]') && !document.querySelector('.ami-barre .tiroir.on') && !document.querySelector('.topnav')"))
ok('bande des registres aprĂšs lâencart du volume, avant le sommaire', E("()=>{const b=document.querySelector('.bande-registres');return b && b.previousElementSibling.classList.contains('ami-volume-encart') && b.nextElementSibling.classList.contains('ami-sommaire-label')}"))
ok('pied V16 aprĂšs la racine, sceau đ, volume 4.1, phrase-source « Traduire⊠»', E("()=>{const f=document.querySelector('.ami-pied');return f && f.previousElementSibling.id==='ami-decryptage' && f.querySelector('.ap-sceau').textContent.trim()==='đ' && f.textContent.includes('4.1') && f.textContent.includes('systĂšme de signes') && !document.querySelector('.ami-footer')}"))
ok('â Emporter aprĂšs la Formule, avant le pied, export nommĂ© 4.1_DECRYPTAGE', E("()=>{const e=document.querySelector('#ami-decryptage .emporter');return e && e.previousElementSibling.classList.contains('ami-formule') && !!document.getElementById('dlHtml') && [...document.scripts].some(s=>s.textContent.includes('4.1_DECRYPTAGE_aMi_'))}"))
# ââ la loupe de la une
if W >= 900:
ok('la loupe animée reste sur grand écran ; la loupe fixe est cachée', E("()=>getComputedStyle(document.querySelector('.loupe-layer')).display!=='none' && getComputedStyle(document.querySelector('.ami-loupe-fixe')).display==='none'"))
ok('la loupe grossit ce quâelle couvre : mĂȘme gĂ©omĂ©trie que la une, origine du zoom qui suit la lentille', E("()=>{const c=document.querySelector('.couverture'),l=document.querySelector('.loupe-inner');const cs=getComputedStyle(c),ls=getComputedStyle(l);const h1=c.querySelector(':scope > .couv-h1').getBoundingClientRect(),h2=l.querySelector('.couv-h1').getBoundingClientRect();return cs.paddingTop===ls.paddingTop && cs.paddingLeft===ls.paddingLeft && ls.animationName.includes('loupeOrigine') && Math.abs((h1.top+h1.bottom)/2-(h2.top+h2.bottom)/2)<h1.height*0.6}"))
else:
ok('sur tĂ©lĂ©phone : loupe fixe đ dans le coin, couche zoomĂ©e et anneau cachĂ©s, rien ne couvre le titre', E("()=>getComputedStyle(document.querySelector('.loupe-layer')).display==='none' && getComputedStyle(document.querySelector('.loupe-ring')).display==='none' && getComputedStyle(document.querySelector('.ami-loupe-fixe')).display==='block'"))
# ââ les onglets
ok('5 onglets, sans rouleau, aucun chevauchement, centrés par rangée', E("()=>{const bs=[...document.querySelectorAll('#tabBar .tab-btn')];const tb=document.getElementById('tabBar');if(bs.length!==5||tb.scrollWidth>tb.clientWidth+1)return false;for(let i=0;i<bs.length;i++)for(let j=i+1;j<bs.length;j++){const a=bs[i].getBoundingClientRect(),b=bs[j].getBoundingClientRect();if(a.left<b.right-1&&b.left<a.right-1&&a.top<b.bottom-1&&b.top<a.bottom-1)return false;}const t=tb.getBoundingClientRect(),rows={};bs.forEach(b=>{const r=b.getBoundingClientRect();const k=Math.round(r.top);rows[k]=rows[k]||{l:1e9,r:0};rows[k].l=Math.min(rows[k].l,r.left);rows[k].r=Math.max(rows[k].r,r.right)});return Object.values(rows).every(o=>Math.abs((o.l-t.left)-(t.right-o.r))<40)}"))
ok('libellĂ©s â„ 11,5 px ; cibles â„ 44 px ; aucun mot coupĂ© au hasard (pas de ligne dâune lettre)', E("()=>[...document.querySelectorAll('#tabBar .tab-btn')].every(b=>parseFloat(getComputedStyle(b).fontSize)>=11.5 && b.getBoundingClientRect().height>=44 && b.getBoundingClientRect().height<110)"))
ok('#amiPile collé (sticky) sous la barre', E("()=>getComputedStyle(document.getElementById('amiPile')).position==='sticky'"))
E("()=>document.querySelectorAll('#tabBar .tab-btn')[1].click()"); pg.wait_for_timeout(400)
ok('đ MĂ©thode Ouroboros : 3 volets ouverts (methode, tuto, resonance), sommaire-accordĂ©on sur la partie', E("()=>[...document.querySelectorAll('.tab-section.active')].map(s=>s.id).join()==='tab-methode,tab-tuto,tab-resonance' && document.getElementById('sommaireBody').classList.contains('open')"))
E("()=>document.querySelectorAll('#som-draw-methode a')[1].click()"); pg.wait_for_timeout(1500)
ok('une entrĂ©e du â sommaire descend au chapitre, sous la pile', E("()=>{const id=document.querySelectorAll('#som-draw-methode a')[1].getAttribute('href').slice(1);const r=document.getElementById(id).getBoundingClientRect();return window.pageYOffset>300 && r.top>=0 && r.top<innerHeight*0.6}"))
E("()=>{window.scrollBy(0,1500)}"); pg.wait_for_timeout(300)
E("()=>document.querySelectorAll('#tabBar .tab-btn')[0].click()"); pg.wait_for_timeout(1400)
ok('un onglet cliqué depuis le bas ramÚne sur la pile, pas en haut de page', E("()=>{const b=document.querySelector('#ami-decryptage .bande-registres').getBoundingClientRect();return window.pageYOffset>200 && b.bottom<=60 && b.bottom>-20}"))
# ââ le Calculateur
ok('âïž Calculateur : les curseurs donnent un C et un Ă©tat', E("()=>{document.getElementById('sliderDI').value=8;document.getElementById('sliderDE').value=4;calcThermo();const r=document.getElementById('calcResult');return r.classList.contains('visible') && /C = /.test(r.textContent)}"))
# ââ le visualiseur
E("()=>switchTab(null,'tomo')"); pg.wait_for_timeout(600)
ok('đ Lecture Tomographique : le visualiseur EN TĂTE du groupe, 15 folios listĂ©s, sans cartouche de dĂ©bogage', E("()=>[...document.querySelectorAll('.tab-section.active')].map(s=>s.id).join()==='tab-tomo,tab-sections,tab-folios' && !!(document.getElementById('tab-tomo').compareDocumentPosition(document.getElementById('tab-sections'))&Node.DOCUMENT_POSITION_FOLLOWING) && document.querySelectorAll('#tSidebar [onclick]').length===15 && !document.body.textContent.includes('SIDEBAR INIT')"))
E("()=>[...document.querySelectorAll('#tSidebar [onclick]')][8].click()"); pg.wait_for_timeout(500)
ok('un folio choisi : image affichée, lecture MétaConScience remplie', E("()=>{const i=document.getElementById('tMainImg');return i.style.display==='block' && i.getBoundingClientRect().width>100 && document.getElementById('tReading').textContent.length>200}"))
if W < 900:
ok('sous 900 px : liste en rangĂ©e dĂ©filante, image pleine largeur, lecture sous lâimage (empilĂ©)', E("()=>{const sb=document.getElementById('tSidebar').getBoundingClientRect(),v=document.querySelector('.t-viewer').getBoundingClientRect(),r=document.getElementById('tReading').getBoundingClientRect();return getComputedStyle(document.querySelector('.tomo-inner')).flexDirection==='column' && sb.width>innerWidth-40 && v.width>innerWidth-40 && r.top>=v.bottom-2 && r.width>innerWidth-40 && getComputedStyle(document.getElementById('tToggleBtn')).display==='none'}"))
else:
ok('℠900 px : trois colonnes (liste · image · lecture)', E("()=>{const sb=document.getElementById('tSidebar').getBoundingClientRect(),v=document.querySelector('.t-viewer').getBoundingClientRect(),r=document.getElementById('tReading').getBoundingClientRect();return sb.right<=v.left+1 && v.right<=r.left+1 && r.width>200}"))
ok('les puces (filtres, scans, chips) â„ 11,5 px et â„ 24 px de haut', E("()=>[...document.querySelectorAll('.t-sf-btn,.t-scan-btn,.t-chip,#tSidebar span')].every(e=>parseFloat(getComputedStyle(e).fontSize)>=11.5) && [...document.querySelectorAll('.t-sf-btn,.t-scan-btn')].every(e=>e.getBoundingClientRect().height>=24)"))
E("()=>document.getElementById('tScanBleu').click()"); pg.wait_for_timeout(900)
ok('scan đ Bleu SacrĂ© : le canvas remplace lâimage', E("()=>document.getElementById('tCanvas').style.display==='block' && document.getElementById('tMainImg').style.display==='none'"))
ok('clic sur le scan â zoom ‹ plein Ă©cran (du canvas), + agrandit, â ferme', E("()=>{tOpenLightbox();const Z=document.getElementById('ami-zoom');if(!Z.classList.contains('on'))return false;const I=Z.querySelector('img');const t0=I.style.transform;Z.querySelector('[data-z=\"+\"]').click();const t1=I.style.transform;Z.querySelector('[data-z=x]').click();return I.src.startsWith('data:image/png') && t0!==t1 && !Z.classList.contains('on') && !document.getElementById('tLightbox')}"))
E("()=>document.getElementById('tScanNormal').click()"); pg.wait_for_timeout(300)
E("()=>document.querySelector('.t-nav.next').click()"); pg.wait_for_timeout(300)
ok('âș passe au folio suivant', E("()=>tCurIdx===9 && document.getElementById('tVmeta').textContent.length>0"))
E("()=>document.querySelectorAll('.t-sf-btn')[4].click()"); pg.wait_for_timeout(300)
ok('filtre đ BalnĂ©ologique : 4 folios', E("()=>document.querySelectorAll('#tSidebar [onclick]').length===4"))
E("()=>document.querySelectorAll('.t-sf-btn')[0].click()"); pg.wait_for_timeout(200)
# ââ đ§ â â¶ zoom
E("()=>switchTab(null,'cadre')"); pg.wait_for_timeout(200)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
ok('đ§ lecture continue : 10 volets dĂ©roulĂ©s, goutte pleine', E("()=>document.querySelectorAll('.tab-section.active').length===10 && document.getElementById('amiLecture').getAttribute('aria-pressed')==='true' && getComputedStyle(document.querySelector('#amiLecture svg')).fill!=='none'"))
E("()=>{document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(300) # on éteint d'abord
E("()=>{switchTab(null,'tuto');}"); pg.wait_for_timeout(500)
E("()=>{const s=document.getElementById('tab-tuto');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+600,behavior:'instant'})}"); pg.wait_for_timeout(300)
_on=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-section'))break;}el.setAttribute('data-ami-on','1');return el.getBoundingClientRect().top}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(900)
ok('đ§ allumĂ©e depuis un volet du milieu : le point lu ne bouge pas (les volets dâavant sâouvrent au-dessus sans pousser la lecture) â Diane 19/09 23h40', pg.evaluate("(t)=>{const el=document.querySelector('[data-ami-on]');return Math.abs(el.getBoundingClientRect().top-t)<3}", _on), str(pg.evaluate("(t)=>[document.querySelector('[data-ami-on]').getBoundingClientRect().top,t]", _on)))
E("()=>{const s=document.getElementById('tab-tuto');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+900,behavior:'instant'})}"); pg.wait_for_timeout(300)
_anc=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-section'))break;}document.querySelectorAll('[data-ami-anc]').forEach(e=>e.removeAttribute('data-ami-anc'));el.setAttribute('data-ami-anc','1');return [el.getBoundingClientRect().top, window.pageYOffset, el.closest('.tab-section,.tab-panel').id, el.closest('.tab-section,.tab-panel').getBoundingClientRect().top]}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(700)
ok('đ§ recliquĂ©e depuis le bas : on reste oĂč on lisait (le chapitre sous les yeux sâouvre, le point lu ne bouge pas) â Diane 19/09 23h', pg.evaluate("(a)=>{const el=document.querySelector('[data-ami-anc]');const s=document.getElementById(a[2]);const r=(el||s).getBoundingClientRect();return Math.abs(r.top-(el?a[0]:a[3]))<3 && s.classList.contains('active')}", _anc))
E("()=>{switchTab(null,'cadre');window.scrollTo(0,0);document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(400)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : les 2 volets du Cadre', E("()=>[...document.querySelectorAll('.tab-section.active')].map(s=>s.id).join()==='tab-cadre,tab-thermo'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
ok('â contraste : body.ami-contraste, cercle plein', E("()=>document.body.classList.contains('ami-contraste') && getComputedStyle(document.querySelector('#amiContraste svg circle')).fill!=='none'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('â au repos : demi-cercle', E("()=>getComputedStyle(document.querySelector('#amiContraste svg circle')).fill==='none' && getComputedStyle(document.querySelector('#amiContraste svg path')).fill!=='none'"))
ok('đ§ Ă gauche (.5rem) puis â (2.9rem) ; â¶ Ă droite', E("()=>{const l=document.getElementById('amiLecture').getBoundingClientRect(),c=document.getElementById('amiContraste').getBoundingClientRect(),p=document.getElementById('amiPlein').getBoundingClientRect();return l.left<c.left && c.left<60 && p.right>innerWidth-60}"))
pg.click('#amiPlein'); pg.wait_for_timeout(700)
ok('â¶ plein Ă©cran : la racine dĂ©file, la barre est dedans, zoom visible, â', E("()=>{const fe=document.fullscreenElement;return fe && fe.id==='ami-decryptage' && getComputedStyle(fe).overflowY==='auto' && fe.contains(document.getElementById('amiBarre')) && getComputedStyle(document.querySelector('.coin-zoom')).display==='flex' && document.getElementById('amiPlein').textContent==='â'}"))
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
ok('zoom + : 115 %', E("()=>document.getElementById('amiZoomV').textContent==='115%'"))
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(100)
pg.click('#amiPlein'); pg.wait_for_timeout(600)
ok('sortie du plein écran : la barre revient avant la racine', E("()=>!document.fullscreenElement && document.getElementById('amiBarre').nextElementSibling.id==='ami-decryptage'"))
# ââ â â° đ + #prog + la pile qui se replie
E("()=>{const v=document.querySelector('.tab-section.active');window.scrollTo({top:v.getBoundingClientRect().top+window.pageYOffset+1200,behavior:'instant'})}"); pg.wait_for_timeout(500)
ok('â paraĂźt aprĂšs 600 px, â° et đ en bas Ă droite (de bas en haut đ â° â)', E("()=>{const g=id=>document.getElementById(id).getBoundingClientRect();const r=g('amiRech'),t=g('ami-toc-toggle'),h=g('amiHaut');return !document.getElementById('amiHaut').hidden && r.bottom>t.bottom && t.bottom>h.bottom && r.right>innerWidth-40}"))
ok('#prog avance en descendant', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>{const tb=document.getElementById('amiPile');window.scrollTo({top:tb.offsetTop+tb.offsetHeight+400,behavior:'instant'})}"); pg.wait_for_timeout(200)
E("()=>window.scrollBy(0,60)"); pg.wait_for_timeout(300)
ok('la pile se replie en descendant : une ligne « oĂč je suis » sous la barre, les onglets ne collent plus', E("()=>{const m=document.getElementById('ami-pile-mini');const r=m.getBoundingClientRect();return document.body.classList.contains('ami-repli') && getComputedStyle(m).display==='flex' && r.height>=40 && r.top<80 && /Cadre/.test(m.textContent) && getComputedStyle(document.getElementById('amiPile')).position==='static'}"))
E("()=>window.scrollBy(0,-60)"); pg.wait_for_timeout(250)
ok('remonter de 40 px redéploie les onglets', E("()=>!document.body.classList.contains('ami-repli') && getComputedStyle(document.getElementById('ami-pile-mini')).display==='none' && getComputedStyle(document.getElementById('amiPile')).position==='sticky'"))
E("()=>window.scrollBy(0,300)"); pg.wait_for_timeout(250)
E("()=>document.getElementById('ami-pile-mini').click()"); pg.wait_for_timeout(900)
ok('un tap sur la ligne remonte aux onglets et les redéploie', E("()=>{const tb=document.getElementById('amiPile').getBoundingClientRect();return !document.body.classList.contains('ami-repli') && tb.top>=-2 && tb.top<120}"))
E("()=>document.getElementById('amiHaut').click()"); pg.wait_for_timeout(1600)
ok('â ramĂšne en haut', E("()=>window.pageYOffset<40"))
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(300)
ok('Ⱐsommaire : 5 onglets, 10 volets nommés', E("()=>!document.getElementById('ami-toc-panel').hidden && document.querySelectorAll('#ami-toc-panel .toc-o').length===5 && document.querySelectorAll('#ami-toc-panel .toc-s a').length===10 && [...document.querySelectorAll('#ami-toc-panel .toc-s a')].every(a=>a.textContent.length>3 && !/^tab-/.test(a.textContent))"))
E("()=>document.querySelectorAll('#ami-toc-panel .toc-s a')[5].click()"); pg.wait_for_timeout(900)
ok('un lien du sommaire ouvre lâonglet et le volet (le visualiseur)', E("()=>document.getElementById('ami-toc-panel').hidden && document.getElementById('tab-tomo').classList.contains('active') && document.querySelector('#tabBar .tab-btn.active').textContent.includes('Tomographique')"))
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', 'placebo'); pg.wait_for_timeout(500)
ok('đ « placebo » trouve des passages, groupĂ©s par onglet âș volet', E("()=>document.querySelectorAll('#rc-l .rc-r').length>0 && [...document.querySelectorAll('#rc-l .rc-g')].some(g=>g.textContent.includes('âș'))"))
E("()=>document.querySelector('#rc-l .rc-r').click()"); pg.wait_for_timeout(900)
ok('un rĂ©sultat ouvre lâonglet et le volet, marque le passage visible', E("()=>{const t=document.querySelector('.ami-trouve');if(!t)return false;const r=t.getBoundingClientRect();return !document.getElementById('ami-rech').classList.contains('on') && r.width>0 && r.top>=0 && r.top<innerHeight}"))
# ââ forme
ok('reduced-motion écrit ; fadeIn sans translation', E("()=>[...document.querySelectorAll('style')].some(s=>/prefers-reduced-motion/.test(s.textContent)) && [...document.styleSheets].some(ss=>{try{return [...ss.cssRules].some(r=>r.name==='fadeIn' && ![...r.cssRules].some(k=>/translate/.test(k.style.transform)))}catch(e){return false}})"))
ok('aucun émoji en italique', E("()=>[...document.querySelectorAll('#ami-decryptage .e')].every(e=>getComputedStyle(e).fontStyle!=='italic')"))
ok('11 tableaux en rouleau (.tw)', E("()=>document.querySelectorAll('#ami-decryptage .tw>table').length===11"))
ok('sĂ©lecteurs nus scopĂ©s : le body du document hĂŽte nâest peint que via :has', E("()=>![...document.styleSheets].some(ss=>{try{return [...ss.cssRules].some(r=>r.selectorText && /^(body|h1|h2|h3|p|strong|em)$/.test(r.selectorText.trim()))}catch(e){return false}})"))
_pl=E("()=>{document.querySelectorAll('.tab-section').forEach(s=>s.classList.add('active'));let n=0;const out={};const w=document.createTreeWalker(document.getElementById('ami-decryptage'),NodeFilter.SHOW_TEXT);let t;while((t=w.nextNode())){if(t.textContent.trim().length<2)continue;const el=t.parentElement;if(!el||el.closest('script,style,svg,.ami-barre,.loupe-layer'))continue;const cs=getComputedStyle(el);if(cs.display==='none')continue;if(parseFloat(cs.fontSize)<11.5){n++;const k=el.tagName+'.'+String(el.className).slice(0,20)+' '+cs.fontSize;out[k]=(out[k]||0)+1}}return n+' '+JSON.stringify(out)}")
ok('plancher 11,5 px tenu dans la racine (tous volets dépliés)', _pl.startswith('0 '), _pl[:300])
ok('0 erreur JS', not [e for e in errs if 'ERR_' not in e], str(errs[:2])); pg.close()
for W in (320, 390, 768, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(800)
E = lambda js: pg.evaluate(js)
ok(f'pas de débordement à {W} px (tous volets dépliés, hors rouleaux)', E("()=>{document.querySelectorAll('.tab-section').forEach(s=>s.classList.add('active'));if(window.tRenderSidebar)tRenderSidebar();const w=document.documentElement.clientWidth;let bad=[];document.querySelectorAll('#ami-decryptage *').forEach(el=>{if(el.closest('svg,.loupe-layer,.loupe-ring'))return;let a=el.parentElement,roule=false;while(a&&a!==document.body){const o=getComputedStyle(a).overflowX;if(o==='auto'||o==='scroll'||o==='hidden'){roule=true;break}a=a.parentElement}if(roule)return;const r=el.getBoundingClientRect();if(r.width<1)return;if(r.right>w+1||r.left<-1)bad.push(el.tagName+'.'+String(el.className).slice(0,20))});return bad.length===0 && document.documentElement.scrollWidth<=w}"))
pg.close()
b.close()
n = sum(1 for r in R if r[1]); print(f'\n{n} / {len(R)} réussis')
if n < len(R): print('â ratĂ©s :', [r[0] for r in R if not r[1]])
p7_arbre_cosmo.py python3 p7_arbre_cosmo.py entree.html sortie.html arbre.webp17 lignes
CosmoLogie : l'Arbre Culturel (export WebP de Diane) recompressé 5200 px q80 et versé en base64 ; bouton ‹ vers la visionneuse zoom.
â§ Copier le script
#!/usr/bin/env python3
"""p7_arbre_cosmo.py src out image.webp â 3.3 CosmoLogie : remplace l'Arbre Culturel (WebP 2600 px du 9 aoĂ»t)
par l'export du 19/09 de Diane (7365Ă6457) recompressĂ© pour le zoom (largeur choisie en amont, q80).
Ne touche qu'à la premiÚre <img ⊠data:image/webp> dont l'alt commence par « L'Arbre Culturel »."""
import sys,re,base64
from PIL import Image
src,out,img=sys.argv[1:4]
h=open(src,encoding='utf-8').read()
b=base64.b64encode(open(img,'rb').read()).decode()
im=Image.open(img); w,ht=im.size
m=re.search(r'<img src="data:image/webp;base64,[^"]+" alt="L\'Arbre Culturel[^"]*"',h)
assert m,'img Arbre introuvable'
new=f'<img src="data:image/webp;base64,{b}" width="{w}" height="{ht}" alt="L\'Arbre Culturel â de la SpiritualitĂ© Ă la Religion (export du 19 septembre 2026, {w}Ă{ht})"'
h=h[:m.start()]+new+h[m.end():]
open(out,'w',encoding='utf-8').write(h)
print(f'â Arbre Culturel remplacĂ© : {w}Ă{ht}, {len(b)*3//4//1024} Ko')
amiblocs.py import amiblocs (à cÎté des p1_*)65 lignes
PartagĂ© : prefixe_css(css, root) (feuille prĂ©fixĂ©e par une racine, :root/html/body â racine, @media rĂ©cursif), bloc_equilibre(h, i, ouvre, ferme), page_en_bloc(html, root) (une page seule â <div id=root> feuille + corps ; lit le DERNIER </body>), ote_postmessage(js).
â§ Copier le script
# -*- coding: utf-8 -*-
"""Petits outils partagés : préfixer une feuille CSS par une racine, extraire un bloc équilibré, faire d'une page seule un BLOC."""
import re
def prefixe_css(css, root):
"""Chaque sélecteur reçoit la racine devant ; :root/html/body deviennent la racine ; @media récursif ; @keyframes/@font-face intacts."""
out = []; i = 0; n = len(css)
while i < n:
j = css.find('{', i)
if j < 0: out.append(css[i:]); break
sel = css[i:j]; k = j + 1; depth = 1
while k < n and depth:
if css[k] == '{': depth += 1
elif css[k] == '}': depth -= 1
k += 1
corps = css[j + 1:k - 1]
s = sel.strip()
if s.startswith('@media') or s.startswith('@container') or s.startswith('@supports'):
out.append(sel + '{' + prefixe_css(corps, root) + '}')
elif s.startswith('@'):
out.append(sel + '{' + corps + '}')
else:
parts = []
for p in s.split(','):
p = p.strip()
if not p: continue
p = re.sub(r'^(:root|html|body)(?![\w-])', root, p)
if p.startswith(root): parts.append(p)
elif p.startswith('*'): parts.append(root + ' ' + p)
else: parts.append(root + ' ' + p)
pre = sel[:len(sel) - len(sel.lstrip())]
out.append(pre + ',\n'.join(parts) + '{' + corps + '}')
i = k
return ''.join(out)
def bloc_equilibre(h, debut, ouvre='<section', ferme='</section>'):
"""De l'index `debut` (sur une balise ouvrante) jusqu'à la fermeture équilibrée ; renvoie (debut, fin)."""
depth = 0
for m in re.finditer(re.escape(ouvre) + r'\b|' + re.escape(ferme), h[debut:]):
depth += 1 if m.group(0).startswith(ouvre) else -1
if depth == 0: return debut, debut + m.end()
raise ValueError('bloc non fermé')
def page_en_bloc(html, root, titre=None):
"""Une page seule (html/head/body) â <div id="root"> feuille prĂ©fixĂ©e + corps. Les <link> et <meta> restent dehors."""
styles = re.findall(r'<style\b[^>]*>(.*?)</style>', re.sub(r'<script\b.*?</script>', '', html, flags=re.S), re.S)
mb = re.search(r'<body[^>]*>', html); body = html[mb.end():html.rfind('</body>')] # le DERNIER </body> : un script peut en écrire un (impression)
body = re.sub(r'<style\b[^>]*>.*?</style>(?![^<]*`)', '', body, flags=re.S) # pas une feuille écrite dans un gabarit JS
css = '\n'.join(prefixe_css(s, '#' + root) for s in styles)
return f'<div id="{root}">\n<style>\n{css}\n</style>\n{body}\n</div>'
def ote_postmessage(js):
"""Ăte les blocs `if(window.parent&&window.parent!==window){âŠ}` (restes de l'Ă©poque iframe)."""
out = js; n = 0
while True:
m = re.search(r'if\s*\(\s*window\.parent\s*&&\s*window\.parent\s*!==\s*window\s*\)\s*\{', out)
if not m: break
k = m.end(); depth = 1
while k < len(out) and depth:
if out[k] == '{': depth += 1
elif out[k] == '}': depth -= 1
k += 1
out = out[:m.start()] + out[k:]; n += 1
return out, n
p1_ego.py · p2_ego.py · p4_ego.py · p5_ego.py bash chaine_ego.sh (ego.html + Widgets V6/Stop-R/Plan/Témoignage à cÎté)1 lignes
EGO : texte (đĄïž, Question/RĂ©ponse, clause, RepĂšre V6 entrĂ© en bloc Ă la place des copies sĂ©parĂ©es, Stop-R/Plan/TĂ©moignage entrĂ©s, un seul RĂ©el sĂ©parĂ©, conclusion), forme (h1 â h2.ami-h1 dans les feuilles, 6 racines normalisĂ©es, onglets 2+2+1, .e inline), anatomie V16 (coque maison ĂŽtĂ©e, pile, switchTab Ă la place naturelle), encres (--or-somb â --or, rouges â -ct, ::before de l'ancienne bande, postMessage, SVG ConScience).
â§ Copier le script
(script absent du dossier outils/)
p6_ego_mutations.py python3 p6_ego_mutations.py page.html119 lignes
EGO : 96 mutations (identitĂ©, conclusion, barre/bande/pied/Emporter/Formule, onglets et place naturelle, RepĂšre V6, Ouverture, Stop-R, Plan, TĂ©moignage âïž, RĂ©el sĂ©parĂ© unique, Test institutionnel, đ§ â â¶ zoom, â â° đ, pile, plancher, dĂ©bordement 4 largeurs).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 1.1 EGO â la mutation des contrĂŽles : on presse chaque commande et on regarde si la page a changĂ© comme promis.
IdentitĂ© (đĄïž, Question sur la une / RĂ©ponse en conclusion, clause), barre/pied/bande/Emporter/Formule, 5 onglets (place naturelle
au clic), les instruments (RepÚre du Consentement V6 · Ouverture à la Révision · Stop-R · Plan d'Attention · Témoignage Guidé ·
RĂ©el sĂ©parĂ© · Test institutionnel), đ§ â â¶ zoom, â â° đ, #prog, la pile qui se replie, plein Ă©cran, plancher, dĂ©bordement,
0 var sans dĂ©claration, 0 erreur JS â Ă 390 et 1280."""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'ego_p6.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1200)
print(f'=== {W} px ==='); E = lambda js: pg.evaluate(js)
# ââ identitĂ©
ok('meta ami:volume 1.1 · ami:version V31 · ami:date 2026-09-19', E("()=>['1.1','V31','2026-09-19'].every((v,i)=>document.querySelector('meta[name=\"ami:'+['volume','version','date'][i]+'\"]').content===v)"))
ok('titre : 1.1 EGO · Evidence Engine â Tu Sais ProtĂ©ger', E("()=>document.title.startsWith('1.1 EGO · Evidence Engine â Tu Sais ProtĂ©ger')"))
ok('un seul h1 (la une) ; les volets et le RepĂšre en h2.ami-h1', E("()=>document.querySelectorAll('h1').length===1 && document.querySelector('h1').classList.contains('couv-h1') && document.querySelectorAll('h2.ami-h1').length>=7"))
ok('la une : đĄïž, EGO, la Question « Comment je me sĂ©curise ? », 5 onglets annoncĂ©s', E("()=>{const c=document.querySelector('.couverture');return c.querySelector('.logo').textContent.trim()==='đĄïž' && /EGO/.test(c.querySelector('.couv-h1').textContent) && /Comment je me sĂ©curise/.test(c.querySelector('.couv-champ').textContent) && /Souverain de Sa DignitĂ©/.test(c.querySelector('.couv-devise').textContent) && (c.querySelector('.couv-tags').textContent.match(/·/g)||[]).length===4 && /PsychĂ©dĂ©liCare/.test(c.querySelector('.couv-tags').textContent)}"))
ok('la conclusion porte la RĂ©ponse « đĄïž Tu Sais ProtĂ©ger. » et la clause (apartheid, esclavage, viol conjugal ; le droit suit lâĂ©thique)', E("()=>{const c=document.querySelector('#ami-ego .ami-conclusion');return c && /Tu Sais ProtĂ©ger\\./.test(c.querySelector('.ac-r').textContent) && /apartheid/.test(c.textContent) && /viol conjugal/.test(c.textContent) && /Le droit suit lâĂ©thique/.test(c.textContent) && !/ni un diagnostic mĂ©dical/.test(document.body.textContent)}"))
ok('aucune var(--x) sans déclaration', E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}for(const el of document.querySelectorAll('[style]')){for(const m of el.getAttribute('style').matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>!decl.has(v)&&!['--cc','--c','--lc','--fx','--fy','--t','--sc'].includes(v)).length===0;}"))
ok('plus aucun lien agdistys.cms.webnode.fr ni fichier .html local ; la Formule en encart', E("()=>!document.documentElement.outerHTML.includes('cms.webnode') && [...document.querySelectorAll('a[href]')].every(a=>!/^[^:]*\\.html/.test(a.getAttribute('href'))) && /ÎI \\/ ÎE/.test(document.querySelector('.ami-formule .af-f').textContent)"))
ok('orthographes : ConScience, TransMission, InCorporation', E("()=>{const t=document.getElementById('ami-ego').innerText;return !/\\bConscience\\b/.test(t) && /ConScience/.test(t) && /TransMission/.test(t) && /InCorporation/.test(document.getElementById('ami-ego').innerHTML)}"))
# ââ la barre, le pied, la bande
ok('barre V16 en tĂȘte, 1.1 marquĂ©e .ici, tiroir fermĂ© ; topbar, bandeau, â¶ â â° maison absents', E("()=>!!document.querySelector('.ami-barre .v.ici[data-v=\"ami-ego\"]') && !document.querySelector('.ami-barre .tiroir.on') && !document.querySelector('.ami-topbar,.ami-bandeau,.ami-fs,.ami-top,#toc-toggle,#floating-toc,#progress-bar,.footer-canon')"))
ok('bande des registres V16 aprĂšs la une, puis la pile des onglets', E("()=>{const b=document.querySelector('#ami-ego .bande-registres');return b && b.previousElementSibling.classList.contains('couverture') && (b.nextElementSibling.id==='amiPile'||b.nextElementSibling.id==='ami-pile-mini') && !b.querySelector('a[href*=\".html\"]')}"))
ok('pied V16 aprĂšs la racine, sceau đĄïž, volume 1.1, « LâĂthique protĂšge »', E("()=>{const f=document.querySelector('.ami-pied');return f && f.previousElementSibling.id==='ami-ego' && f.querySelector('.ap-sceau').textContent.trim()==='đĄïž' && f.textContent.includes('1.1') && f.textContent.includes('Ăthique protĂšge')}"))
ok('â Emporter aprĂšs la Formule, export nommĂ© 1.1_EGO ; conclusion avant la Formule', E("()=>{const e=document.querySelector('#ami-ego .emporter');return e && e.previousElementSibling.classList.contains('ami-formule') && !!document.getElementById('dlHtml') && [...document.scripts].some(s=>s.textContent.includes('1.1_EGO_Evidence-Engine_aMi_')) && !!(document.querySelector('.ami-conclusion').compareDocumentPosition(document.querySelector('.ami-formule'))&Node.DOCUMENT_POSITION_FOLLOWING)}"))
# ââ les onglets
ok('5 onglets, sans rouleau, aucun chevauchement, centrés par rangée', E("()=>{const bs=[...document.querySelectorAll('#ami-ego .tab-btn-custom')];const tb=document.querySelector('#ami-ego .tab-bar');if(bs.length!==5||tb.scrollWidth>tb.clientWidth+1)return false;for(let i=0;i<bs.length;i++)for(let j=i+1;j<bs.length;j++){const a=bs[i].getBoundingClientRect(),b=bs[j].getBoundingClientRect();if(a.left<b.right-1&&b.left<a.right-1&&a.top<b.bottom-1&&b.top<a.bottom-1)return false;}const t=tb.getBoundingClientRect(),rows={};bs.forEach(b=>{const r=b.getBoundingClientRect();const k=Math.round(r.top);rows[k]=rows[k]||{l:1e9,r:0};rows[k].l=Math.min(rows[k].l,r.left);rows[k].r=Math.max(rows[k].r,r.right)});return Object.values(rows).every(o=>Math.abs((o.l-t.left)-(t.right-o.r))<40)}"))
ok('libellés ℠11,5 px ; cibles ℠44 px', E("()=>[...document.querySelectorAll('#ami-ego .tab-btn-custom')].every(b=>parseFloat(getComputedStyle(b).fontSize)>=11.5 && b.getBoundingClientRect().height>=44 && b.getBoundingClientRect().height<110)"))
ok('#amiPile collé (sticky) sous la barre', E("()=>getComputedStyle(document.getElementById('amiPile')).position==='sticky'"))
E("()=>window.scrollTo(0,0)"); pg.wait_for_timeout(200)
E("()=>document.getElementById('btn-bia').click()"); pg.wait_for_timeout(700)
ok('đĄïž Manuel Anti-Biais : seul volet ouvert ; cliquĂ© depuis le haut, la page ne bouge pas', E("()=>[...document.querySelectorAll('#ami-ego .tab-panel.active')].map(s=>s.id).join()==='tab-bia' && document.getElementById('btn-bia').classList.contains('active') && window.pageYOffset<5"))
E("()=>{window.scrollBy(0,2500)}"); pg.wait_for_timeout(300)
E("()=>document.getElementById('btn-eth').click()"); pg.wait_for_timeout(1400)
ok('un onglet cliqué depuis le bas ramÚne sur la pile, pas en haut de page', E("()=>{const b=document.querySelector('#ami-ego .bande-registres').getBoundingClientRect();return window.pageYOffset>200 && b.bottom<=60 && b.bottom>-20 && document.getElementById('tab-eth').classList.contains('active')}"))
# ââ les instruments (âïž Ăthique)
ok('đȘ· RepĂšre du Consentement V6 : 8 vues (test, solo, table, regards, tĂ©moin, spectre, grille, rĂ©el), dans lâonglet Ăthique', E("()=>{const c=document.getElementById('ami-consent');return c && c.closest('.tab-panel').id==='tab-eth' && ['t-test','t-solo','t-table','t-regards','t-temoin','t-spectre','t-grille','t-reel'].every(id=>document.getElementById(id)) && !document.getElementById('outil-ami-violentometre') && !document.getElementById('outil-pane-distinction')}"))
E("()=>document.getElementById('t-solo').click()"); pg.wait_for_timeout(300)
ok('la vue đŸ Solo sâouvre (p-solo visible, p-test cachĂ©), bouton so-go', E("()=>{const s=document.getElementById('p-solo'),t=document.getElementById('p-test');return s.getBoundingClientRect().height>50 && (t.hidden||getComputedStyle(t).display==='none') && !!document.getElementById('so-go')}"))
ok('đ§ Ouverture Ă la RĂ©vision : un « oui » se marque', E("()=>{const b=document.querySelector('#ami-revision .ami-btn[data-v=yes]');const before=b.outerHTML;b.click();return b.outerHTML!==before || b.getAttribute('aria-pressed')==='true' || b.classList.length>1}"))
ok('đ§ Stop-R : signaux rendus ; cocher « Urgence de mort » â rĂ©sultat urgent', E("()=>{const h=document.getElementById('stopr-signals');const bs=[...h.querySelectorAll('input[type=checkbox]')];if(bs.length<5)return false;const r=document.getElementById('stopr-result');const t0=r.textContent;const u=bs.find(x=>x.dataset.alert==='urgent');u.checked=true;u.dispatchEvent(new Event('change',{bubbles:true}));return r.textContent!==t0 && !r.classList.contains('safe')}"))
ok('đ€ Plan dâAttention Solidaire : cocher une action change le rĂ©sultat', E("()=>{const h=document.getElementById('support-actions');const bs=[...h.querySelectorAll('input[type=checkbox]')];if(bs.length<3)return false;const r=document.getElementById('support-result');const t0=r.textContent;bs[0].checked=true;bs[0].dispatchEvent(new Event('change',{bubbles:true}));return r.textContent!==t0}"))
pg.fill('#ami-temoignage #faits', 'Le 3 mars, devant moi, il a crié et jeté un verre.'); pg.fill('#ami-temoignage #nom-victime', 'A.'); pg.wait_for_timeout(100)
E("()=>tgGenerer()"); pg.wait_for_timeout(400)
ok('đĄïž TĂ©moignage GuidĂ© : âïž GĂ©nĂ©rer produit lâattestation (fonctions tg*, sans window.open Ă la gĂ©nĂ©ration)', E("()=>{const o=document.getElementById('output-box');return o && o.textContent.length>80 && /jetĂ© un verre/.test(o.textContent) && typeof tgReinit==='function' && typeof tgCopier==='function'}"))
ok('đȘ la fiche « La Cascade des Tabous » Ă la fin de âïž Ăthique : trois marches, la part rĂ©futable, la ligne', E("()=>{const f=document.getElementById('fiche-cascade-tabous');return f && f.closest('.tab-panel').id==='tab-eth' && f.querySelectorAll('.fc-marche').length===3 && /retirer/.test(f.querySelector('.fc-ligne').textContent) && f.querySelectorAll('ol li').length===3}"))
ok('đ§© RĂ©el sĂ©parĂ© : une seule fois (la vue du RepĂšre V6, la section de Profilage ĂŽtĂ©e) ; đïž Test institutionnel (Anti-Biais) : boutons oui/non', E("()=>!document.getElementById('outil-reel-separe') && document.querySelectorAll('#p-reel input[type=checkbox]').length>=3 && document.querySelectorAll('#outil-test-inst button[data-v]').length>=4"))
# ââ đ§ â â¶ zoom
E("()=>{switchTab('eth');window.scrollTo(0,0)}"); pg.wait_for_timeout(300)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(400)
ok('đ§ lecture continue : 5 volets dĂ©roulĂ©s, goutte pleine', E("()=>document.querySelectorAll('#ami-ego .tab-panel.active').length===5 && document.getElementById('amiLecture').getAttribute('aria-pressed')==='true' && getComputedStyle(document.querySelector('#amiLecture svg')).fill!=='none'"))
E("()=>{document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(300) # on éteint d'abord
E("()=>{switchTab('pro');}"); pg.wait_for_timeout(500)
E("()=>{const s=document.getElementById('tab-pro');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+600,behavior:'instant'})}"); pg.wait_for_timeout(300)
_on=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-panel'))break;}el.setAttribute('data-ami-on','1');return el.getBoundingClientRect().top}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(900)
ok('đ§ allumĂ©e depuis un volet du milieu : le point lu ne bouge pas (les volets dâavant sâouvrent au-dessus sans pousser la lecture) â Diane 19/09 23h40', pg.evaluate("(t)=>{const el=document.querySelector('[data-ami-on]');return Math.abs(el.getBoundingClientRect().top-t)<3}", _on), str(pg.evaluate("(t)=>[document.querySelector('[data-ami-on]').getBoundingClientRect().top,t]", _on)))
E("()=>{const s=document.getElementById('tab-pro');window.scrollTo({top:s.getBoundingClientRect().top+window.pageYOffset+900,behavior:'instant'})}"); pg.wait_for_timeout(300)
_anc=E("()=>{let el=null;const tb=document.getElementById('amiPile');const pb=tb&&tb.getBoundingClientRect().bottom>0?tb.getBoundingClientRect().bottom:0;for(const k of [0.3,0.5,0.7,0.9]){el=document.elementFromPoint(innerWidth/2,pb+(innerHeight-pb)*k);if(el&&el.closest('.tab-panel'))break;}document.querySelectorAll('[data-ami-anc]').forEach(e=>e.removeAttribute('data-ami-anc'));el.setAttribute('data-ami-anc','1');return [el.getBoundingClientRect().top, window.pageYOffset, el.closest('.tab-section,.tab-panel').id, el.closest('.tab-section,.tab-panel').getBoundingClientRect().top]}")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(700)
ok('đ§ recliquĂ©e depuis le bas : on reste oĂč on lisait (le volet sous les yeux sâouvre, le point lu ne bouge pas) â Diane 19/09 23h', pg.evaluate("(a)=>{const el=document.querySelector('[data-ami-anc]');const r=el.getBoundingClientRect();return Math.abs(r.top-a[0])<3 && el.closest('.tab-panel').classList.contains('active') && document.querySelectorAll('#ami-ego .tab-panel.active').length===1}", _anc), str(pg.evaluate("(a)=>{const el=document.querySelector('[data-ami-anc]');return [el.getBoundingClientRect().top,a[0],el.closest('.tab-panel').id,[...document.querySelectorAll('#ami-ego .tab-panel.active')].map(x=>x.id)]}", _anc)))
E("()=>{switchTab('eth');window.scrollTo(0,0);document.getElementById('amiLecture').click()}"); pg.wait_for_timeout(400)
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('đ§ retour : le volet Ăthique seul', E("()=>[...document.querySelectorAll('#ami-ego .tab-panel.active')].map(s=>s.id).join()==='tab-eth'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
ok('â contraste : body.ami-contraste, cercle plein', E("()=>document.body.classList.contains('ami-contraste') && getComputedStyle(document.querySelector('#amiContraste svg circle')).fill!=='none'"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('đ§ Ă gauche (.5rem) puis â (2.9rem) ; â¶ Ă droite', E("()=>{const l=document.getElementById('amiLecture').getBoundingClientRect(),c=document.getElementById('amiContraste').getBoundingClientRect(),p=document.getElementById('amiPlein').getBoundingClientRect();return l.left<c.left && c.left<60 && p.right>innerWidth-60}"))
pg.click('#amiPlein'); pg.wait_for_timeout(700)
ok('â¶ plein Ă©cran : la racine dĂ©file, la barre est dedans, zoom visible, â', E("()=>{const fe=document.fullscreenElement;return fe && fe.id==='ami-ego' && getComputedStyle(fe).overflowY==='auto' && fe.contains(document.getElementById('amiBarre')) && getComputedStyle(document.querySelector('.coin-zoom')).display==='flex' && document.getElementById('amiPlein').textContent==='â'}"))
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
ok('zoom + : 115 %', E("()=>document.getElementById('amiZoomV').textContent==='115%'"))
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(100)
pg.click('#amiPlein'); pg.wait_for_timeout(600)
ok('sortie du plein écran : la barre revient avant la racine', E("()=>!document.fullscreenElement && document.getElementById('amiBarre').nextElementSibling.id==='ami-ego'"))
# ââ â â° đ + #prog + la pile qui se replie
E("()=>{const v=document.querySelector('#ami-ego .tab-panel.active');window.scrollTo({top:v.getBoundingClientRect().top+window.pageYOffset+1200,behavior:'instant'})}"); pg.wait_for_timeout(500)
ok('â paraĂźt aprĂšs 600 px, â° et đ en bas Ă droite (de bas en haut đ â° â)', E("()=>{const g=id=>document.getElementById(id).getBoundingClientRect();const r=g('amiRech'),t=g('ami-toc-toggle'),h=g('amiHaut');return !document.getElementById('amiHaut').hidden && r.bottom>t.bottom && t.bottom>h.bottom && r.right>innerWidth-40}"))
ok('#prog avance en descendant', E("()=>parseFloat(document.getElementById('prog').style.width)>0"))
E("()=>{const tb=document.getElementById('amiPile');window.scrollTo({top:tb.offsetTop+tb.offsetHeight+400,behavior:'instant'})}"); pg.wait_for_timeout(200)
E("()=>window.scrollBy(0,60)"); pg.wait_for_timeout(300)
ok('la pile se replie en descendant : « âïž Ăthique ⎠» sous la barre, les onglets ne collent plus', E("()=>{const m=document.getElementById('ami-pile-mini');const r=m.getBoundingClientRect();return document.body.classList.contains('ami-repli') && getComputedStyle(m).display==='flex' && r.height>=40 && r.top<80 && /Ăthique/.test(m.textContent) && getComputedStyle(document.getElementById('amiPile')).position==='static'}"))
E("()=>window.scrollBy(0,-60)"); pg.wait_for_timeout(250)
ok('remonter de 40 px redéploie les onglets', E("()=>!document.body.classList.contains('ami-repli') && getComputedStyle(document.getElementById('ami-pile-mini')).display==='none' && getComputedStyle(document.getElementById('amiPile')).position==='sticky'"))
E("()=>window.scrollBy(0,300)"); pg.wait_for_timeout(250)
E("()=>document.getElementById('ami-pile-mini').click()"); pg.wait_for_timeout(900)
ok('un tap sur la ligne remonte aux onglets et les redéploie', E("()=>{const tb=document.getElementById('amiPile').getBoundingClientRect();return !document.body.classList.contains('ami-repli') && tb.top>=-2 && tb.top<120}"))
E("()=>document.getElementById('amiHaut').click()"); pg.wait_for_timeout(1600)
ok('â ramĂšne en haut', E("()=>window.pageYOffset<40"))
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(300)
ok('Ⱐsommaire : 5 onglets, leurs chapitres nommés (℠20)', E("()=>!document.getElementById('ami-toc-panel').hidden && document.querySelectorAll('#ami-toc-panel .toc-o').length===5 && document.querySelectorAll('#ami-toc-panel .toc-s a').length>=20 && [...document.querySelectorAll('#ami-toc-panel .toc-s a')].every(a=>a.textContent.length>3)"))
_n = E("()=>{const as=[...document.querySelectorAll('#ami-toc-panel .toc-s a')];const a=as.find(x=>x.dataset.t==='sem');a.click();return a.dataset.a}"); pg.wait_for_timeout(1200)
ok('un lien du sommaire ouvre lâonglet †SĂ©miologie et descend au chapitre, sous la pile', pg.evaluate("(id)=>{const el=document.getElementById(id);const r=el.getBoundingClientRect();return document.getElementById('ami-toc-panel').hidden && document.getElementById('tab-sem').classList.contains('active') && document.querySelector('#ami-ego .tab-btn-custom.active').id==='btn-sem' && r.top>=0 && r.top<innerHeight*0.6}", _n))
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(200); pg.fill('#rc-q', 'darvo'); pg.wait_for_timeout(500)
ok('đ « darvo » trouve des passages, groupĂ©s par onglet âș chapitre', E("()=>document.querySelectorAll('#rc-l .rc-r').length>0 && [...document.querySelectorAll('#rc-l .rc-g')].some(g=>g.textContent.includes('âș'))"))
E("()=>document.querySelector('#rc-l .rc-r').click()"); pg.wait_for_timeout(900)
ok('un rĂ©sultat ouvre lâonglet, marque le passage visible', E("()=>{const t=document.querySelector('.ami-trouve');if(!t)return false;const r=t.getBoundingClientRect();return !document.getElementById('ami-rech').classList.contains('on') && r.width>0 && r.top>=0 && r.top<innerHeight && t.closest('.tab-panel').classList.contains('active')}"))
# ââ forme
ok('reduced-motion écrit ; shieldGleam et pulse sans translation, ℠7 s', E("()=>[...document.querySelectorAll('style')].some(s=>/prefers-reduced-motion/.test(s.textContent)) && ![...document.styleSheets].some(ss=>{try{return [...ss.cssRules].some(r=>(r.name==='shieldGleam'||r.name==='pulse') && [...r.cssRules].some(k=>/translate|rotate/.test(k.style.transform)))}catch(e){return false}}) && parseFloat(getComputedStyle(document.querySelector('#ami-ego .couverture'),'::after').animationDuration)>=7"))
ok('aucun émoji en italique ni en bloc', E("()=>[...document.querySelectorAll('#ami-ego .e')].every(e=>{const cs=getComputedStyle(e);return cs.fontStyle!=='italic' && (cs.display!=='block' || /flex|grid/.test(getComputedStyle(e.parentElement).display))})"))
ok('sĂ©lecteurs nus scopĂ©s : le body du document hĂŽte nâest peint que via :has', E("()=>![...document.styleSheets].some(ss=>{try{return [...ss.cssRules].some(r=>r.selectorText && /^(body|h1|h2|h3|p|strong|em)$/.test(r.selectorText.trim()))}catch(e){return false}})"))
_pl=E("()=>{document.querySelectorAll('#ami-ego .tab-panel').forEach(s=>s.classList.add('active'));let n=0;const out={};const w=document.createTreeWalker(document.getElementById('ami-ego'),NodeFilter.SHOW_TEXT);let t;while((t=w.nextNode())){if(t.textContent.trim().length<2)continue;const el=t.parentElement;if(!el||el.closest('script,style,svg,.ami-barre'))continue;const cs=getComputedStyle(el);if(cs.display==='none')continue;if(parseFloat(cs.fontSize)<11.5){n++;const k=el.tagName+'.'+String(el.className).slice(0,20)+' '+cs.fontSize;out[k]=(out[k]||0)+1}}return n+' '+JSON.stringify(out)}")
ok('plancher 11,5 px tenu dans la racine (tous volets dépliés)', _pl.startswith('0 '), _pl[:300])
ok('0 erreur JS', not [e for e in errs if 'ERR_' not in e], str(errs[:2])); pg.close()
for W in (320, 390, 768, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(800)
E = lambda js: pg.evaluate(js)
ok(f'pas de débordement à {W} px (tous volets dépliés, hors rouleaux)', E("()=>{document.querySelectorAll('#ami-ego .tab-panel').forEach(s=>s.classList.add('active'));const w=document.documentElement.clientWidth;let bad=[];document.querySelectorAll('#ami-ego *').forEach(el=>{if(el.closest('svg'))return;let a=el.parentElement,roule=false;while(a&&a!==document.body){const o=getComputedStyle(a).overflowX;if(o==='auto'||o==='scroll'||o==='hidden'){roule=true;break}a=a.parentElement}if(roule)return;const r=el.getBoundingClientRect();if(r.width<1)return;if(r.right>w+1||r.left<-1)bad.push(el.tagName+'.'+String(el.className).slice(0,20))});return bad.length===0 && document.documentElement.scrollWidth<=w}"))
pg.close()
b.close()
n = sum(1 for r in R if r[1]); print(f'\n{n} / {len(R)} réussis')
if n < len(R): print('â ratĂ©s :', [r[0] for r in R if not r[1]])
chaine_ego.sh bash chaine_ego.sh15 lignes
Rejoue la chaĂźne EGO p1 â p5 + p15 'ego' d'un trait (p3_palette Ă6 racines, p19, p2_plancher inclus).
â§ Copier le script
set -e -o pipefail
python3 p1_ego.py ego.html ego_p1.html >/dev/null
python3 p2_ego.py ego_p1.html ego_p2.html | tail -2
cp ego_p2.html ego_p3.html
python3 p3_palette.py rapatrier ego_p3.html '#ami-ego' >/dev/null
for r in '#ami-ego' '#ami-consent' '#ami-defenses' '#ami-stopr' '#ami-plan' '#ami-temoignage'; do python3 p3_palette.py repeindre ego_p3.html "$r" >/dev/null; done
python3 p4_ego.py ego_p3.html ego_p4.html | tail -1
python3 ../acc/p19_plein_ecran_v16.py ego_p4.html ego_p4b.html ami-ego >/dev/null
cp ego_p4b.html ego_p5.html
python3 p2_plancher.py ego_p5.html 390 ami-ego 2>&1 | tail -1
python3 p2_plancher.py ego_p5.html 1280 ami-ego 2>&1 | tail -1
python3 p5_ego.py ego_p5.html ego_p6.html | tail -1
cp ego_p6.html 1.1_EGO_Evidence-Engine_aMi_V31.html
python3 ../ini/p15_cadre_webnode.py 1.1_EGO_Evidence-Engine_aMi_V31.html 1.1_EGO_Evidence-Engine_aMi_V31_WEBNODE.html ego
profil.py python3 profil.py page.html (audit_statique.py + audit_rendu_leger.py à cÎté)72 lignes
Le profil d'une page en un coup d'Ćil : identitĂ©, coque, onglets, images, liens, palette, orthographes + les lignes qui comptent de l'audit statique et de l'audit au rendu (petits, contrastes, dĂ©bordements) â profil_page.txt.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Le profil d'une page en un coup d'Ćil : ce que l'audit statique et l'audit au rendu disent, condensĂ© pour Ă©crire l'audit.
Usage : python3 profil.py page.html â profil_page.txt (+ as_page.txt, ar_page.txt, audit_rendu_page.json, captures)"""
import re, sys, os, json, html, collections, subprocess, hashlib
SRC = sys.argv[1]; K = os.path.splitext(os.path.basename(SRC))[0]
h = open(SRC, encoding='utf-8').read()
def txt(s): return html.unescape(re.sub(r'<[^>]+>', ' ', s))
out = []
P = out.append
P(f'## {SRC} · {len(h)//1024} Ko · sha {hashlib.sha256(open(SRC,"rb").read()).hexdigest()[:16]}')
P('title: ' + ' | '.join(re.findall(r'<title>(.*?)</title>', h, re.S))[:160])
P('metas ami: ' + str(re.findall(r'<meta name="ami:[^"]+" content="[^"]*"', h)))
P('racines: ' + str(re.findall(r'<(?:div|main|section)[^>]*id="(ami-[^"]+)"', h)[:6]))
P('scripts %d · styles %d · onclick %d · h1 %d · h2 %d · h3 %d · tables %d (.tw %d, table-wrap %d) · svg %d · img %d · iframe %d · video/audio %d' % (
h.count('<script'), h.count('<style'), h.count('onclick='), h.count('<h1'), h.count('<h2'), h.count('<h3'), h.count('<table'), h.count('class="tw"'), len(re.findall(r'class="[^"]*table-wrap', h)), h.count('<svg'), h.count('<img'), h.count('<iframe'), h.count('<video') + h.count('<audio')))
imgs = re.findall(r'<img[^>]*src="([^"]{0,80})', h); b64 = sum(len(m) for m in re.findall(r'src="data:[^"]+"', h))
P(f'images: base64 total {b64//1024} Ko · sources non-data : {[i for i in imgs if not i.startswith("data:")][:8]}')
P('iframes src: ' + str(re.findall(r'<iframe[^>]*src="([^"]{0,80})', h)[:6]) + ' · srcdoc ' + str(h.count('srcdoc=')))
P('cms.webnode: %d · liens .html locaux: %d %s' % (h.count('cms.webnode'), len(re.findall(r'href="[^"#:]*\.html', h)), sorted(set(re.findall(r'href="([^"#:]*\.html)', h)))[:12]))
P('liens https: ' + str(sorted(set(re.findall(r'href="(https?://[^"/]+)', h)))[:12]))
P('topnav/topbar/bandeau/footer: %s' % [(k, h.count(k)) for k in ['class="topnav', 'ami-topbar', 'ami-bandeau', '<footer', 'ami-barre', 'ami-pied', 'bande-registres', 'amiPile', 'ami-coin', 'emporter']])
P('localStorage %d · postMessage %d · fetch( %d · requestFullscreen %d · position:fixed %d · min-height:100vh %d · reduced-motion %d' % (h.count('localStorage'), h.count('postMessage'), h.count('fetch('), h.count('requestFullscreen'), h.count('position:fixed') + h.count('position: fixed'), h.count('100vh'), h.count('prefers-reduced-motion')))
# tabs
btns = re.findall(r'<button([^>]*)>(.*?)</button>', h, re.S)
cls = collections.Counter()
for a, t in btns:
m = re.search(r'class="([^"]+)"', a)
if m: cls[m.group(1).split()[0]] += 1
P('boutons par classe: ' + str(cls.most_common(8)))
tabs = [re.sub(r'\s+', ' ', txt(t)).strip()[:40] for a, t in btns if re.search(r'tab', a)]
P(f'onglets ({len(tabs)}): ' + ' | '.join(tabs[:24]))
# une
i = h.find('<body'); body = h[i:i+40000]
P('début du corps: ' + re.sub(r'\s+', ' ', txt(re.sub(r'<(script|style)\b.*?</\1>', '', re.sub(r'src="data:[^"]+"', '', body), flags=re.S)))[:900])
# h2 list
h2 = [re.sub(r'\s+', ' ', txt(m.group(1))).strip()[:70] for m in re.finditer(r'<h2[^>]*>(.*?)</h2>', h, re.S)]
P(f'h2 ({len(h2)}): ' + ' · '.join(h2[:80]))
# footer text
j = h.rfind('<footer');
if j > 0: P('pied: ' + re.sub(r'\s+', ' ', txt(h[j:h.find('</footer>', j)]))[:500])
# palette vars
P('vars déclarées: ' + str(sorted(set(re.findall(r'(--[a-z][a-z0-9-]*)\s*:', h)))[:40]))
P('hex distincts hors scripts/svg: %d' % len(set(re.findall(r'#[0-9a-fA-F]{6}\b', re.sub(r'<script\b.*?</script>|<svg\b.*?</svg>', '', h, flags=re.S)))))
# orthographes
t = txt(re.sub(r'<(script|style)\b.*?</\1>', '', h, flags=re.S))
P('orthographes: ' + str({k: len(re.findall(r'\b' + k + r'\b', t)) for k in ['Conscience', 'conscience', 'Transmission', 'transmission', 'Réflexion', 'réflexion', 'Cosmologie', 'Cosmogonie', 'Thermocognition', 'Incorporation']}))
P('tutoiements approx: %d · années: %s' % (len(re.findall(r"\btu\b|\bton\b|\bta\b|\btes\b", t)), collections.Counter(re.findall(r'\b(20[12]\d)\b', t)).most_common(6)))
# static audit (summary lines)
sa = subprocess.run(['python3', 'audit_statique.py', SRC], capture_output=True, text=True).stdout
open(f'as_{K}.txt', 'w').write(sa)
keep = [l for l in sa.splitlines() if re.search(r'balises Ă©quilibrĂ©es|identifiants en double|ancres mortes|variables employĂ©es sans|confrontĂ©es Ă la palette|hexadĂ©cimaux Ă©crits en dur â (CSS|style)|rgb\(\)/rgba|animations|keyframes|Ă©moji dans une balise|tableaux â|sĂ©lecteurs nus|paragraphes .*deux fois|caractĂšres de zone|poids du fichier|racine â|onclick=', l)]
P('--- statique'); out += [l[:420] for l in keep]
# rendered audit
tops = next(('.' + c for c, n in cls.most_common() if 'tab' in c and n >= 2), '.tab-btn')
r = subprocess.run(['timeout', '580', 'python3', 'audit_rendu_leger.py', SRC, tops, tops], capture_output=True, text=True); ar = r.stdout + '\n[stderr]\n' + r.stderr[-800:]
open(f'ar_{K}.txt', 'w').write(ar)
P(f'--- rendu (onglets {tops})')
for l in ar.splitlines():
if re.search(r'^=== |erreurs JS|^ |contraste|texte <|overflow', l): P(l[:300])
try:
os.replace('audit_rendu.json', f'audit_rendu_{K}.json')
d = json.load(open(f'audit_rendu_{K}.json')); w = d['390']['mesures']
c = collections.Counter((x['tag'][:24], x['fs']) for x in w['petits']); P('petits (390): ' + str(c.most_common(10)))
c = collections.Counter((x['tag'][:24], x.get('fg'), x.get('bg')) for x in w['texte'] if 'r' in x and x['r'] < x['seuil']); P('contrastes (390): ' + str(c.most_common(8)))
c = collections.Counter((x['tag'][:24], x['t'][:12]) for x in w['ital']); P('ital: ' + str(c.most_common(6)))
P('tables débordantes distinctes (390): %d · grilles: %s' % (len(set(x['t'][:40] for x in w['tables'])), collections.Counter(g['cls'] for g in w['grilles']).most_common(5)))
P('overflow: ' + str({k: (v['overflow']['n'], [e['tag'] for e in v['overflow']['ex'][:4]]) for k, v in d.items()}))
except Exception as e: P('json: ' + str(e))
for f in ['capture_390.png', 'capture_1280.png']:
if os.path.exists(f): os.replace(f, f'{K}_{f}')
open(f'profil_{K}.txt', 'w').write('\n'.join(out)); print('\n'.join(out))
fiche_cascade.py · p10_apo_cascade.py python3 p10_apo_cascade.py entree.html sortie.html1 lignes
La fiche « La Cascade des Tabous » en bloc HTML (css(root), html(root, oĂč)) ; entrĂ©e dans EGO par p1_ego, dans Apokalypse par p10.
â§ Copier le script
(script absent du dossier outils/)
page_cq.py page_cq.py315 lignes
GénÚre cette page depuis etat_chantier.json + referentiel.json et le dossier outils/.
â§ Copier le script
# -*- coding: utf-8 -*-
"""page_cq.py â gĂ©nĂšre CONTROLE_QUALITE_aMi.html depuis etat_chantier.json et le dossier outils/.
Une page perso, Ă la charte (onyx, or, Cormorant + Jost, plancher 11,5 px, onglets qui replient), qui tient
l'Ă©tat du chantier, l'historique, les dĂ©cisions, les piĂšges, les mesures et LA BOĂTE Ă OUTILS â chaque script
lisible et copiable. Usage : python3 page_cq.py [etat_chantier.json] [outils/] [sortie.html] [referentiel.json]"""
import json, sys, os, html
ETAT = sys.argv[1] if len(sys.argv) > 1 else 'etat_chantier.json'
OUTILS = sys.argv[2] if len(sys.argv) > 2 else 'outils'
OUT = sys.argv[3] if len(sys.argv) > 3 else 'CONTROLE_QUALITE_aMi.html'
d = json.load(open(ETAT, encoding='utf-8'))
REF = sys.argv[4] if len(sys.argv) > 4 else 'referentiel.json'
r = json.load(open(REF, encoding='utf-8'))
E = html.escape
STATUT = {'fait': ('â
', 'fait', 'emeraude'), 'en cours': ('đ', 'en cours', 'ambre'), 'Ă faire': ('âł', 'Ă faire', 'gris'), 'Ă corriger': ('â ïž', 'Ă corriger', 'rubis')}
def pages():
rows = []
for p in d['pages']:
em, lab, col = STATUT.get(p['statut'], ('·', p['statut'], 'gris'))
rows.append(f'<tr><td><span class="em">{em}</span> <span class="st st-{col}">{E(lab)}</span></td><td><b>{E(p["nom"])}</b><br><code>{E(p["fichier"])}</code></td><td>{E(p["version"])}</td><td>{E(p["date"])}</td><td>{E(p["note"])}</td></tr>')
return '<div class="tw pages-state"><table><colgroup><col class="c-statut"><col class="c-page"><col class="c-version"><col class="c-date"><col class="c-etat"></colgroup><thead><tr><th>Statut</th><th>Page</th><th>Version</th><th>Date</th><th>OĂč on en est</th></tr></thead><tbody>' + ''.join(rows) + '</tbody></table></div>'
def tests_recherches():
chemin = os.path.join(OUTILS, 'section_tests_recherches.html')
if not os.path.exists(chemin):
return '<section id="tests" hidden><h2>Tests & Recherches</h2><p class="lead">Fragment absent : section_tests_recherches.html.</p></section>'
return open(chemin, encoding='utf-8').read()
def liste(items, cls=''):
return f'<ul class="{cls}">' + ''.join(f'<li>{E(x)}</li>' for x in items) + '</ul>'
def historique():
return '<ol class="hist">' + ''.join(f'<li><span class="hd">{E(h["date"])}</span><span class="hq">{E(h["quoi"])}</span></li>' for h in d['historique']) + '</ol>'
def mesures():
rows = ''.join(f'<tr><td>{E(m[0])}</td><td>{E(m[1])}</td><td><b>{E(m[2])}</b></td></tr>' for m in d['mesures_03'])
return '<div class="tw"><table><thead><tr><th>Mesure</th><th>V21 (avant)</th><th>V22 (aprĂšs)</th></tr></thead><tbody>' + rows + '</tbody></table></div>'
def tag(t):
return ''.join(f'<span class="tg em">{E(x)}</span>' for x in t.replace('/', ' ').split())
def groupes(gs, cls='chk'):
out = []
for g in gs:
out.append(f'<details class="{cls}" open><summary>{E(g["t"])}</summary><ul>' + ''.join(f'<li>{tag(t)} {E(x)}</li>' for t, x in g['items']) + '</ul></details>')
return '\n'.join(out)
def legende():
return '<div class="leg">' + ''.join(f'<span><b class="em">{E(a)}</b> {E(b)} â {E(c)}</span>' for a, b, c in r['legende']) + '</div>'
def registre():
rows = ''.join(f'<tr><td><span class="em">{E(a["em"])}</span> <b>{E(a["nom"])}</b><br><a href="{E(a["url"])}" rel="noopener" target="_blank"><code>{E(a["url"].replace("https://www.",""))}</code></a></td><td>{E(a["type"])}</td><td><span class="st st-{"emeraude" if a["statut"]=="accessible" else "ambre"}">{E(a["statut"])}</span></td><td>{E(a["quoi"])}</td></tr>' for a in r['applications']['registre'])
return '<div class="tw"><table><thead><tr><th>Application</th><th>Nature</th><th>Statut</th><th>Ce quâelle fait</th></tr></thead><tbody>' + rows + '</tbody></table></div>'
def socle():
return '\n'.join(f'<details class="chk"><summary>{E(g["t"])} <span class="tg-r">{E(g["tag"])}</span></summary><ul>' + ''.join(f'<li>{E(x)}</li>' for x in g['items']) + '</ul></details>' for g in r['applications']['socle'])
def etats():
return '<ul class="etats">' + ''.join(f'<li><span class="em">{E(a)}</span> <b>{E(b)}</b> â {E(c)}</li>' for a, b, c in r['applications']['etats']) + '</ul>'
def cycle():
return '<div class="cycle">' + ' <span class="fl">â</span> '.join(f'<span>{E(x)}</span>' for x in r['applications']['cycle']) + '</div>'
def outils():
out = []
for nom, usage, quoi in d['outils']:
chemin = os.path.join(OUTILS, nom)
if not os.path.exists(chemin) and os.path.exists(nom): chemin = nom
src = open(chemin, encoding='utf-8').read() if os.path.exists(chemin) else '(script absent du dossier outils/)'
lignes = src.count('\n') + 1
out.append(f'''<details class="outil" id="o-{E(nom.replace('.','-'))}">
<summary><span class="on">{E(nom)}</span><span class="ou"><code>{E(usage)}</code></span><span class="ol">{lignes} lignes</span></summary>
<p class="oq">{E(quoi)}</p>
<div class="ob"><button type="button" class="copie" data-cible="c-{E(nom.replace('.','-'))}">â§ Copier le script</button><span class="etat" aria-live="polite"></span></div>
<pre id="c-{E(nom.replace('.','-'))}"><code>{E(src)}</code></pre>
</details>''')
return '\n'.join(out)
def tableau(entetes, lignes, gras=None):
th = ''.join(f'<th>{E(x)}</th>' for x in entetes)
def cell(i, x):
if gras is not None and i == gras: return f'<td><b>{E(x)}</b></td>'
return f'<td>{E(x)}</td>'
rows = ''.join('<tr>' + ''.join(cell(i, x) for i, x in enumerate(l)) + '</tr>' for l in lignes)
return f'<div class="tw"><table><thead><tr>{th}</tr></thead><tbody>{rows}</tbody></table></div>'
def publication():
q = d.get('publication')
if not q: return '<p class="lead">(pas encore de rĂšgle de publication dans etat_chantier.json)</p>'
return f'''<h2><span class="em">đȘ</span>Publier sur Webnode â le Cadre et le Pont</h2>
<p class="lead">{E(q['intro'])}</p>
{tableau(["Ce qu'on croyait", "Ce qui a été mesuré", "Verdict"], q['mesures'], 2)}
<h3>La rĂšgle</h3>
<blockquote class="principe">{E(q['regle'])}</blockquote>
<h3>Ce que le Pont rebranche (pont_cadre.js)</h3>
{tableau(['Dans la Page seule', 'Dans le Cadre â ce que fait le Pont'], q['pont'])}
<h3>Mesuré dans le simulateur</h3>
{tableau(['Mesure', 'Attendu'], q['simule'], 1)}
<h3>VĂ©rifier en ligne â sans accĂšs au code du bloc</h3>
<p class="lead">{E(q['verifier'])}</p>
{liste(q['checklist'], 'done')}
<h2><span class="em">â</span>Les boutons d\'accessibilitĂ© â places et Ă©tats</h2>
<p class="lead">{E(q['boutons_intro'])}</p>
{tableau(["Place", "Boutons", "Ce qu'ils décident", "Détail"], q['places'], 1)}
{tableau(['Bouton', 'Au repos', 'Actif', 'Comment'], q['etats'], 2)}
{liste(q['boutons_regles'], 'dec')}'''
page = f'''<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="ami:volume" content="atelier"><meta name="ami:version" content="{E(d['mis_a_jour'])}">
<title>đ§° {E(d['titre'])} â {E(d['mis_a_jour'])}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Jost:wght@300;400;500;600&family=Space+Mono&display=swap" rel="stylesheet">
<style>
:root{{--onyx:#09090e;--encre:#111118;--voile:#181826;--nacre:#f2ede2;--or:#ffc545;--or-pale:#ffd06a;--or-somb:#7a611a;--or-rgb:255,197,69;
--gris:#9595a2;--gris-cl:#b0b0bd;--emeraude:#0b8d38;--emeraude-ct:#32b15e;--rubis:#e33232;--rubis-ct:#fe6969;--ambre-ct:#ffd06a;--amethyste:#a04afa;--amethyste-ct:#bb7cfe;--saphir-ct:#7697ee}}
*{{box-sizing:border-box}}
html{{scroll-padding-top:5rem}}
body{{margin:0;background:var(--onyx);color:var(--gris-cl);font:400 1rem/1.65 'Jost',system-ui,sans-serif}}
.em{{font-style:normal;font-family:'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif}}
header{{text-align:center;padding:2.6rem 1.2rem 1.4rem;background:radial-gradient(ellipse at 50% 0%,rgba(var(--or-rgb),.12),transparent 70%)}}
header .k{{font-size:.72rem;letter-spacing:.28em;text-transform:uppercase;color:var(--or)}}
header h1{{font:400 clamp(1.9rem,5vw,3rem)/1.1 'Cormorant Garamond',serif;color:var(--nacre);margin:.3rem 0 .4rem}}
header p{{max-width:44rem;margin:0 auto;font:italic 1.1rem 'Cormorant Garamond',serif;color:var(--gris-cl)}}
header .maj{{margin-top:.8rem;font-size:.78rem;color:var(--gris)}}
nav.tabs{{position:sticky;top:0;z-index:9;display:flex;flex-wrap:wrap;justify-content:center;background:rgba(9,9,14,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--or-somb)}}
nav.tabs button{{flex:1 1 15%;min-width:6.4rem;min-height:44px;padding:.65rem .6rem;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--gris-cl);font:500 .86rem 'Jost',sans-serif;cursor:pointer}}
nav.tabs button .em{{display:block;font-size:1.3rem;line-height:1.2}}
nav.tabs button[aria-selected=true]{{color:var(--or-pale);border-bottom-color:var(--or);background:rgba(var(--or-rgb),.1)}}
nav.tabs button:focus-visible{{outline:2px solid var(--or);outline-offset:-2px}}
.pages-state table{{table-layout:fixed;min-width:68rem}}
.pages-state .c-statut{{width:8%}}.pages-state .c-page{{width:21%}}.pages-state .c-version{{width:10%}}.pages-state .c-date{{width:8%}}.pages-state .c-etat{{width:53%}}
.pages-state td{{overflow-wrap:anywhere}}
nav.subtabs{{display:flex;flex-wrap:wrap;justify-content:center;gap:.35rem;margin:0 0 1.2rem;padding:.45rem;border:1px solid rgba(var(--or-rgb),.18);border-radius:8px;background:var(--encre)}}
nav.subtabs button{{min-height:38px;padding:.45rem .72rem;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--gris-cl);font:500 .78rem 'Jost',sans-serif;cursor:pointer}}
nav.subtabs button[aria-selected=true]{{color:var(--or-pale);border-color:var(--or-somb);background:rgba(var(--or-rgb),.1)}}
.alerte{{margin:1rem 0;padding:.8rem 1rem;border-left:3px solid var(--ambre-ct);background:var(--encre)}}
.v5-merge .rails{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem}}
.v5-merge .rail,.v5-merge .milestone,.v5-merge .integrated-summary,.v5-merge .embedded,.v5-merge .doc{{background:var(--encre);border:1px solid rgba(var(--or-rgb),.18);border-radius:8px;padding:1rem}}
.v5-merge .rail{{border-left:4px solid var(--saphir-ct)}}.v5-merge .rail-accordage{{border-left-color:var(--emeraude-ct)}}.v5-merge .rail-profileur{{border-left-color:var(--or)}}.v5-merge .rail-table_ronde{{border-left-color:var(--amethyste-ct)}}
.v5-merge .rail-head{{display:flex;gap:.7rem;align-items:flex-start}}.v5-merge .sigil{{display:grid;place-items:center;min-width:42px;height:34px;border:1px solid rgba(var(--or-rgb),.22);background:var(--voile);font-weight:600}}
.v5-merge .rail h2{{font-size:1.2rem;margin:.1rem 0}}.v5-merge .rail h3{{font:500 .72rem 'Jost',sans-serif;text-transform:uppercase;letter-spacing:.08em;color:var(--gris);margin:1rem 0 .25rem}}
.v5-merge dl{{display:flex;gap:1rem;margin:.8rem 0}}.v5-merge dl div{{display:flex;gap:.4rem;min-width:0}}.v5-merge dt{{color:var(--gris)}}.v5-merge dd{{margin:0;font-weight:600;overflow-wrap:anywhere}}
.v5-merge .milestones{{display:grid;grid-template-columns:repeat(auto-fit,minmax(165px,1fr));gap:.6rem;margin:1rem 0}}
.v5-merge .milestone b{{display:block;color:var(--emeraude-ct);font-size:1.25rem}}.v5-merge .milestone span{{color:var(--gris);font-size:.76rem}}
.v5-merge .documents{{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.7rem}}.v5-merge .doc-top{{display:flex;justify-content:space-between;gap:.6rem}}.v5-merge .doc-link{{display:inline-block;padding:.35rem .65rem;background:#285f4a;color:#fff;text-decoration:none}}
.v5-merge .embedded{{margin:.7rem 0;padding:0}}.v5-merge .embedded summary{{cursor:pointer;padding:.8rem 1rem;color:var(--or-pale);font-weight:600}}.v5-merge .embedded-content{{padding:.1rem 1rem 1rem;overflow-wrap:anywhere}}
.v5-merge .embedded-table{{overflow-x:auto;border:1px solid rgba(var(--or-rgb),.16);margin:.8rem 0}}.v5-merge .embedded-table table{{min-width:58rem}}
@media(max-width:760px){{.v5-merge .rails,.v5-merge .documents{{grid-template-columns:1fr}}.v5-merge dl{{display:block}}}}
main{{max-width:60rem;margin:0 auto;padding:1.4rem 1rem 4rem}}
section[hidden]{{display:none}}
h2{{font:400 1.8rem/1.15 'Cormorant Garamond',serif;color:var(--or-pale);margin:1.6rem 0 .6rem}}
h2 .em{{margin-right:.3rem}}
p.lead{{font:italic 1.08rem 'Cormorant Garamond',serif;color:var(--gris-cl);max-width:50rem}}
.tw{{overflow-x:auto;border:1px solid rgba(var(--or-rgb),.18);border-radius:10px}}
table{{width:100%;border-collapse:collapse;font-size:.86rem;min-width:40rem}}
th{{text-align:left;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--or);background:rgba(var(--or-rgb),.07);padding:.6rem .7rem;border-bottom:1px solid var(--or-somb)}}
td{{padding:.6rem .7rem;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:top}}
td b{{color:var(--nacre);font-weight:500}}
code{{font-family:'Space Mono',monospace;font-size:.78rem;color:var(--gris)}}
.st{{font-size:.72rem;letter-spacing:.1em;text-transform:uppercase}}
.st-emeraude{{color:var(--emeraude-ct)}}.st-ambre{{color:var(--ambre-ct)}}.st-gris{{color:var(--gris)}}.st-rubis{{color:var(--rubis-ct)}}
ul.dec li,ul.pieges li,ul.val li{{margin:.45rem 0 .45rem 0;padding-left:.2rem}}
ul.dec,ul.pieges,ul.val{{padding-left:1.2rem}}
ul.dec li::marker{{color:var(--or)}}ul.pieges li::marker{{color:var(--rubis-ct)}}ul.val li::marker{{color:var(--amethyste-ct)}}
ol.hist{{list-style:none;padding:0;margin:0;border-left:2px solid var(--or-somb)}}
ol.hist li{{position:relative;padding:.2rem 0 .9rem 1.2rem}}
ol.hist li::before{{content:'';position:absolute;left:-6px;top:.55rem;width:10px;height:10px;border-radius:50%;background:var(--or)}}
.hd{{display:block;font-family:'Space Mono',monospace;font-size:.74rem;color:var(--or-pale);letter-spacing:.04em}}
.hq{{display:block;font-size:.92rem}}
details.outil{{border:1px solid rgba(var(--or-rgb),.18);border-radius:10px;margin:.7rem 0;background:var(--encre)}}
details.outil summary{{display:flex;flex-wrap:wrap;gap:.4rem 1rem;align-items:baseline;padding:.75rem 1rem;cursor:pointer;list-style:none;min-height:44px}}
details.outil summary::-webkit-details-marker{{display:none}}
details.outil summary::before{{content:'âž';color:var(--or);margin-right:.2rem}}
details.outil[open] summary::before{{content:'âŸ'}}
.on{{color:var(--nacre);font-weight:500}}
.ou code{{color:var(--or-pale)}}
.ol{{margin-left:auto;font-size:.74rem;color:var(--gris)}}
.oq{{margin:0 1rem .6rem;font-size:.9rem}}
.ob{{display:flex;gap:.8rem;align-items:center;margin:0 1rem .5rem}}
button.copie{{min-height:32px;padding:.35rem .9rem;border-radius:999px;border:1px solid var(--or-somb);background:rgba(255,255,255,.05);color:var(--or);font:500 .8rem 'Jost',sans-serif;cursor:pointer}}
button.copie:hover{{color:var(--or-pale);background:rgba(255,255,255,.1)}}
.etat{{font-size:.78rem;color:var(--emeraude-ct)}}
details.outil pre{{margin:0;padding:1rem;overflow:auto;max-height:70vh;background:var(--onyx);border-top:1px solid rgba(var(--or-rgb),.14);border-radius:0 0 10px 10px;font-family:'Space Mono',monospace;font-size:.74rem;line-height:1.5;color:var(--gris-cl);tab-size:4}}
h3{{font:400 1.35rem/1.2 'Cormorant Garamond',serif;color:var(--nacre);margin:1.4rem 0 .5rem}}
.leg{{display:flex;flex-wrap:wrap;gap:.4rem 1.2rem;font-size:.82rem;color:var(--gris-cl);margin:.4rem 0 1rem}}
.tg{{display:inline-block;min-width:1.4em;text-align:center;margin-right:.1rem}}
.tg-r{{margin-left:auto;font-size:.8rem;color:var(--gris)}}
details.chk{{border:1px solid rgba(var(--or-rgb),.16);border-radius:10px;margin:.5rem 0;background:var(--encre)}}
details.chk summary{{display:flex;gap:.6rem;align-items:baseline;padding:.6rem .9rem;cursor:pointer;list-style:none;color:var(--or-pale);font-weight:500;min-height:44px}}
details.chk summary::-webkit-details-marker{{display:none}}
details.chk summary::before{{content:'âž';color:var(--or)}}
details.chk[open] summary::before{{content:'âŸ'}}
details.chk ul{{margin:0;padding:0 1rem .7rem 2.2rem;font-size:.9rem}}
details.chk li{{margin:.3rem 0}}
ul.done{{padding-left:1.2rem;columns:2;column-gap:2rem}}
#publier ul.done{{columns:1}}
ul.done li::marker{{content:'â ';color:var(--emeraude-ct)}}
@media(max-width:600px){{ul.done{{columns:1}}}}
.cycle{{display:flex;flex-wrap:wrap;gap:.3rem .5rem;align-items:center;font-size:.86rem}}
.cycle span{{padding:.25rem .6rem;border:1px solid rgba(var(--or-rgb),.25);border-radius:999px;background:var(--encre)}}
.cycle .fl{{border:0;background:none;color:var(--or);padding:0}}
ul.etats{{list-style:none;padding:0}}ul.etats li{{margin:.3rem 0}}
blockquote.principe{{margin:1rem 0;padding:.8rem 1.1rem;border-left:3px solid var(--or);background:var(--encre);border-radius:0 10px 10px 0;font:italic 1.05rem 'Cormorant Garamond',serif;color:var(--or-pale)}}
pre.gab{{margin:0 1rem .8rem;padding:.8rem;background:var(--onyx);border-radius:8px;font-family:'Space Mono',monospace;font-size:.74rem;line-height:1.5;color:var(--gris-cl);white-space:pre-wrap}}
a{{color:var(--saphir-ct)}}
footer{{text-align:center;padding:2rem 1rem 3rem;font-size:.78rem;color:var(--gris);border-top:1px solid var(--or-somb)}}
@media(prefers-reduced-motion:reduce){{*{{animation:none!important;transition:none!important}}}}
</style>
</head>
<body>
<header>
<div class="k">Protocole de ConScience aMi · Atelier</div>
<h1><span class="em">đ§°</span> {E(d['titre'])}</h1>
<p>{E(d['sous_titre'])}</p>
<div class="maj">Mis Ă jour le {E(d['mis_a_jour'])} â gĂ©nĂ©rĂ© par <code>page_cq.py</code></div>
</header>
<nav class="tabs" role="tablist" aria-label="Sections">
<button role="tab" aria-selected="true" data-t="volumes"><span class="em">đ</span>Les Volumes</button>
<button role="tab" aria-selected="false" data-t="apps"><span class="em">đČ</span>Les Applications</button>
<button role="tab" aria-selected="false" data-t="publier"><span class="em">đȘ</span>Publier</button>
<button role="tab" aria-selected="false" data-t="tests"><span class="em">đ§Ș</span>Tests & Recherches</button>
<button role="tab" aria-selected="false" data-t="hist"><span class="em">đ°ïž</span>L'historique</button>
<button role="tab" aria-selected="false" data-t="outils"><span class="em">đ§°</span>La boĂźte Ă outils</button>
</nav>
<main>
<section id="volumes">
<h2><span class="em">đșïž</span>OĂč en est chaque page</h2>
<p class="lead">Une page est « faite » quand elle est mesurée au rendu, à 320, 390 et 1280 px, et que chaque commande a été pressée.</p>
{pages()}
<h2><span class="em">đ§ż</span>Le rĂ©fĂ©rentiel des Volumes â ce que la Charte demande</h2>
<p class="lead">{E(r['volumes']['source'])}</p>
{legende()}
<h3>§ 3 · Le prompt de contrÎle</h3>
<p class="lead">{E(r['volumes']['prompt']['intro'])}</p>
{groupes(r['volumes']['prompt']['familles'])}
<h3>{E(r['volumes']['checklist_page']['titre'])}</h3>
{groupes(r['volumes']['checklist_page']['groupes'])}
<h3>{E(r['volumes']['checklist_widget']['titre'])}</h3>
{groupes(r['volumes']['checklist_widget']['groupes'])}
<h2><span class="em">âïž</span>Les dĂ©cisions â la grille qui s'applique Ă toutes les pages</h2>
{liste(d['decisions'], 'dec')}
<h2><span class="em">đ</span>0.3 L'Union du TouT â avant / aprĂšs</h2>
{mesures()}
<h2><span class="em">đź</span>Ce qui attend l'avis de Diane</h2>
{liste(d['a_valider'], 'val')}
<h2><span class="em">đȘ€</span>Les piĂšges rencontrĂ©s â Ă ne pas refaire</h2>
{liste(d['pieges'], 'pieges')}
</section>
<section id="apps" hidden>
<h2><span class="em">đČ</span>Le registre des Applications</h2>
<p class="lead">« La QualitĂ© de Nos DĂ©cisions dĂ©pend de Notre CapacitĂ© Ă Voir Juste. » â <a href="https://www.agdistys.fr/mes-applications" rel="noopener" target="_blank">agdistys.fr/mes-applications</a></p>
{registre()}
<h2><span class="em">đ§ż</span>Le rĂ©fĂ©rentiel des Applications â machine · inspection · publication</h2>
<p class="lead">{E(r['applications']['source'])}</p>
<blockquote class="principe">{E(r['applications']['principe'])}</blockquote>
{legende()}
<h3>Le cycle de vie canonique</h3>
{cycle()}
<p class="lead">Aucune fonctionnalité ne passe de « je l'ai codée » à « production » : un critÚre d'acceptation, au moins un test automatique quand c'est testable, une inspection humaine quand il y a du sens, de l'usage ou de l'esthétique.</p>
<h3>DONE â seulement si</h3>
{liste(r['applications']['done'], 'done')}
<h3>Le socle commun, section par section</h3>
{socle()}
<h3>Les états officiels d'un chantier</h3>
{etats()}
<h3>Ce que la machine ne doit pas décider</h3>
{liste(r['applications']['machine_ne_decide_pas'], 'dec')}
<h3>Les cinq questions de l'Inspectrice des Travaux Finis</h3>
{liste(r['applications']['cinq_questions'], 'val')}
<h3>Gabarits</h3>
<details class="chk"><summary>Fiche d'acceptation par fonctionnalité</summary><pre class="gab">{E(r['applications']['fiche'])}</pre></details>
<details class="chk"><summary>ProcĂšs-verbal automatique de PR</summary><pre class="gab">{E(r['applications']['pv'])}</pre></details>
<blockquote class="principe">{E(r['applications']['formule'])}</blockquote>
</section>
<section id="publier" hidden>
{publication()}
</section>
{tests_recherches()}
<section id="hist" hidden>
<h2><span class="em">đ°ïž</span>L'historique du chantier</h2>
{historique()}
</section>
<section id="outils" hidden>
<h2><span class="em">đ§°</span>La boĂźte Ă outils</h2>
<p class="lead">Dans l'ordre oĂč ils se rejouent sur une page. PrĂ©requis : <code>python3</code> et <code>pip install playwright && playwright install chromium</code>. Chaque script est lisible ici et se copie d'un geste.</p>
{outils()}
</section>
</main>
<footer>Agdistys · Diane Serant & Claude + Isil · CC BY-SA 4.0 â les fichiers du Mac font foi ; cette page se rĂ©gĂ©nĂšre depuis <code>etat_chantier.json</code>.</footer>
<script>
(function(){{'use strict';
var tabs=[].slice.call(document.querySelectorAll('nav.tabs button')), secs=[].slice.call(document.querySelectorAll('main > section'));
function ouvre(k){{ tabs.forEach(function(b){{ var on=b.dataset.t===k; b.setAttribute('aria-selected',on?'true':'false'); }});
secs.forEach(function(s){{ s.hidden = s.id!==k; }}); try{{ history.replaceState(null,'','#'+k); }}catch(e){{}} }}
tabs.forEach(function(b){{ b.addEventListener('click',function(){{ ouvre(b.dataset.t); }}); }});
document.querySelector('nav.tabs').addEventListener('keydown',function(e){{ var i=tabs.indexOf(document.activeElement); if(i<0) return; var j=i;
if(e.key==='ArrowRight') j=(i+1)%tabs.length; else if(e.key==='ArrowLeft') j=(i-1+tabs.length)%tabs.length; else return; e.preventDefault(); tabs[j].focus(); ouvre(tabs[j].dataset.t); }});
var h=(location.hash||'').slice(1); if(h==='etat'||h==='dec'||h==='pieges') h='volumes'; if(h && secs.some(function(s){{return s.id===h;}})) ouvre(h);
else if(h && h.indexOf('o-')===0){{ ouvre('outils'); var d=document.getElementById(h); if(d){{ d.open=true; d.scrollIntoView(); }} }}
document.querySelectorAll('nav.subtabs').forEach(function(nav){{
var buttons=[].slice.call(nav.querySelectorAll('button[data-sub]'));
var panes=buttons.map(function(b){{return document.getElementById(b.dataset.sub);}}).filter(Boolean);
function openSub(id){{buttons.forEach(function(b){{b.setAttribute('aria-selected',b.dataset.sub===id?'true':'false');}});panes.forEach(function(p){{p.hidden=p.id!==id;}});}}
buttons.forEach(function(b){{b.addEventListener('click',function(){{openSub(b.dataset.sub);}});}});
}});
document.addEventListener('click',function(e){{ var b=e.target.closest&&e.target.closest('button.copie'); if(!b) return;
var pre=document.getElementById(b.dataset.cible), etat=b.parentNode.querySelector('.etat'), txt=pre?pre.textContent:'';
function ok(){{ etat.textContent='copiĂ© â'; setTimeout(function(){{ etat.textContent=''; }},1800); }}
if(navigator.clipboard&&navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(ok,function(){{ etat.textContent='sélectionne et copie à la main'; }});
else {{ var r=document.createRange(); r.selectNodeContents(pre); var s=getSelection(); s.removeAllRanges(); s.addRange(r); etat.textContent='texte sĂ©lectionnĂ© â âC'; }} }});
}})();
</script>
</body>
</html>'''
open(OUT, 'w', encoding='utf-8').write(page)
print(f'â {OUT} â {len(page)//1024} Ko, {len(d["outils"])} outils, {len(d["pages"])} pages, {len(d["historique"])} entrĂ©es d\'historique')
p1_et.py python3 p1_et.py et.html et_p1.html (les six astro_*.html et amiblocs.py à cÎté)125 lignes
5.3 Astro : les six instruments entrent dans la page. Six documents base64 en blob:/iframe â six BLOCS (une racine chacun, feuille prĂ©fixĂ©e) ; Astronomy Engine dĂ©dupliquĂ©e (4 copies â 1, â339 Ko) ; le relais de hauteur postMessage ĂŽtĂ© ; les scripts nus enveloppĂ©s en IIFE ; les dix identifiants en collision prĂ©fixĂ©s (HTML, CSS, JS, ancres) ; l'export limitĂ© Ă sa racine ; le dictionnaire sources={âŠ} (968 Ko) et load() ĂŽtĂ©s.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P1 · 5.3 Astro · Espace-Temps â LES SIX INSTRUMENTS ENTRENT DANS LA PAGE (Diane, 20/09 : « (b) les entrer en BLOCS »).
Avant : six documents complets encodés en base64 (1 Mo) dans le script, décodés en blob: et servis dans six <iframe> à hauteur
FIXE (760â1200 px) et d'origine opaque â la page ne pouvait pas mesurer leur contenu, donc un instrument de 4 000 px vivait
dans une fenĂȘtre de 1 200 px : un dĂ©filement dans le dĂ©filement, ingĂ©rable au doigt (la frustration du festival). Le đ ne les
indexait pas, le zoom ne les atteignait pas, et un blob: dans le cadre Webnode ne s'affiche pas toujours.
AprĂšs : six BLOCS dans la page, une racine chacun (#ami-et-themeâŠ), leur feuille prĂ©fixĂ©e, plus d'iframe, plus de blob.
· la bibliothĂšque Astronomy Engine (113 Ko, md5 6106cfeb97) Ă©tait copiĂ©e QUATRE fois : une seule copie en tĂȘte (â339 Ko) ;
· le relais de hauteur postMessage (époque iframe) Îté des six ;
· les scripts nus (Ipséïté, Citoyenneté, Village) enveloppés en IIFE : leurs `const root`, `const tabs`, `const FIELDS`
auraient écrasé les globales de la page ;
· les dix identifiants en collision préfixés (reset, calculate, export, results, birthDate, birthTime, timezone entre
instruments ; calendriers-du-monde, glossaire-astrologique, origine-des-signes avec l'hĂŽte), HTML, JS et ancres ;
· le dictionnaire `sources={âŠ}` (1 Mo de base64) et la fonction load() s'en vont.
Identité : ASTRO en h1 (Espace-Temps en sous-titre), la Question reste sur la une, la Réponse ira en conclusion (P4) ;
métas 5.3 · V15 ; un seul <title> ; « & » affiché en clair sur le portail réparé.
Usage : python3 p1_et.py entree.html sortie.html (les six astro_*.html et amiblocs.py à cÎté)"""
import re, sys, base64, hashlib
from amiblocs import prefixe_css
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ les six instruments : clĂ© du cadre â (fichier, racine, prĂ©fixe des ids) âââââââââââââââââââââââââââââââ
INST = [('ipseite', 'astro_space.html', 'ami-et-ipseite', 'ip'),
('theme', 'astro_theme.html', 'ami-et-theme', 'tm'),
('complement', 'astro_complement.html', 'ami-et-complement', 'cp'),
('synastrie', 'astro_synastry.html', 'ami-et-synastrie', 'sy'),
('citoyennete', 'astro_citizenship.html', 'ami-et-citoyennete', 'ct'),
('village', 'astro_village.html', 'ami-et-village', 'vi')]
MD5_LIB = '6106cfeb97' # Astronomy Engine © Don Cross (MIT)
MD5_PONT = '639220408c' # le relais de hauteur de l'époque iframe
COLLISIONS = ['reset', 'calculate', 'export', 'results', 'birthDate', 'birthTime', 'timezone',
'calendriers-du-monde', 'glossaire-astrologique', 'origine-des-signes']
IIFE = {'ipseite', 'citoyennete', 'village'} # leurs scripts déclarent des globales génériques
lib = None
blocs = {}
for cle, fichier, racine, pre in INST:
d = open(fichier, encoding='utf-8').read()
titre = re.search(r'<title>(.*?)</title>', d, re.S).group(1)
# 1 · les scripts : la bibliothÚque sortie, le pont Îté, les scripts nus enveloppés
scripts = []
for m in re.finditer(r'<script\b([^>]*)>(.*?)</script>', d, re.S):
js, md5 = m.group(2), hashlib.md5(m.group(2).encode()).hexdigest()[:10]
if md5 == MD5_LIB:
if lib is None: lib = js
else: assert hashlib.md5(lib.encode()).hexdigest()[:10] == md5
continue # une seule copie, posĂ©e en tĂȘte de page
if md5 == MD5_PONT: continue # code mort hors iframe
scripts.append(js)
d = re.sub(r'<script\b[^>]*>.*?</script>', '', d, flags=re.S)
# 2 · la feuille, préfixée par la racine
styles = re.findall(r'<style\b[^>]*>(.*?)</style>', d, re.S)
css = '\n'.join(prefixe_css(s, '#' + racine) for s in styles)
# 3 · le corps
mb = re.search(r'<body[^>]*>', d); corps = d[mb.end():d.rfind('</body>')]
corps = re.sub(r'<style\b[^>]*>.*?</style>', '', corps, flags=re.S)
# 4 · les identifiants en collision : HTML, ancres, JS
n_id = 0
for i in COLLISIONS:
if f'id="{i}"' not in corps: continue
neuf = f'{pre}-{i}'
corps = corps.replace(f'id="{i}"', f'id="{neuf}"').replace(f'href="#{i}"', f'href="#{neuf}"')
css = re.sub(r'#' + re.escape(i) + r'(?![\w-])', '#' + neuf, css)
scripts = [re.sub(r"(['\"])#?" + re.escape(i) + r"\1", lambda m, n=neuf, j=i: m.group(0).replace(j, n), s) for s in scripts]
n_id += 1
# 4 bis · les exports : en bloc, « document.documentElement.outerHTML » emporterait la page entiÚre
n_ex = 0
for av, ap in [("document.documentElement.outerHTML", f"document.getElementById('{racine}').outerHTML")]:
scripts = [s2.replace(av, ap) for s2 in scripts]
n_ex += sum(s2.count(ap) for s2 in scripts)
if n_ex: J.append(f'{cle} : export limitĂ© Ă sa racine Ă{n_ex} (il emportait la page)')
# 5 · les scripts nus enveloppés
js = '\n'.join(f"(function(){{'use strict';\n{s}\n}})();" if cle in IIFE else s for s in scripts)
blocs[cle] = (f'<div id="{racine}" class="ami-et-outil" data-outil="{cle}">\n'
f'<style>\n{css}\n</style>\n{corps}\n'
f'<script>\n{js}\n</script>\n</div>')
J.append(f'{cle} : bloc {len(blocs[cle])//1024} Ko · {len(scripts)} script(s){" (IIFE)" if cle in IIFE else ""} · {n_id} id(s) préfixé(s) {pre}- · « {titre[:40]} »')
assert lib, 'bibliothĂšque Astronomy Engine introuvable'
# ââ les six cadres remplacĂ©s par les six blocs ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
for cle, _, racine, _ in INST:
m = re.search(r'<iframe class="astro-frame" id="frame-' + cle + r'"[^>]*>\s*</iframe>', h)
assert m, cle
h = h[:m.start()] + blocs[cle] + h[m.end():]
J.append('six cadres â six blocs (plus aucun <iframe>)')
assert '<iframe' not in h
# ââ le dictionnaire base64 et le chargeur s'en vont âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
i = h.find('const sources={'); assert i > 0, 'sources'
m = re.search(r"'\};", h[i:]); poids = m.end()
h = h[:i] + h[i + m.end() + 1:]
m = re.search(r"var last=\{\},urls=\{\};\n?function load\(sub\)\{.*?\n\}\n", h, re.S); assert m, 'load()'
h = h[:m.start()] + 'var last={};\n' + h[m.end():]
h, k = re.subn(r'[ \t]*load\([a-z]+\);?\n?', '', h); J.append(f'appels Ă load() ĂŽtĂ©s Ă{k}')
# le mĂ©nage des blob: Ă la fermeture (il n'y a plus de blob) â il restait « urls is not defined » Ă chaque sortie
m = re.search(r"window\.addEventListener\('beforeunload',function\(\)\{Object\.keys\(urls\)[^\n]*?\}\);\n?", h)
assert m, 'le ménage des blob:'
h = h[:m.start()] + h[m.end():]
J.append('le ménage des blob: à la fermeture Îté (il levait « urls is not defined »)')
J.append(f'sources base64 ({poids//1024} Ko) + load() Îtés')
assert 'sources[' not in h and 'atob(' not in h # les createObjectURL qui restent sont les EXPORTS des instruments (légitimes)
# ââ la bibliothĂšque, une seule fois, avant les blocs ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
mb = re.search(r'<body[^>]*>\n?', h)
h = (h[:mb.end()] + '<script id="ami-astronomy-engine">\n/* une seule copie pour les quatre instruments (Ipséïté, ThÚme, '
'ComplĂ©mentaritĂ©, Synastrie) â elle Ă©tait Ă©crite quatre fois */\n' + lib + '\n</script>\n' + h[mb.end():])
J.append(f'Astronomy Engine posé une fois ({len(lib)//1024} Ko au lieu de 4 à {len(lib)//1024})')
# ââ identitĂ© ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<title>đ§âł ESPACE-TEMPS · Protocole de ConScience aMi â Volume 5</title>',
'<title>5.3 Astro · Espace-Temps â Tu Sais Ta Juste Place · Protocole de ConScience aMi</title>\n'
'<meta name="ami:volume" content="5.3"><meta name="ami:version" content="V17"><meta name="ami:date" content="2026-09-20">',
1, 'title + métas 5.3 · V15')
rep('&AMP; AVEC LES AUTRES', '& AVEC LES AUTRES', 0, '') if '&AMP;' in h else None
h2, k = re.subn(r'&amp;', '&', h); h = h2
if k: J.append(f'&amp; â & Ă{k} (le « & » affichĂ© en clair)')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h)//1024} Ko')
p2_et.py python3 p2_et.py et_p1.html et_p2.html255 lignes
5.3 Astro : le chemin (ASTRO en h1, Espace-Temps en sous-titre, un seul h1), LE DOIGT (feuille ami-doigt posĂ©e en DERNIER dans le corps : 44 px, cases 26 px dans une zone de 44, curseurs Ă pouce de 30, champs â„ 16 px contre le zoom iOS), les trois sections que le ThĂšme doublait â renvois, polices de la Charte, dĂ©rives des Ă©toiles en opacitĂ©, reduced-motion sur toute la page, et les littĂ©raux </head>/</body> du gabarit d'export coupĂ©s en deux.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P2 · 5.3 Astro · Espace-Temps â LA FORME : le chemin, les doublons, le doigt.
Le chemin (arbitrage de Diane, 20/09) : ASTRO en h1, Espace-Temps en sous-titre ; la Question reste sur la une,
la Réponse ira en conclusion (P4). Un seul h1 dans la page (les sept autres deviennent des h2 qui gardent l'allure).
Les doublons. L'audit du 20/09 annonçait « 13 paragraphes en double â la mĂȘme sĂ©rie "PourquoiâŠ" Ă©crite deux
fois » : VĂRIFIĂ ICI, c'est FAUX, et rien n'est ĂŽtĂ©. Les treize phrases vivent une fois dans « đ La BioMatrice ·
le Corps » (p.bm-sens) et une fois dans « đžïž Les 13 Champs · la PsychĂ© » (p.ev-sens) : deux chapitres, deux volets,
le mĂȘme pivot amenĂ© des deux cĂŽtĂ©s. Ă Diane de dire si elle n'en veut qu'un.
Le vrai doublon est ailleurs : l'instrument ThĂšme portait sa propre copie de « D'oĂč viennent les Signes ? »,
« Les Calendriers du Monde » et du « Lexique du ThĂšme » â nĂ©cessaire quand il vivait seul dans son cadre. En bloc,
deux d'entre elles tombent dans le MĂME volet que l'introduction commune de la page (« Une Histoire commune avant
les deux Lectures », qui existe justement pour ça) : 51 paragraphes lus deux fois de suite. Les trois copies
s'en vont â SANS renvoi ([stated] Diane, 20/09 : « ça me convient pas d'avoir plein de renvois dans la page ;
soit on prĂ©sente les parties lĂ oĂč elles sont nommĂ©es, soit on les laisse et on laisse le sommaire faire son
travail ») ; l'export du ThÚme va chercher la version de la page.
Le doigt (Diane : « il faut bien les reprendre pour qu'ils soient compatibles pour mobile car pour l'instant
c'est dur dur ») : dans les six instruments, tout ce qui se touche passe à 44 px de haut, les cases et les curseurs
reçoivent une vraie zone, et AUCUN champ ne descend sous 16 px â sous 16 px, iOS zoome la page Ă la saisie et
ne la dézoome jamais : c'est la moitié de la misÚre au doigt.
Le reste (le sûr) : polices Charte (Cormorant/Jost) jusque dans le document exporté, variables déclarées,
les trois dĂ©rives (Ă©toiles, orbes) en opacitĂ© â elles dĂ©bordaient la une de 22 px â, deux animations sous 7 s
remises Ă la rĂšgle, reduced-motion Ă©tendu Ă toute la page, accents rendus à « OĂč la DĂ©mocratie Vivante existe dĂ©jĂ ? ».
Note de vérité : les cinq « sélecteurs nus `to{` » signalés par l'audit statique sont les images-clés des
@keyframes (`from{} to{}`) â c'est un faux positif de l'outil, pas une faute de la page. Rien Ă corriger lĂ .
Usage : python3 p2_et.py entree.html sortie.html
"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new)
J.append(f'{why or old[:52]} Ă{n}')
def subn(pat, new, n, why, flags=0):
global h
h2, k = re.subn(pat, new, h, flags=flags)
assert k == n, f'â {why} : {k} remplacement(s), {n} attendu(s)'
h = h2; J.append(f'{why} Ă{k}')
def bloc(h, i, ouvre, ferme):
"""de l'index i (sur la balise ouvrante) jusqu'à sa fermeture équilibrée"""
d = 0
for m in re.finditer(re.escape(ouvre) + r'\b|' + re.escape(ferme), h[i:]):
d += 1 if m.group(0).startswith(ouvre) else -1
if d == 0: return i, i + m.end()
raise ValueError(ouvre)
# ââ 1 · LE CHEMIN : ASTRO en h1, Espace-Temps en sous-titre ââââââââââââââââââââââââââââââââââââââââââââââ
rep('<p class="vol-label">Protocole de ConScience aMi â Volume 5</p>',
'<p class="vol-label">Protocole de ConScience aMi â Volume 5.3</p>', 1, 'vol-label â Volume 5.3')
rep('<div class="couv-glyphes">đ§ âł</div>', '<div class="couv-glyphes">đ« đ§ âł</div>', 1, 'sceau đ« sur la une')
rep('<h1 class="couv-h1">ESPACE-TEMPS</h1>',
'<h1 class="couv-h1">ASTRO</h1>\n <p class="couv-sous">Espace-Temps</p>', 1, 'h1 ASTRO · sous-titre Espace-Temps')
rep('Intelligences Culturelles · Conscience Universelle',
'Intelligences Culturelles · ConScience UniVerCelle', 2, 'ConScience UniVerCelle (graphie de la Charte)')
# les sept autres h1 de la page â h2.ami-h1 (le h1 du document exportĂ©, dans le script, n'est pas touchĂ©)
sc = [(m.start(), m.end()) for m in re.finditer(r'<script\b.*?</script>', h, re.S)]
dans_script = lambda x: any(a <= x < b for a, b in sc)
coupes = []
for m in re.finditer(r'<h1(\s[^>]*)?>(.*?)</h1>', h, re.S):
if dans_script(m.start()) or 'couv-h1' in (m.group(1) or ''): continue
at = (m.group(1) or '').strip()
cl = re.search(r'class="([^"]*)"', at)
at = re.sub(r'class="[^"]*"', '', at).strip()
neuf = f'<h2 class="ami-h1{" " + cl.group(1) if cl else ""}"{" " + at if at else ""}>{m.group(2)}</h2>'
coupes.append((m.start(), m.end(), neuf))
for a, b, neuf in reversed(coupes): h = h[:a] + neuf + h[b:]
J.append(f'un seul h1 : les {len(coupes)} autres â h2.ami-h1')
# ââ 2 · LES DOUBLONS âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 2a · la sĂ©rie « Pourquoi ⊠? » â VĂRIFIĂE, et l'audit se trompait : ce n'est PAS un collage.
# Les 13 phrases vivent une fois dans « đ La BioMatrice · le Corps » (p.bm-sens, le substrat biologique)
# et une fois dans « đžïž Les 13 Champs · la PsychĂ© » (p.ev-sens, les cartes de maturation) : deux chapitres,
# deux volets, le mĂȘme pivot. Je n'ĂŽte rien : c'est Ă Diane de dire si elle veut n'en garder qu'un.
occ = [m.start() for m in re.finditer('Pourquoi ressentir ?', h)]
assert len(occ) == 2, f'série « Pourquoi » : {len(occ)} occurrence(s)'
cl = [re.search(r'<p class="([\w-]+)"[^>]*>(?:(?!</p>).)*?Pourquoi ressentir', h[i - 400:i + 40], re.S) for i in occ]
J.append('sĂ©rie « Pourquoi ⊠? » : VĂRIFIĂ â pas un collage (BioMatrice ' +
f'{cl[0].group(1) if cl[0] else "?"} / 13 Champs {cl[1].group(1) if cl[1] else "?"}) â rien ĂŽtĂ©, Ă trancher')
# 2b · les trois copies que l'instrument ThÚme portait pour vivre seul
# [stated] Diane, 20/09 aprĂšs-midi : « soit on prĂ©sente les parties lĂ oĂč elles sont nommĂ©es et renvoyĂ©es,
# soit on les laisse lĂ oĂč elles sont et on laisse faire le sommaire son travail, mais ça me convient pas
# d'avoir plein de renvois dans la page » â les copies s'en vont SANS renvoi. L'introduction commune de la
# page les porte (deux des trois sont dans le MĂME volet), et le â° mĂšne Ă la troisiĂšme.
OTER = ['<div class="zodiac-origins" id="tm-origine-des-signes">',
'<section class="panel calendar-world" id="tm-calendriers-du-monde">',
'<section class="panel astro-glossary" id="tm-glossaire-astrologique">']
gagne = 0
for tag in OTER:
i = h.find(tag); assert i > 0, tag
ouvre = '<div' if tag.startswith('<div') else '<section'
a, b = bloc(h, i, ouvre, '</div>' if ouvre == '<div' else '</section>')
gagne += b - a
h = h[:a] + h[b:]
J.append(f'ThÚme : les trois sections qui doublaient la page Îtées, sans renvoi ({gagne // 1024} Ko)')
# l'export du ThÚme emportait sa propre copie de l'origine des signes : il prend désormais celle de la page
rep("document.getElementById('tm-origine-des-signes')?.outerHTML",
"document.getElementById('origine-des-signes')?.outerHTML", 1,
'lâexport du ThĂšme lit lâorigine des signes de la page (sa copie nâexiste plus)')
assert 'tm-origine-des-signes' not in h and 'tm-glossaire-astrologique' not in h and 'tm-calendriers-du-monde' not in h
# le renvoi doit pouvoir changer de volet : le clic délégué s'élargit à tout a[data-goto]
rep("e.target.closest('.ency-som-links a')", "e.target.closest('a[data-goto]')", 1,
'le clic délégué ouvre le bon volet pour tout renvoi')
# ââ 2 ter · LE GABARIT D'EXPORT NE FAIT PLUS PASSER SA TĂTE POUR CELLE DE LA PAGE âââââââââââââââââââââââ
# Le script d'export du ThÚme écrit un document complet dans une chaßne : « </head> », « </body> » y figurent
# en toutes lettres. Tout outil qui cherche la fin de la tĂȘte de LA PAGE tombait dessus (et posait sa feuille
# dans la chaßne JS : « Invalid or unexpected token »). On coupe les trois littéraux en deux : le document
# exportĂ© est rigoureusement identique, la page n'a plus qu'une seule tĂȘte et un seul corps.
rep("+'</style></head><body><main><", "+'</style></hea'+'d><bo'+'dy><main><", 1,
'le gabarit dâexport ne porte plus « </head><body> » en un seul morceau')
rep("+'</main></body></html>';", "+'</main></bo'+'dy></ht'+'ml>';", 1,
'ni « </body></html> »')
# ââ 3 · LE DOIGT âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
DOIGT = """
/* ââ LE DOIGT · les six instruments sont entrĂ©s dans la page : leurs commandes passent Ă la main (P2, 20/09) ââ
Cette feuille est la DERNIĂRE du corps et parle avec autoritĂ© : chaque instrument est arrivĂ© avec la sienne,
et « #ami-et-theme .panel a » l'emporterait sinon sur « #ami-espacetemps a ». Au doigt, c'est elle qui tranche. */
#ami-espacetemps .ami-et-outil button,
#ami-espacetemps .ami-et-outil select,
#ami-espacetemps .ami-et-outil summary,
#ami-espacetemps .ami-et-outil [role=button],
#ami-espacetemps .ami-et-outil input:not([type=checkbox]):not([type=radio]):not([type=range]){
min-height:44px!important; /* la pulpe du doigt : 44 px, pas 31 */
font-size:max(16px,1em)!important; /* sous 16 px, iOS zoome Ă la saisie et ne revient jamais */
}
#ami-espacetemps .ami-et-outil input[type=checkbox],
#ami-espacetemps .ami-et-outil input[type=radio]{
width:26px!important;height:26px!important;min-height:26px!important;flex:0 0 auto;
accent-color:var(--or,#ffc545);cursor:pointer;margin:0 .1rem;
}
#ami-espacetemps .ami-et-outil label:has(> input[type=checkbox]),
#ami-espacetemps .ami-et-outil label:has(> input[type=radio]),
#ami-espacetemps .ami-et-outil .chk,#ami-espacetemps .ami-et-outil .check{
min-height:44px!important;display:flex!important;align-items:center;gap:.6rem;
}
#ami-espacetemps .ami-et-outil input[type=range]{
height:44px!important;min-height:44px!important;background:transparent;
-webkit-appearance:none;appearance:none;width:100%;
}
#ami-espacetemps .ami-et-outil input[type=range]::-webkit-slider-runnable-track{
height:6px;border-radius:3px;background:linear-gradient(90deg,var(--or,#ffc545),rgba(255,255,255,.18));
}
#ami-espacetemps .ami-et-outil input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:30px;height:30px;border-radius:50%;margin-top:-12px;
background:var(--or,#ffc545);border:2px solid rgba(0,0,0,.45);
}
#ami-espacetemps .ami-et-outil input[type=range]::-moz-range-track{height:6px;border-radius:3px;background:rgba(255,255,255,.22)}
#ami-espacetemps .ami-et-outil input[type=range]::-moz-range-thumb{width:30px;height:30px;border:2px solid rgba(0,0,0,.45);border-radius:50%;background:var(--or,#ffc545)}
/* les liens de sources, en fin de chapitre : ils se touchaient Ă 12 px de haut */
#ami-espacetemps .ami-et-outil a[href]{display:inline-block;min-height:24px!important;padding-block:.28rem;line-height:1.55}
/* et les commandes de la page elle-mĂȘme */
#ami-espacetemps .ency-som-links a,
#ami-espacetemps .ency-som-more-btn,
#ami-espacetemps #et-toc-links a,
#ami-espacetemps .ency-som-tab > summary,
#ami-espacetemps .ency-som-group > summary{min-height:32px!important;display:flex;align-items:center;gap:.5rem}
@media (pointer:coarse){
#ami-espacetemps .ency-som-links a,#ami-espacetemps #et-toc-links a,
#ami-espacetemps .ency-som-more-btn{min-height:44px!important}
#ami-espacetemps .ami-et-outil .tab,#ami-espacetemps .ami-et-outil .btn,
#ami-espacetemps .ami-et-outil .chip{min-height:44px!important}
}
/* ââ UN SEUL h1 : les anciens gardent leur allure ââ */
#ami-espacetemps .ami-h1{
font-family:'Cormorant Garamond',Georgia,serif;font-weight:300;
font-size:clamp(1.9rem,4.4vw,2.9rem);line-height:1.12;margin:0 0 .6rem;
}
#ami-espacetemps .couv-sous{
font-family:'Jost',system-ui,sans-serif;font-weight:300;letter-spacing:.24em;text-transform:uppercase;
font-size:clamp(.85rem,2.4vw,1.15rem);color:var(--or-pale,#ffd06a);margin:0 0 .9rem;
}
/* ââ LES VARIABLES QUE LA PAGE LISAIT SANS LES AVOIR (elles vivaient dans les cadres) ââ */
#ami-espacetemps{--cyan:#4ec8b8;--pale:#b3b1a8;--rose:#f56aaf;--stem:#0b8d38}
/* ââ LE REPOS · reduced-motion sur toute la page, pas seulement sur la une ââ */
@media (prefers-reduced-motion:reduce){
#ami-espacetemps *,#ami-espacetemps *::before,#ami-espacetemps *::after{
animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;
}
}
"""
# elle se pose EN DERNIER dans le corps : les six instruments ont apporté leurs feuilles avec eux et
# « #ami-et-theme .panel a » bat « #ami-espacetemps .ami-renvoi-a » ; le dernier mot au doigt.
# (et pas avant </head> : le gabarit du document exporté écrit lui aussi « </head> » dans une chaßne JS)
i = h.rfind('</body>')
assert i > h.rfind('</script>'), 'le </body> visĂ© nâest pas celui de la page'
h = h[:i] + f'<style id="ami-doigt">{DOIGT}</style>\n' + h[i:]
J.append('feuille « ami-doigt » : 44 px au doigt, champs ℠16 px (iOS ne zoome plus), h2.ami-h1, variables, repos')
# ââ 4 · LES POLICES DE LA CHARTE âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# dans les feuilles (16 fois Georgia, 3 fois system-ui) ; dans le script d'export, SANS guillemets (chaĂźne JS)
sc = [(m.start(), m.end()) for m in re.finditer(r'<script\b.*?</script>', h, re.S)]
dans_script = lambda x: any(a <= x < b for a, b in sc)
def police(m):
q = "'" if not dans_script(m.start()) else ''
return m.group(0).replace('Georgia,serif', f'{q}Cormorant Garamond{q},Georgia,serif')
n1 = 0
def f1(m):
global n1; n1 += 1; return police(m)
h = re.sub(r'font-family:\s*Georgia,\s*serif', f1, h)
J.append(f'Georgia â Cormorant Garamond Ă{n1}')
def f2(m):
global n2; n2 += 1
q = "'" if not dans_script(m.start()) else ''
return m.group(0).replace('system-ui', f'{q}Jost{q},system-ui')
n2 = 0
h = re.sub(r'font-family:\s*system-ui,\s*sans-serif', f2, h)
h = re.sub(r"font:15px/1\.6 system-ui", lambda m: 'font:15px/1.6 Jost,system-ui', h)
J.append(f'system-ui â Jost Ă{n2} (+ le document exportĂ©)')
# ââ 5 · LES DĂRIVES QUI DĂBORDAIENT LA UNE âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subn(r'@keyframes etStars\{from\{transform:translate3d\(0,0,0\)\}to\{transform:translate3d\(-3%,6%,0\)\}\}',
'@keyframes etStars{0%,100%{opacity:.55}50%{opacity:1}}', 1,
'étoiles A : la dérive (elle débordait la une de 22 px) devient une respiration')
subn(r'@keyframes etStarsB\{from\{transform:translate3d\(0,0,0\)\}to\{transform:translate3d\(4%,-5%,0\)\}\}',
'@keyframes etStarsB{0%,100%{opacity:.5}50%{opacity:.95}}', 1,
'Ă©toiles B : dĂ©rive â respiration')
subn(r'@keyframes etOrbe\{from\{transform:translate\(-50%,-50%\) rotate\(0\)\}to\{transform:translate\(-50%,-50%\) rotate\(360deg\)\}\}',
'@keyframes etOrbe{0%,100%{transform:translate(-50%,-50%) scale(1);opacity:.55}50%{transform:translate(-50%,-50%) scale(1.06);opacity:.85}}',
1, 'orbes : la rotation devient un souffle (opacité/échelle)')
subn(r'animation:pulse 4s', 'animation:pulse 9s', 1, 'pulse 4 s â 9 s (la rĂšgle : â„ 7 s)')
subn(r'animation:etPulse 6s', 'animation:etPulse 9s', 1, 'etPulse 6 s â 9 s')
# ââ 6 · LE RESTE âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<h2 class="ami-h1">OĂč la Democratie Vivante existe deja ?</h2>',
'<h2 class="ami-h1">OĂč la DĂ©mocratie Vivante existe dĂ©jĂ ?</h2>', 1, 'accents rendus au titre de CitoyennetĂ©')
# le relais de hauteur des cadres : il n'y a plus de cadre
m = re.search(r"window\.addEventListener\('message',function\(e\)\{\n if\(!e\.data\|\|e\.data\.type!=='astroHeight'\)return;.*?\n\}\);\n", h, re.S)
if m:
h = h[:m.start()] + h[m.end():]
J.append('le relais de hauteur des cadres ĂŽtĂ© (il nây a plus de cadre)')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h) // 1024} Ko')
p3_et.py python3 p3_et.py et_p2.html et_p3.html (canon.py/couleur.py dans ../verif/verificateur)96 lignes
5.3 Astro : la palette. La page passe aux treize pierres ; les treize pierres + leurs triplets -rgb dĂ©clarĂ©s une fois ; les cinq instruments reçoivent un PONT (--bg:var(--noir), --gold:var(--or), --cyan:var(--turq-pale)âŠ) sans qu'un seul de leurs sĂ©lecteurs change. Inter â Jost.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P3 · 5.3 Astro â LA PALETTE : une seule, celle de la Charte.
La page portait sa palette (un or Ă©teint, #c8a84a) et les six instruments portaient la leur (--bg/--panel/--goldâŠ,
un fond #07070c légÚrement différent du #09090e de la page) : sept palettes sur une page. Ici :
· la page passe aux treize pierres de la Charte (6 sept 2026) â l'or redevient l'ambre #ffc545 ;
· les six instruments ne changent pas un seul de leurs sélecteurs : ils continuent d'écrire var(--gold),
var(--panel), var(--cyan)⊠mais ces noms pointent désormais sur les pierres de la page. Une valeur, un lieu.
· la page déclare aussi les triplets -rgb : les rgba() des instruments peuvent suivre la pierre.
Ce qui n'est pas une couleur (--r, --wrap, --mini, --score, --phase, --shadow, --grad) n'est pas touché.
Usage : python3 p3_et.py entree.html sortie.html
"""
import re, sys
sys.path.insert(0, '../verif/verificateur')
import canon as K, couleur as C
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read()
J = []
# ââ 1 · la page passe aux pierres ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
PIERRE = { # nom de la page â valeur de la Charte (pierre, niveau)
'--noir': '#09090e', '--encre': '#111118', '--voile': '#181826', '--nacre': '#f2ede2',
'--or': '#ffc545', '--or-pale': '#ffd06a', '--or-somb': '#7a611a',
'--gris': '#9595a2', '--gris-cl': '#b0b0bd',
'--vert': '#0b8d38', '--vert-cl': '#32b15e',
'--rubis': '#e33232', '--rubis-cl': '#fe6969',
'--bleu': '#0077ee', '--bleu-cl': '#7697ee',
'--fuchsia': '#cf458a', '--fuch-cl': '#f56aaf',
'--turq': '#009f9d', '--turq-pale': '#31b4b3',
'--eco': '#cf458a', '--eco-pale': '#f56aaf', '--eco-med': '#be3278',
}
n = 0
def peint(m):
global n
k, v = m.group(1), m.group(2).lower()
if k in PIERRE and v != PIERRE[k]:
n += 1; return f'{k}:{PIERRE[k]}'
return m.group(0)
h = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*(#[0-9a-fA-F]{6})\b', peint, h)
J.append(f'la page (et lâIpséïtĂ©, qui partage ses noms) repeinte aux pierres Ă{n}')
# ââ 2 · les treize pierres et leurs triplets, dĂ©clarĂ©s une fois pour toute la page ââââââââââââââââââââââââ
decl = []
for p, trois in K.PALETTE.items():
for niv, hx in zip(K.NIVEAUX, trois): decl.append(f'--{p}{niv}:{hx}')
decl.append(f'--{p}-rgb:{",".join(str(x) for x in C.hex2rgb(trois[0]))}')
for hx, nom in K.NEUTRES.items(): decl.append(f'--{nom}:{hx}')
for k, v in PIERRE.items():
decl.append(f'{k}-rgb:{",".join(str(x) for x in C.hex2rgb(v))}')
PIERRES = '/* ââ les treize pierres de la Charte, dĂ©clarĂ©es ici et nulle part ailleurs (P3, 20/09) ââ */\n#ami-espacetemps{' + ';'.join(decl) + '}\n'
# ââ 3 · les six instruments portent la palette de la page ââââââââââââââââââââââââââââââââââââââââââââââââ
# (leurs sélecteurs ne bougent pas : seuls les NOMS qu'ils lisent changent de source)
PONT = {
'--bg': 'var(--noir)', '--panel': 'var(--encre)', '--panel2': 'var(--voile)',
'--text': 'var(--nacre)', '--muted': 'var(--gris)',
'--gold': 'var(--or)', '--pale': 'var(--or-pale)', '--accent': 'var(--or)',
'--cyan': 'var(--turq-pale)', '--blue': 'var(--bleu-cl)', '--pink': 'var(--fuch-cl)',
'--red': 'var(--rubis-cl)', '--green': 'var(--vert-cl)',
'--purple': 'var(--amethyste-ct)', '--violet': 'var(--tanzanite-ct)',
'--line': 'rgba(var(--or-rgb),.19)',
}
RACINES = ['ami-et-theme', 'ami-et-complement', 'ami-et-synastrie', 'ami-et-citoyennete', 'ami-et-village']
ponts = []
for rid in RACINES:
# on ne ponte que les noms que CET instrument déclare vraiment
i = h.find(f'<div id="{rid}" class="ami-et-outil"')
assert i > 0, rid
d = 0
for m in re.finditer(r'<div\b|</div>', h[i:]):
d += 1 if m.group(0) == '<div' else -1
if d == 0: fin = i + m.end(); break
seg = h[i:fin]
siens = set(re.findall(r'(--[a-z][a-z0-9-]*)\s*:', seg))
paires = [f'{k}:{v}' for k, v in PONT.items() if k in siens]
ponts.append(f'#{rid}{{' + ';'.join(paires) + '}')
J.append(f'{rid} : {len(paires)} noms pontés vers les pierres')
PONTS = ('/* ââ les six instruments lisent dĂ©sormais la palette de la page : leurs sĂ©lecteurs nâont pas bougĂ©,\n'
' seuls les noms quâils interrogent changent de source (P3, 20/09) ââ */\n' + '\n'.join(ponts) + '\n')
# ââ 4 · les polices restĂ©es en Inter âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
h2, k = re.subn(r'font:(\s*[^;{}]*?)Inter,\s*system-ui', lambda m: f'font:{m.group(1)}Jost,system-ui', h)
h = h2
if k: J.append(f'Inter â Jost Ă{k}')
# ââ 5 · on pose les deux feuilles AVANT celle du doigt (elle garde le dernier mot) ââââââââââââââââââââââââ
i = h.find('<style id="ami-doigt">')
assert i > 0, 'feuille du doigt introuvable : lance p2 dâabord'
h = h[:i] + f'<style id="ami-palette">\n{PIERRES}{PONTS}</style>\n' + h[i:]
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h) // 1024} Ko')
p4_et.py python3 p4_et.py et_p3.html et_p4.html (../spec/barre_extrait.json + boite_css.txt)458 lignes
5.3 Astro : la coque V16 sur une page qui a dĂ©jĂ sa navigation. La navigation (fermeture) expose window.amiActive/currentSub/currentMain au lieu d'ĂȘtre réécrite ; barre + #prog ; bande ; #amiPile + la pile qui se replie ; conclusion « đ« Tu Sais Ta Juste Place » ; â Emporter ; pied V16 ; â â° đ (la carte et la recherche lisent les noms DANS la page â rien Ă tenir Ă jour en double).
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · 5.3 Astro · Espace-Temps â L'ANATOMIE DE PAGE (la coque V16).
La page avait déjà sa racine `#ami-espacetemps` et sa propre navigation (4 portails à 17 volets) : on ne la défait pas,
on l'habille. Barre V16 + #prog en tĂȘte (data-ici=ami-astro, tiroir fermĂ©) ; bande des registres aprĂšs la une ;
la navigation entre dans `#amiPile` et se replie en une ligne de 44 px quand on descend ; conclusion « đ« Tu Sais Ta
Juste Place » (la RĂ©ponse â Diane, 20/09 : « Question sur la une, RĂ©ponse en conclusion : yes ») ; â Emporter ;
pied V16 (sceau đ«) Ă la place du pied maison ; coins đ§ â â¶ zoom Ă gauche, â â° đ Ă droite â le â Sommaire de la page
(les titres du volet ouvert) reste, il ne dit pas la mĂȘme chose que le â° (la carte des 4 portails et des 17 volets).
La navigation de la page vit dans une fermeture : elle expose ce qu'il faut pour que la boĂźte Ă outils la pilote
(window.amiActive, currentSub, currentMain) plutĂŽt que d'ĂȘtre réécrite.
Usage : python3 p4_et.py entree.html sortie.html
"""
import re, sys, json
SRC, OUT = sys.argv[1], sys.argv[2]
d = json.load(open('../spec/barre_extrait.json'))
BOITE_CSS = open('../spec/boite_css.txt', encoding='utf-8').read().replace('#ami-rech{display:none!important}\n', '')
h = open(SRC, encoding='utf-8').read(); J = []
ROOT = '#ami-espacetemps'; RID = 'ami-espacetemps'
def tete(bloc, why):
"""pose un bloc avant le VRAI </head> : le gabarit du document exporté en écrit un autre dans une chaßne JS"""
global h
i = h.find('</head>'); assert 0 < i < h.find('<body'), 'le </head> visĂ© nâest pas celui de la page'
h = h[:i] + bloc + h[i:]; J.append(why)
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:50]} Ă{n}')
# ââ 1 · la navigation de la page expose de quoi la piloter âââââââââââââââââââââââââââââââââââââââââââââââ
rep(' var main=MAIN_OF[sub];if(!main)return;last[main]=sub;',
' var main=MAIN_OF[sub];if(!main)return;last[main]=sub;window.currentSub=sub;window.currentMain=main;', 1,
'la navigation dit oĂč elle est (currentSub/currentMain)')
rep("ROOT.querySelectorAll('.grp').forEach(function(b){b.addEventListener('click'",
"window.amiActive=activate;window.ET_MAIN_OF=MAIN_OF;window.ET_FIRST=FIRST;\n"
"ROOT.querySelectorAll('.grp').forEach(function(b){b.addEventListener('click'", 1,
'la navigation sâouvre Ă la boĂźte Ă outils (window.amiActive)')
# ââ 1 bis · LA MĂMOIRE DE L'ONGLET ([stated] Diane, 20/09 : « quand je clique sur un autre onglet alors que
# j'ai déroulé la page, ça ouvre le nouvel onglet bien en bas, ça va pas ; faudrait la mémoire de l'onglet :
# il reste lĂ oĂč je suis partie, donc je peux comparer les informations sur 2 pages »).
# Chaque volet garde sa hauteur de lecture ; on y revient lĂ oĂč on l'a laissĂ©, et la premiĂšre fois on arrive
# en haut du volet, sous la pile â jamais « bien en bas ».
rep(" if(scroll&&wrap&&wrap.scrollIntoView)setTimeout(function(){wrap.scrollIntoView({behavior:'smooth',block:'start'})},20);",
" if(window.amiPoseVolet)setTimeout(function(){window.amiPoseVolet(sub,scroll)},20);\n"
" else if(scroll&&wrap&&wrap.scrollIntoView)setTimeout(function(){wrap.scrollIntoView({behavior:'smooth',block:'start'})},20);", 1,
'le volet se repose lĂ oĂč on lâavait laissĂ© (mĂ©moire de lâonglet)')
rep(" var main=MAIN_OF[sub];if(!main)return;last[main]=sub;window.currentSub=sub;window.currentMain=main;",
" var main=MAIN_OF[sub];if(!main)return;last[main]=sub;\n"
" if(window.amiNoteVolet)window.amiNoteVolet(window.currentSub,sub);\n"
" window.currentSub=sub;window.currentMain=main;", 1,
'la hauteur de lecture du volet quâon quitte est notĂ©e')
# ââ 2 · la pile : la navigation collĂ©e devient #amiPile ââââââââââââââââââââââââââââââââââââââââââââââââââ
m = re.search(r'<nav class="et-nav" id="et-tabs-wrap"[^>]*>', h); assert m, 'et-nav'
d0 = 0
for mm in re.finditer(r'<nav\b|</nav>', h[m.start():]):
d0 += 1 if mm.group(0) == '<nav' else -1
if d0 == 0: fin = m.start() + mm.end(); break
h = h[:m.start()] + '<div id="amiPile">\n' + h[m.start():fin] + '\n</div>' + h[fin:]
J.append('#amiPile autour des 4 portails et des 17 volets')
# ââ 3 · barre V16 + #prog, avant la racine âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="ami-astro">')
assert nav.count('data-ici') == 1
rep('<div id="ami-espacetemps">', d['prog'] + '\n' + nav + '\n<div id="ami-espacetemps">', 1, 'barre V16 + #prog en tĂȘte')
# ââ 4 · la bande des registres, aprĂšs la une âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"',
'href="https://agdistys.fr/ami-charte" title="Les quatre registres de lecture â dĂ©finis une fois, dans la Charte de Communication"')
rep('</header>\n<div id="amiPile">', '</header>\n' + bande + '\n<div id="amiPile">', 1, 'bande des registres aprĂšs la une')
# ââ 5 · la conclusion (la RĂ©ponse) + â Emporter + le pied V16 ââââââââââââââââââââââââââââââââââââââââââââ
CONCLUSION = '''
<section class="ami-conclusion" aria-labelledby="amiRep">
<p class="ac-kick">La Réponse du volume</p>
<h2 class="ami-h1" id="amiRep">đ« Tu Sais Ta Juste Place</h2>
<p class="ac-lead">La Question de la une demandait : <em>Comment Trouver Ma Place dans le Moment Justement ?</em>
Le chemin lâa parcourue â đ Observer, đ Situer, đȘ InterprĂ©ter, đ§ Choisir.</p>
<p class="ac-p">Ta place nâest pas un rang : câest une <strong>position dans un instant</strong>. Le ciel ne la dĂ©cide pas,
il la <strong>date</strong>. Les calendriers ne mesurent pas le Temps, ils lâ<strong>accordent</strong> â et chaque culture
lâaccorde autrement sans quâaucune ait tort. Ce que tu portes nâest ni une nature ni un destin : câest une
<strong>grammaire</strong> que tu peux lire, et une lecture nâa dâautoritĂ© que celle que tu lui donnes.</p>
<p class="ac-p">Alors ĂȘtre au bon moment, ce nâest pas attendre le moment juste : câest <strong>habiter celui-ci</strong>,
avec ceux qui y sont, sans effacer les Je dans le Nous.</p>
<p class="ac-sceau">đ« <span>Tu Sais Ta Juste Place pour Ătre au Bon Moment</span> đŻ</p>
</section>
'''
EMPORTER = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">â Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong> : une fois tĂ©lĂ©chargĂ©, <strong>il fonctionne hors ligne</strong> et reste vivant â quatre portails, dix-sept volets, <strong>les six instruments dans la page</strong> (IpséïtĂ©, ThĂšme, ComplĂ©mentaritĂ©, Synastrie, CitoyennetĂ©, Village), lecture continue, contraste, recherche.</p>
<div class="emp-b">
<button id="dlHtml" type="button">âŹïž Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">đ¶ PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">đ
<span data-ami="version">V17</span> â arrĂȘtĂ©e au <span data-ami="date">20 septembre 2026</span>. <em>Les renvois vers lâextĂ©rieur peuvent se briser avec le temps : vĂ©rifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="đ Le sceau du volume">đ</div>',
'<div class="ap-sceau" title="đ« Le sceau du volume">đ«</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', '<p class="ap-titre">Astro â Espace-Temps</p>')
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple â La Vie</p>',
'<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">5.3</span> â Tu Sais Ta Juste Place pour Ătre au Bon Moment</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas ĂȘtre crue sur parole au vol,\nje veux ĂȘtre sue sans le moindre bĂ©mol.</p>",
"<p class=\"ap-cit\">Pour un Ici & Maintenant plus Joli,\nJusquâĂ Demain, en Ayant Appris\nLes Leçons dâHier, Aujourdâhui.</p>")
assert 'đ« Le sceau' in pied and 'Astro â Espace-Temps' in pied and 'Volume <span' in pied and 'Leçons dâHier' in pied
m = re.search(r'<footer class="et-footer">.*?</footer>\s*', h, re.S); assert m, 'pied maison'
h = h[:m.start()] + h[m.end():]
J.append('le pied maison (AGDISTYS · DIANE SERANT & ISILâŠ) sâen va : sa phrase est dĂ©jĂ sur la une')
i = h.rfind('</main>'); assert i > 0, '</main>'
h = h[:i] + CONCLUSION + EMPORTER + h[i:]
J.append('conclusion « đ« Tu Sais Ta Juste Place » (la RĂ©ponse) + â Emporter, avant la fin de la page')
# le pied V16 se pose JUSTE APRĂS la fermeture de la racine (calculĂ©e, pas devinĂ©e)
i = h.find('<div id="ami-espacetemps">'); d0 = 0
for mm in re.finditer(r'<div\b|</div>', h[i:]):
d0 += 1 if mm.group(0) == '<div' else -1
if d0 == 0: fin = i + mm.end(); break
h = h[:fin] + '\n' + pied + '\n' + h[fin:]
J.append('pied V16 (sceau đ«) posĂ© aprĂšs la racine')
# ââ 5 bis · UN SEUL SOMMAIRE ([stated] Diane, 20/09 : « y'a 2 toc ça va pas » â « on garde le burger charte
# et on supprime l'autre »). Le â de la page et son panneau s'en vont ; le â° de la coque V16 reprend ce
# qu'il faisait (les titres du volet ouvert) EN PLUS de la carte des 4 portails et des 17 volets.
m = re.search(r'<button type="button" id="et-toc-btn".*?</button>\s*<nav id="et-toc-panel".*?</nav>\s*', h, re.S)
assert m, 'le â de la page'
h = h[:m.start()] + h[m.end():]
J.append('le â de la page ĂŽtĂ© : un seul sommaire, le â° de la Charte')
# ââ 6 · la feuille du composant ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
css = d['css']
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', ROOT + r' \1', css)
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items() if not v.startswith('4px}'))
feuille = ('<style id="ami-composant">\n/* ââ LA BARRE, LE PIED, LES REGISTRES, LâEMPORT â pris Ă la Charte de Communication V5 ââ */\n'
':root{' + rootvals + ';--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
f'{ROOT} .emporter{{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);border:1px solid var(--or-somb);border-radius:14px;text-align:center}}\n'
f"{ROOT} .emporter h2{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none;border:0;padding:0}}\n"
f"{ROOT} .emporter p{{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.95rem;line-height:1.7;margin:.4rem auto;max-width:60ch}}\n"
f"{ROOT} .emporter p strong{{color:var(--nacre);font-weight:500;font-size:inherit}}\n"
f"{ROOT} .emporter .emp-b{{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}}\n"
f"{ROOT} .emporter .emp-b button{{font-family:'Jost',sans-serif;font-size:.95rem;color:var(--or);background:rgba(255,255,255,.05);border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:44px}}\n"
f"{ROOT} .emporter .emp-b button:hover{{background:rgba(255,255,255,.1);color:var(--or-pale)}}\n"
f"{ROOT} .emporter .emp-d{{font-size:.86rem;color:var(--gris)}}\n{ROOT} .emporter [data-ami]{{color:var(--or-pale);font-size:inherit}}\n"
f'{ROOT} .bande-registres{{margin:1.2rem auto 0}}\n'
f'{ROOT} #amiPile{{position:sticky;top:var(--nav-h,0px);z-index:100;background:var(--voile)}}\n'
f'{ROOT} .et-nav{{position:static;top:auto}}\n'
# ââ la conclusion
f'{ROOT} .ami-conclusion{{max-width:46rem;margin:3.4rem auto 1rem;padding:1.8rem 1.6rem;border:1px solid var(--or-somb);border-radius:16px;'
'background:radial-gradient(circle at 50% 0,rgba(var(--or-rgb),.07),transparent 22rem),var(--encre);text-align:center}\n'
f"{ROOT} .ami-conclusion .ac-kick{{font-family:'Jost',sans-serif;font-size:.8rem;letter-spacing:.28em;text-transform:uppercase;color:var(--or-somb);margin:0 0 .6rem}}\n"
f'{ROOT} .ami-conclusion .ami-h1{{color:var(--or-pale);margin:0 0 1rem}}\n'
f"{ROOT} .ami-conclusion .ac-lead{{font-family:'Cormorant Garamond',Georgia,serif;font-size:1.22rem;line-height:1.7;color:var(--nacre);margin:0 auto 1rem;max-width:52ch}}\n"
f"{ROOT} .ami-conclusion .ac-p{{font-family:'Jost',sans-serif;font-size:1rem;line-height:1.8;color:var(--gris-cl);margin:0 auto .9rem;max-width:56ch;text-align:left}}\n"
f'{ROOT} .ami-conclusion .ac-p strong{{color:var(--nacre);font-weight:500}}\n'
f"{ROOT} .ami-conclusion .ac-sceau{{font-family:'Cormorant Garamond',serif;font-size:1.3rem;color:var(--or);margin:1.4rem 0 0;line-height:1.5}}\n"
f'{ROOT} .ami-conclusion .ac-sceau span{{color:var(--or-pale)}}\n'
# ââ la signature de la une garde ses majuscules internes (ConScience UniVerCelle)
f'{ROOT} .couv-inner .sig{{text-transform:none;letter-spacing:.12em}}\n'
+ css + '\n</style>\n')
assert '*{margin:0' not in feuille
tete(feuille, 'feuille du composant')
# ââ 7 · les scripts du composant (barre, đ§ â â¶ zoom, Emporter) ââââââââââââââââââââââââââââââââââââââââââ
js = d['js_barre'].replace("document.getElementById('ami-charte')||document.body", f"document.getElementById('{RID}')||document.body")
# ce que le composant faisait pour la Charte (ses onglets .ong/.pan) n'a pas de sens ici : la page a sa navigation
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" /* l'export part du premier volet, pile dépliée, comme une page neuve */
d.querySelectorAll('.et-pane').forEach(function(p){ p.classList.toggle('active', p.id==='pane-inconcevable'); });
d.querySelectorAll('.subbar').forEach(function(s){ s.classList.toggle('active', s.getAttribute('data-subs')==='evolution'); });
d.querySelectorAll('.grp').forEach(function(b){ b.classList.toggle('active', b.getAttribute('data-main')==='evolution'); });
var mini=d.querySelector('#ami-pile-mini'); if(mini) mini.remove(); d.querySelector('body').classList.remove('ami-repli');""")
assert "'.et-pane').forEach" in js
js = js.replace(""" if(a){ a.classList.add('ici');
deplie(true,false);
var p=N.querySelector('.porte[data-c="foret"]'); if(p) pose(p,true);
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}""", """ if(a){ a.classList.add('ici');
/* la page se marque, mais n'ouvre plus le tiroir toute seule (Diane, 17 sept) */
var t=a.closest('.vols'), c=t&&N.querySelector('button.m[data-p="'+t.dataset.p+'"]');
if(c){ c.classList.add('on'); t.classList.add('on'); }
}"""); assert "n'ouvre plus le tiroir" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';",
"a.download='5.3_ASTRO_Espace-Temps_aMi_'+(window.__amiV||'')+'.html';"); assert '5.3_ASTRO' in js
shim = f"""
/* ââ CE QUE LE VOLUME ĂCOUTE : la lecture continue (les 17 volets dĂ©roulĂ©s) ââ */
(function(){{'use strict';
var R=document.getElementById('{RID}'); if(!R) return; var memo=null;
/* la rĂšgle du đ§ (Diane, 19/09) : allumer OU Ă©teindre la lecture continue ne bouge pas ce qu'on lit â
on note le point sous les yeux (sondé SOUS la pile collée), et on lui rend sa hauteur aprÚs, dans les deux sens */
function ancre(){{ var cs=getComputedStyle(document.documentElement), vt=parseFloat(cs.getPropertyValue('--vue-top'))||0, vh=parseFloat(cs.getPropertyValue('--vue-h'))||window.innerHeight;
var pb=0, tb=document.getElementById('amiPile')||document.getElementById('ami-pile-mini'); if(tb){{ var rb=tb.getBoundingClientRect(); if(rb.bottom>0&&rb.bottom<vh*0.9) pb=rb.bottom; }}
var el=null, sec=null, ks=[0.3,0.5,0.7,0.9,0.15];
for(var q=0;q<ks.length&&!sec;q++){{ el=document.elementFromPoint(window.innerWidth/2, vt+pb+(vh-pb)*ks[q]); sec=el&&el.closest('#{RID} .et-pane'); }}
return {{el:el, sec:sec, top:el?el.getBoundingClientRect().top:0, stop:sec?sec.getBoundingClientRect().top:0}}; }}
function rends(a){{ if(!a.el) return; function pose(){{ var dd = a.el.isConnected ? a.el.getBoundingClientRect().top-a.top : (a.sec&&a.sec.isConnected ? a.sec.getBoundingClientRect().top-a.stop : 0); if(Math.abs(dd)>1) window.scrollTo({{top:Math.max(0,window.pageYOffset+dd), behavior:'instant'}}); }}
pose();
if(document.documentElement.classList.contains('i-cadre-fil')) [200,700,1300,2000].forEach(function(t){{ setTimeout(pose,t); }}); }}
document.addEventListener('ami:lecture',function(e){{
var on=e.detail&&e.detail.actif;
if(on){{ memo=window.currentSub||'inconcevable'; var a=ancre(); R.querySelectorAll('.et-pane').forEach(function(p){{ p.classList.add('active'); }}); R.classList.add('lecture'); rends(a); }}
else {{ R.classList.remove('lecture'); var a=ancre(); var cible=a.sec?a.sec.id.replace('pane-',''):memo;
window.__amiSansSaut=true; try{{ if(cible&&window.amiActive) window.amiActive(cible,false); }} finally {{ setTimeout(function(){{ window.__amiSansSaut=false; }},400); }}
rends(a); }}
}});
}})();
/* ââ LA BARRE DE PROGRESSION â elle mesure le volet ouvert ââ */
(function(){{'use strict';
var prog=document.getElementById('prog'), R=document.getElementById('{RID}'); if(!prog||!R) return;
function pile(){{ var n=document.querySelector('.ami-barre'); return n?Math.min(n.getBoundingClientRect().height,144):0; }}
function volet(){{ if(R.classList.contains('lecture')) return R; return R.querySelector('.et-pane.active')||R; }}
function avance(){{ var v=volet(), r=v.getBoundingClientRect(), y=window.pageYOffset, debut=r.top+y-pile(), fin=r.bottom+y-window.innerHeight, course=fin-debut,
p=course>4 ? (y-debut)/course : (r.bottom<=window.innerHeight?1:0); prog.style.width=(Math.max(0,Math.min(1,p))*100).toFixed(2)+'%'; }}
window.addEventListener('scroll',avance,{{passive:true}}); window.addEventListener('resize',avance);
document.addEventListener('click',function(e){{ if(e.target.closest&&e.target.closest('.grp,.subbar button,#amiLecture,#amiPlein,[data-act]')) setTimeout(avance,90); }},true);
document.addEventListener('ami:lecture',function(){{ setTimeout(avance,90); }}); avance();
}})();
"""
contraste = f"""
<style id="ami-contraste">
/* ââ â LE CONTRASTE ĂLEVĂ : chaque encre monte Ă son niveau -ct ââ */
body.ami-contraste {ROOT},body.ami-contraste{{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;--or-somb:#b09030;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;--fuchsia:var(--grenat-ct);--fuch-cl:#ffb3dd;--turq:var(--amazonite-ct);--turq-pale:#9fe8e6}}
body.ami-contraste {ROOT}{{font-weight:400}}
body.ami-contraste {ROOT} em{{font-style:normal}}
</style>
"""
# ââ 8 · les coins et la boĂźte Ă outils âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
coins = '''
<button class="ami-coin h" id="amiHaut" type="button" hidden aria-label="Revenir en haut" title="â Revenir en haut">â</button>
<button class="ami-coin d3" id="ami-toc-toggle" type="button" aria-expanded="false" aria-controls="ami-toc-panel" aria-label="Carte du volume" title="â° Les quatre portails et les dix-sept volets">â°</button>
<div id="ami-toc-panel" class="ami-toc" hidden role="dialog" aria-label="Carte du volume"></div>
<button class="ami-coin r" id="amiRech" type="button" aria-label="Rechercher" title="Chercher dans le volume (Ctrl+F)"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></button>
<div id="ami-rech" role="dialog" aria-modal="true" aria-label="Rechercher dans le volume">
<div class="rc-b">
<div class="rc-h"><span class="rc-loupe"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" style="width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.7"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.4 15.4 20.5 20.5"/></svg></span>
<input type="search" id="rc-q" placeholder="Chercher dans les dix-sept voletsâŠ" autocomplete="off" spellcheck="false" aria-label="Chercher">
<span class="rc-n" id="rc-n"></span>
<button class="rc-x" type="button" id="rc-x" aria-label="Fermer">â</button></div>
<div class="rc-l" id="rc-l" role="listbox"></div>
</div>
</div>
'''
boite_js = r'''
<script id="ami-boite-js">
/* ââ LA BOĂTE Ă OUTILS : â Haut · â° la carte (4 portails â 17 volets) · đ la recherche (elle indexe la racine,
les six instruments compris â ils sont dans la page maintenant â et ouvre le bon volet avant de sauter). ââ */
(function(){'use strict';
var R=document.getElementById('ami-espacetemps'); if(!R) return;
var H=document.getElementById('amiHaut');
if(H){ var suit=function(){ H.hidden = window.pageYOffset < 600; }; window.addEventListener('scroll',suit,{passive:true}); suit();
H.addEventListener('click',function(){ window.scrollTo({top:0,behavior:'smooth'}); }); }
/* les noms se lisent dans la page elle-mĂȘme : rien Ă tenir Ă jour en double */
function nomGroupe(g){ var b=R.querySelector('.grp[data-main="'+g+'"]'); if(!b) return g;
var e=b.querySelector('.grp-em'), l=b.querySelector('.grp-lbl'); return (e?e.textContent.trim()+' ':'')+(l?l.textContent.trim():g); }
function nomChap(s){ var b=R.querySelector('.subbar button[data-sub="'+s+'"]'); if(!b) return s;
var e=b.querySelector('.sb-em'); return e ? (e.textContent.trim()+' '+b.textContent.replace(e.textContent,'').trim()) : b.textContent.trim(); }
function groupeDe(s){ var M=window.ET_MAIN_OF||{}; return M[s]||null; }
function ordreGroupes(){ return [].slice.call(R.querySelectorAll('.grp')).map(function(b){ return b.getAttribute('data-main'); }); }
function voletsDe(g){ return [].slice.call(R.querySelectorAll('.subbar[data-subs="'+g+'"] button')).map(function(b){ return b.getAttribute('data-sub'); }); }
function pile(yDoc){ var p=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0;
var tb=document.getElementById('amiPile'); if(!tb) return p;
var b=R.querySelector('.bande-registres')||R.querySelector('.couverture'), h0=0;
if(b){ var rb=b.getBoundingClientRect(); h0=rb.bottom+window.pageYOffset; } else h0=tb.offsetTop;
var repli = (yDoc!==undefined) ? (yDoc > h0 + tb.offsetHeight + 160) : document.body.classList.contains('ami-repli');
return p + (repli ? 44 : tb.offsetHeight); }
function va(sub, apres){ if(window.amiActive) window.amiActive(sub,false);
setTimeout(function(){ var el=document.getElementById('pane-'+sub); if(el){ var yd=el.getBoundingClientRect().top+window.pageYOffset; window.scrollTo({top:Math.max(0,yd-pile(yd)-8),behavior:'smooth'}); } if(apres) apres(); },160); }
/* â° la carte */
var T=document.getElementById('ami-toc-toggle'), P=document.getElementById('ami-toc-panel');
/* le â° dit DEUX choses : la carte du volume (4 portails â 17 volets) et, dĂ©pliĂ©s, les titres du volet ouvert
â c'est l'ancien â de la page, entrĂ© ici (Diane, 20/09 : « y'a 2 toc ça va pas ») */
function esc2(s){ return String(s).replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function titresDuVolet(sub){ var pane=document.getElementById('pane-'+sub); if(!pane) return [];
return [].slice.call(pane.querySelectorAll('h2,h3')).filter(function(t){ return t.offsetParent!==null && t.textContent.trim().length>1 && !t.closest('.ami-conclusion,.emporter'); })
.map(function(t,n){ if(!t.id) t.id='et-t-'+sub+'-'+n; return {id:t.id, txt:t.textContent.trim(), p:t.tagName==='H3'}; }); }
function batiToc(){ var ici=window.currentSub;
var html='<div class="toc-h"><span>â° Sommaire</span><button type="button" class="toc-x" aria-label="Fermer">â</button></div>';
var ts=titresDuVolet(ici);
if(ts.length){ html+='<div class="toc-ici"><span class="toc-ici-t">Dans ce volet â '+esc2(nomChap(ici))+'</span><div class="toc-s">';
ts.forEach(function(t){ html+='<a href="#'+t.id+'" data-h="'+t.id+'"'+(t.p?' class="lvl3"':'')+'>'+esc2(t.txt)+'</a>'; });
html+='</div></div>'; }
html+='<div class="toc-carte-t">La carte du volume</div>';
ordreGroupes().forEach(function(g){ html+='<button type="button" class="toc-o" data-t="'+voletsDe(g)[0]+'">'+nomGroupe(g)+'</button><div class="toc-s">';
voletsDe(g).forEach(function(s){ html+='<a href="#pane-'+s+'" data-t="'+s+'"'+(s===ici?' class="ici"':'')+'>'+esc2(nomChap(s))+'</a>'; }); html+='</div>'; });
P.innerHTML=html;
P.querySelector('.toc-x').addEventListener('click',fermeToc);
P.querySelectorAll('.toc-o,.toc-s a[data-t]').forEach(function(b){ b.addEventListener('click',function(e){ e.preventDefault(); fermeToc(); va(b.dataset.t); }); });
P.querySelectorAll('a[data-h]').forEach(function(a){ a.addEventListener('click',function(e){ e.preventDefault(); fermeToc();
var el=document.getElementById(a.dataset.h); if(!el) return;
var yd=el.getBoundingClientRect().top+window.pageYOffset; window.scrollTo({top:Math.max(0,yd-pile(yd)-12),behavior:'smooth'}); }); }); }
function ouvreToc(){ batiToc(); P.hidden=false; T.setAttribute('aria-expanded','true'); }
function fermeToc(){ P.hidden=true; T.setAttribute('aria-expanded','false'); }
if(T&&P){ T.addEventListener('click',function(){ P.hidden?ouvreToc():fermeToc(); }); document.addEventListener('keydown',function(e){ if(e.key==='Escape'&&!P.hidden) fermeToc(); }); }
/* đ la recherche */
var B=document.getElementById('amiRech'), D=document.getElementById('ami-rech'), Q=document.getElementById('rc-q'), L=document.getElementById('rc-l'), N=document.getElementById('rc-n'), X=document.getElementById('rc-x');
if(!B||!D) return;
var IDX=null, RES=[], SEL=-1;
function bati(){ IDX=[];
var w=document.createTreeWalker(R,NodeFilter.SHOW_ELEMENT|NodeFilter.SHOW_TEXT), n, chap='';
while((n=w.nextNode())){
if(n.nodeType===1){ if(n.tagName==='H2'||n.tagName==='H3'){ chap=n.textContent.trim(); } continue; }
var s=n.textContent.replace(/\s+/g,' ').trim(); if(s.length<3) continue;
var p=n.parentElement; if(!p||p.closest('script,style,svg,#ami-rech,#ami-toc-panel,#amiPile,#ami-pile-mini,.ami-barre,.ami-pied,.emporter,button,select,option')) continue;
var sec=p.closest('.et-pane'); IDX.push({t:s, tl:s.toLowerCase(), el:p, id:sec?sec.id.replace('pane-',''):null, chap:chap}); } }
function esc(s){ return s.replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); }
function cherche(q){ q=q.trim().toLowerCase(); L.innerHTML=''; RES=[]; SEL=-1;
if(q.length<2){ N.textContent=''; L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; return; }
if(!IDX) bati();
var vus={}, par={}, ordre=[];
IDX.forEach(function(e){ var i=e.tl.indexOf(q); if(i<0) return; var k=(e.id||'')+'|'+e.chap+'|'+e.t.slice(0,60); if(vus[k]) return; vus[k]=1;
var g=e.id?(nomGroupe(groupeDe(e.id))+' âș '+nomChap(e.id)):'â'; if(!par[g]){par[g]=[];ordre.push(g);} if(par[g].length<40) par[g].push({e:e,i:i}); });
var tot=0; ordre.forEach(function(g){ tot+=par[g].length; });
N.textContent = tot? tot+(tot>1?' passages':' passage') : 'rien';
if(!tot){ L.innerHTML='<div class="rc-v">Rien pour « '+esc(q)+' ».</div>'; return; }
ordre.forEach(function(g){ var dv=document.createElement('div'); dv.className='rc-g'; dv.textContent=g; L.appendChild(dv);
par[g].forEach(function(r){ var b=document.createElement('button'); b.type='button'; b.className='rc-r'; b.setAttribute('role','option');
var a=Math.max(0,r.i-46), z=Math.min(r.e.t.length,r.i+q.length+70);
b.innerHTML=(a?'âŠ':'')+esc(r.e.t.slice(a,r.i))+'<mark>'+esc(r.e.t.slice(r.i,r.i+q.length))+'</mark>'+esc(r.e.t.slice(r.i+q.length,z))+(z<r.e.t.length?'âŠ':'')+(r.e.chap?'<span class="rc-c">'+esc(r.e.chap)+'</span>':'');
b.dataset.k=RES.length; RES.push(r.e); b.addEventListener('click',function(){ saute(+b.dataset.k); }); L.appendChild(b); }); }); }
function saute(k){ var e=RES[k]; if(!e) return; ferme();
if(e.id&&window.currentSub!==e.id&&window.amiActive) window.amiActive(e.id,false);
setTimeout(function(){ var cible=e.el.closest('p,li,td,th,h1,h2,h3,h4,blockquote,figure,div')||e.el;
var yd=cible.getBoundingClientRect().top+window.pageYOffset; window.scrollTo({top:Math.max(0,yd-pile(yd)-64),behavior:'smooth'});
cible.classList.remove('ami-trouve'); void cible.offsetWidth; cible.classList.add('ami-trouve'); setTimeout(function(){cible.classList.remove('ami-trouve');},2600); },220); }
function ouvre(){ D.classList.add('on'); B.setAttribute('aria-pressed','true'); Q.value=''; Q.focus(); L.innerHTML='<div class="rc-v">Deux lettres au moins.</div>'; N.textContent=''; }
function ferme(){ D.classList.remove('on'); B.setAttribute('aria-pressed','false'); }
X.addEventListener('click',ferme); D.addEventListener('click',function(e){ if(e.target===D) ferme(); });
var tm=null; Q.addEventListener('input',function(){ clearTimeout(tm); tm=setTimeout(function(){ cherche(Q.value); },140); });
Q.addEventListener('keydown',function(e){ var bs=L.querySelectorAll('.rc-r');
if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); if(!bs.length) return; SEL=Math.max(0,Math.min(bs.length-1,SEL+(e.key==='ArrowDown'?1:-1))); bs.forEach(function(b2,i){b2.classList.toggle('sel',i===SEL);}); bs[SEL].scrollIntoView({block:'nearest'}); }
else if(e.key==='Enter'){ e.preventDefault(); saute(SEL>=0?SEL:0); } else if(e.key==='Escape'){ ferme(); } });
addEventListener('keydown',function(e){ if((e.ctrlKey||e.metaKey)&&(e.key==='f'||e.key==='F')){ e.preventDefault(); ouvre(); } else if(e.key==='Escape'&&D.classList.contains('on')) ferme(); });
B.addEventListener('click',ouvre);
})();
</script>
<script id="ami-volet-memoire">
/* ââ LA MĂMOIRE DE L'ONGLET (Diane, 20/09) : chaque volet garde sa hauteur de lecture. On revient dedans lĂ oĂč
on l'avait laissĂ© ; la premiĂšre fois, on arrive EN HAUT du volet, sous la pile collĂ©e â jamais « bien en bas ».
Ainsi on peut dĂ©rouler un volet, aller en comparer un autre, et revenir sans se reperdre. ââ */
(function(){'use strict';
var R=document.getElementById('ami-espacetemps'); if(!R) return;
var pos={};
function pile(){ var p=parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0;
var tb=document.getElementById('amiPile'); if(!tb) return p;
return p + (document.body.classList.contains('ami-repli') ? 44 : tb.offsetHeight); }
function hautDu(sub){ var el=document.getElementById('pane-'+sub); if(!el) return 0;
var yd=el.getBoundingClientRect().top+window.pageYOffset; return Math.max(0, yd-pile()-8); }
window.amiNoteVolet=function(avant,neuf){ if(!avant||avant===neuf) return;
if(R.classList.contains('lecture')) return; /* en lecture continue tout est ouvert : rien Ă noter */
pos[avant]=window.pageYOffset; };
window.amiPoseVolet=function(sub,scroll){
if(R.classList.contains('lecture')) return;
if(!scroll && pos[sub]===undefined) return; /* appel programmĂ© (â°, đ, đ§) : ils posent eux-mĂȘmes */
var y=pos[sub], haut=hautDu(sub);
if(y===undefined) y=haut; /* premiĂšre visite : le haut du volet */
else y=Math.max(haut, Math.min(y, document.documentElement.scrollHeight-window.innerHeight));
window.scrollTo({top:y,behavior:'instant'}); };
/* on note aussi en continu : un volet quittĂ© par le â° ou le đ garde sa place */
var t=null;
window.addEventListener('scroll',function(){ if(t) return; t=setTimeout(function(){ t=null;
if(window.currentSub && !R.classList.contains('lecture')) pos[window.currentSub]=window.pageYOffset; },200); },{passive:true});
})();
</script>
<script id="ami-pile-js">
/* ââ LA PILE QUI SE REPLIE : quand on descend dans un volet, les deux rangĂ©es collĂ©es (4 portails + 17 volets)
se replient en une ligne de 44 px qui dit oĂč on est â « âĄïž Trouver ses RĂ©sonances âș đ« Mon ThĂšme Astro aMi ⎠».
Un tap la redéploie et remonte aux portails ; remonter de 40 px la redéploie. Sur un téléphone, cette pile
mangeait un tiers de l'Ă©cran : c'est une bonne part de la gĂȘne du festival. ââ */
(function(){'use strict';
var R=document.getElementById('ami-espacetemps'); if(!R) return;
var tb=document.getElementById('amiPile'); if(!tb) return;
var mini=document.createElement('button'); mini.type='button'; mini.id='ami-pile-mini'; mini.setAttribute('aria-label','OĂč je suis â redĂ©ployer les onglets'); mini.setAttribute('data-ami-sous-pile','1');
tb.parentNode.insertBefore(mini, tb);
function nomGroupe(g){ var b=R.querySelector('.grp[data-main="'+g+'"]'); if(!b) return g||'';
var e=b.querySelector('.grp-em'), l=b.querySelector('.grp-lbl'); return (e?e.textContent.trim()+' ':'')+(l?l.textContent.trim():g); }
function nomChap(s){ var b=R.querySelector('.subbar button[data-sub="'+s+'"]'); if(!b) return s;
var e=b.querySelector('.sb-em'); return e ? (e.textContent.trim()+' '+b.textContent.replace(e.textContent,'').trim()) : b.textContent.trim(); }
function texte(){ var g=window.currentMain, t=window.currentSub; var s=nomGroupe(g); if(t) s+=' <span class="pm-sep">âș</span> '+nomChap(t);
mini.innerHTML='<span class="pm-ou">'+s+'</span><span class="pm-fl">âŽ</span>'; }
function haut(){ var b=R.querySelector('.bande-registres')||R.querySelector('.couverture'); return b?b.getBoundingClientRect().bottom+window.pageYOffset:tb.offsetTop; }
var last=window.pageYOffset, repli=false, monte=0;
function pose(on){ if(repli===on) return; repli=on; document.body.classList.toggle('ami-repli',on); if(on){ texte(); tb.setAttribute('data-ami-decolle','1'); } else tb.removeAttribute('data-ami-decolle'); }
function veille(){ if(document.body.classList.contains('ami-sans-repli')||R.classList.contains('lecture')){ pose(false); return; }
var y=window.pageYOffset, h0=haut(), bas=h0+Math.min(tb.offsetHeight, innerHeight*0.35)+120;
if(y<last) monte+=last-y; else monte=0;
if(y<h0+40){ pose(false); }
else if(y>last+2 && y>bas){ pose(true); }
else if(monte>40){ pose(false); }
last=y; }
window.addEventListener('scroll',veille,{passive:true});
mini.addEventListener('click',function(){ pose(false); var y=haut()-(parseInt(getComputedStyle(document.documentElement).getPropertyValue('--nav-h'))||0); window.scrollTo({top:Math.max(0,y),behavior:'smooth'}); });
document.addEventListener('click',function(e){ if(e.target.closest&&e.target.closest('.grp,.subbar button')) setTimeout(texte,60); },true);
document.addEventListener('ami:lecture',function(){ setTimeout(veille,60); });
texte();
})();
</script>
'''
boite_css = '''
<style id="ami-boite">
''' + BOITE_CSS + '''
/* les places du 18/09 : de bas en haut đ â° â (le â de la page est parti : un seul sommaire) */
#amiRech{bottom:1rem!important;right:1rem!important}#ami-toc-toggle{bottom:4rem!important;right:1rem!important}#amiHaut{bottom:7rem!important;right:1rem!important}
.ami-toc{position:fixed;right:1rem;bottom:6.6rem;z-index:61;width:min(22rem,calc(100vw - 2rem));max-height:min(70vh,34rem);overflow:auto;background:var(--encre);border:1px solid rgba(var(--or-rgb),.34);border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.6);font-family:'Jost',sans-serif}
.ami-toc .toc-h{display:flex;justify-content:space-between;align-items:center;padding:.6rem .9rem;border-bottom:1px solid rgba(var(--or-rgb),.18);color:var(--or-pale);font-size:.9rem;letter-spacing:.08em}
.ami-toc .toc-x{background:none;border:0;color:var(--gris);cursor:pointer;font-size:1rem;min-width:44px;min-height:44px}
.ami-toc .toc-o{display:block;width:100%;text-align:left;background:none;border:0;border-top:1px solid rgba(255,255,255,.05);color:var(--or-pale);font:500 .95rem 'Jost',sans-serif;padding:.55rem .9rem;cursor:pointer;min-height:44px;text-transform:none;letter-spacing:0}
.ami-toc .toc-o:hover{background:rgba(var(--or-rgb),.08)}
.ami-toc .toc-s{padding:0 .4rem .4rem .9rem}
.ami-toc .toc-s a{display:block;color:var(--gris-cl);text-decoration:none;font-size:.9rem;padding:.5rem;border-radius:6px;min-height:36px}
.ami-toc .toc-s a:hover{background:rgba(var(--or-rgb),.08);color:var(--nacre)}
.ami-toc .toc-s a.ici{color:var(--or);background:rgba(var(--or-rgb),.10)}
.ami-toc .toc-s a.lvl3{padding-left:1.4rem;color:var(--gris)}
.ami-toc .toc-ici{border-top:1px solid rgba(var(--or-rgb),.14);background:rgba(var(--or-rgb),.04)}
.ami-toc .toc-ici-t,.ami-toc .toc-carte-t{display:block;padding:.6rem .9rem .2rem;color:var(--or-gras);font:500 .8rem 'Jost',sans-serif;letter-spacing:.16em;text-transform:uppercase}
.ami-toc .toc-carte-t{border-top:1px solid rgba(var(--or-rgb),.14);margin-top:.3rem}
.ami-trouve{outline:2px solid var(--or);outline-offset:4px;border-radius:4px;transition:outline-color 2s}
/* ââ la pile qui se replie ââ */
/* la ligne « oĂč je suis » doit ĂȘtre OPAQUE : « #ami-espacetemps button{background:rgba(200,168,74,.07)} »
de la page bat un simple #ami-pile-mini, et le texte de la page transparaissait derriĂšre (vu le 20/09) */
#ami-espacetemps #ami-pile-mini,#ami-pile-mini{display:none;position:fixed;left:0;right:0;top:var(--nav-h,0px);z-index:101;width:100%;box-sizing:border-box;min-height:44px;padding:.5rem 1rem;background:var(--voile)!important;border:0;border-bottom:1px solid rgba(var(--or-rgb),.25);color:var(--or-pale);font-family:'Jost',sans-serif;font-size:.9rem;text-align:left;cursor:pointer;align-items:center;justify-content:space-between;gap:.6rem;box-shadow:0 8px 24px rgba(0,0,0,.55)}
#ami-pile-mini .pm-ou{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
#ami-pile-mini .pm-sep{color:var(--gris);margin:0 .1rem}
#ami-pile-mini .pm-fl{color:var(--or);flex:0 0 auto}
body.ami-repli #ami-espacetemps #ami-pile-mini,body.ami-repli #ami-pile-mini{display:flex}
body.ami-repli #ami-espacetemps #amiPile{position:static}
</style>
'''
tete(boite_css + contraste, 'feuilles boĂźte + pile + contraste')
i = h.rfind('</body>'); assert i > 0
h = h[:i] + coins + '\n<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n' + boite_js + h[i:]
J.append('coins đ§ â â¶ zoom â â° đ · scripts du composant · boĂźte Ă outils · pile qui se replie')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h) // 1024} Ko')
p5_et.py python3 p5_et.py et_p19.html et_p5.html67 lignes
5.3 Astro : les encres mesurées (grp-pol et ac-kick relevés au-dessus de 4,5 ; capitales espacées en Jost ; émojis droits), --wrap réglé sur le conteneur (94vw débordait à 320), les fils qui sortaient (fichier .html local, cms.webnode), un <footer> égaré redevenu note, --ame-cl et --vigilance déclarées.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P5 · 5.3 Astro â LES ENCRES ET LES FINITIONS (mesurĂ©, pas supposĂ©).
Mesuré sur et_p19 (Chromium, 320/390/768/1280) :
· `--wrap:min(1320px,94vw)` : 94 % de la FENĂTRE dans un conteneur qui, lui, a des marges â Ă 320 px la page
dépassait de 3 px (scrollW 323 pour 320). La largeur se rÚgle sur le conteneur : min(1320px,100%).
· `.grp-pol` (le sous-titre des quatre portails) et `.ac-kick` étaient écrits en --or-somb : ratio 3,35 et 3,18
sur le noir, sous le seuil de 4,5. Ils passent Ă --or-gras / --gris-cl.
· `#et-portal-pol` : des capitales espacĂ©es en Cormorant â la Charte les veut en Jost.
· l'axiome de la une est en italique et porte un đ± : l'Ă©moji penchait. Il est enveloppĂ© et redressĂ©.
· les émojis de la page qui ne le sont pas encore reçoivent .e (droits, jamais en italique).
Ce qui reste sous le seuil appartient au COMPOSANT V16 (le tiroir de la barre : .gly, .p-e, .sub, .m-n, a.m,
le â€ïžâđ„) â il est commun Ă tous les volumes : on ne le corrige pas ici, sinon 5.3 divergerait des autres.
Usage : python3 p5_et.py entree.html sortie.html
"""
import re, sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:52]} Ă{n}')
# ââ 1 · la largeur se rĂšgle sur le conteneur, pas sur la fenĂȘtre âââââââââââââââââââââââââââââââââââââââââ
rep('--wrap:min(1320px,94vw)', '--wrap:min(1320px,100%)', 1,
'la largeur suit le conteneur (94vw débordait de 3 px à 320)')
# ââ 2 · l'axiome de la une : l'Ă©moji ne penche plus ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('<p class="couv-axiome">« đ± Pour', '<p class="couv-axiome">« <span class="e">đ±</span> Pour', 1,
'le đ± de lâaxiome enveloppĂ© (il penchait avec lâitalique)')
# ââ 2 bis · les fils qui sortaient de la page ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
rep('href="3.3_CosmoLogie_La-Bible-2.0_aMi_EGO-SPIRITUEL.html"', 'href="https://agdistys.fr/ami-cosmologie"', 1,
'le renvoi vers CosmoLogie : fichier local (mort en ligne) â agdistys.fr/ami-cosmologie')
rep('href="https://agdistys.cms.webnode.fr/la-theorie-du-tout/"', 'href="https://agdistys.fr/la-theorie-du-tout"', 1,
'cms.webnode (lâadresse de dĂ©veloppement) â agdistys.fr')
# un <footer> au milieu d'un chapitre n'est pas le pied de la page : c'est une note
rep("<footer>Agdistys · Diane Serant â Protocole de ConScience aMi · CC BY-SA 4.0<br>Associations documentĂ©es, non diagnostiques. Claude n'est pas mĂ©decin â Ă valider avec un·e spĂ©cialiste.</footer>",
'<p class="note ami-note-chap">Associations documentĂ©es, non diagnostiques â Ă valider avec un·e spĂ©cialiste. '
'<span class="ami-note-src">Agdistys · Diane Serant · CC BY-SA 4.0</span></p>', 1,
'le pied Ă©garĂ© au milieu dâun chapitre devient la note quâil Ă©tait')
# ââ 3 · les encres de la page ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ENCRES = """
/* ââ les deux variables que le composant lit et que la Charte dĂ©clare dans SA racine (ici elles manquaient) ââ */
:root{--ame-cl:var(--tanzanite-ct,#9d8bff);--vigilance:var(--cornaline,#ff7800)}
/* ââ LES ENCRES · mesurĂ©es Ă 320/390/768/1280, relevĂ©es au-dessus de 4,5 (P5, 20/09) ââ */
#ami-espacetemps .grp .grp-pol,#ami-espacetemps .grp:not(.active) .grp-pol{color:var(--or-gras)!important} /* 3,35 â 7,2 */
#ami-espacetemps .ami-conclusion .ac-kick{color:var(--gris-cl)} /* 3,18 â 8,4 sur lâencre */
#et-portal-pol,#ami-espacetemps #et-portal-pol{font-family:'Jost',system-ui,sans-serif!important} /* capitales espacées : Jost, pas Cormorant */
#ami-espacetemps .couv-axiome .e{font-style:normal;display:inline}
#ami-espacetemps .ami-note-chap{font-family:'Jost',system-ui,sans-serif;font-size:.92rem;color:var(--gris-cl);border-left:2px solid var(--or-somb);padding:.5rem .9rem;margin:1.4rem 0}
#ami-espacetemps .ami-note-src{display:block;color:var(--gris);font-size:.82rem;margin-top:.3rem}
#ami-espacetemps .e,#ami-espacetemps .sb-em,#ami-espacetemps .grp-em,#ami-espacetemps .couv-glyphes{
font-family:'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif;font-style:normal!important;display:inline-block}
"""
i = h.find('<style id="ami-doigt">')
assert i > 0
h = h[:i] + f'<style id="ami-encres">{ENCRES}</style>\n' + h[i:]
J.append('feuille « ami-encres » : grp-pol, ac-kick, portal-pol, émojis droits')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h) // 1024} Ko')
p6_et_mutations.py python3 p6_et_mutations.py et_p5.html379 lignes
5.3 Astro : 146 mutations Ă 390 et 1280 (+320 pour le dĂ©bordement). IdentitĂ© et chemin, coque V16, les six instruments (0 cible sous 24 px, 0 champ sous 16, 0 dĂ©bordement, ils CALCULENT : roue de 53 Ă©lĂ©ments + aspects, constellation, carte des apports), les renvois, â° đ â, la pile qui se replie, đ§ sans saut dans les deux sens (tĂ©moin marquĂ©, pas comparĂ© par son texte), â â¶ zoom, plancher, 0 erreur JS. 20/09 aprĂšs-midi : les quatre champs de ville, la traduction en clair, un seul sommaire, la mĂ©moire de l'onglet, la ligne collĂ©e opaque.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P6 · 5.3 Astro · Espace-Temps â LA MUTATION DES CONTRĂLES : on presse chaque commande et on regarde si la page
a changé comme promis. Identité, coque V16, les 4 portails à 17 volets, LES SIX INSTRUMENTS DANS LA PAGE (ils
calculent vraiment), le doigt (44 px, champs â„ 16 px), les renvois, đ§ sans saut, â â¶ zoom, â â° đ, #prog,
la pile qui se replie, plein Ă©cran, plancher, dĂ©bordement, 0 var sans dĂ©claration, 0 erreur JS â Ă 390 et 1280.
Usage : python3 p6_et_mutations.py et_p8.html"""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'et_p8.html'
R = []
def ok(nom, cond, det=''):
R.append((nom, bool(cond), det)); print(('â' if cond else 'â'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1800)
print(f'=== {W} px ==='); E = lambda js, *a: pg.evaluate(js, *a)
# ââ identitĂ© et chemin ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('métas ami:volume 5.3 · version V21 · date 2026-09-20',
E("()=>['5.3','V21','2026-09-20'].every((v,i)=>document.querySelector('meta[name=\"ami:'+['volume','version','date'][i]+'\"]').content===v)"))
ok('titre : 5.3 Astro · Espace-Temps â Tu Sais Ta Juste Place',
E("()=>document.title.startsWith('5.3 Astro · Espace-Temps â Tu Sais Ta Juste Place')"))
ok('un seul <title>', E("()=>document.querySelectorAll('title').length===1"))
ok('ASTRO en h1, Espace-Temps en sous-titre (arbitrage du 20/09)',
E("()=>document.querySelectorAll('h1').length===1 && document.querySelector('h1.couv-h1').textContent.trim()==='ASTRO' && document.querySelector('.couv-sous').textContent.trim()==='Espace-Temps'"))
ok('les autres titres de premier rang sont devenus des h2.ami-h1',
E("()=>document.querySelectorAll('h2.ami-h1').length>=8"))
ok('la une : Volume 5.3, sceau đ«, la Question',
E("()=>/Volume 5\\.3/.test(document.querySelector('.vol-label').textContent) && document.querySelector('.couv-glyphes').textContent.includes('đ«') && /Comment Trouver Ma Place/.test(document.querySelector('.couv-question').textContent)"))
ok('la conclusion porte la RĂ©ponse « đ« Tu Sais Ta Juste Place »',
E("()=>{const c=document.querySelector('.ami-conclusion');return !!c && /Tu Sais Ta Juste Place/.test(c.querySelector('.ami-h1').textContent) && /Ătre au Bon Moment/.test(c.querySelector('.ac-sceau').textContent)}"))
ok('la signature garde ses majuscules internes (ConScience UniVerCelle)',
E("()=>{const s=document.querySelector('.couv-inner .sig');return getComputedStyle(s).textTransform==='none' && /ConScience UniVerCelle/.test(s.textContent)}"))
ok('aucune var(--x) sans déclaration',
E("()=>{const used=new Set(),decl=new Set();for(const s of document.querySelectorAll('style')){for(const m of s.textContent.matchAll(/var\\((--[a-z0-9-]+)/g)) used.add(m[1]);for(const m of s.textContent.matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}for(const el of document.querySelectorAll('[style]')){for(const m of (el.getAttribute('style')||'').matchAll(/(--[a-z0-9-]+)\\s*:/g)) decl.add(m[1]);}return [...used].filter(v=>!decl.has(v)&&!['--cc','--c','--lc','--fx','--fy','--ami-pb'].includes(v)).length===0;}"))
# ââ la coque V16 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('barre V16 en tĂȘte, tiroir fermĂ©', E("()=>!!document.querySelector('.ami-barre') && !document.querySelector('.ami-barre .tiroir.on')"))
ok('#prog présent', E("()=>!!document.getElementById('prog')"))
ok('bande des registres juste aprĂšs la une',
E("()=>{const b=document.querySelector('.bande-registres');return !!b && b.previousElementSibling.classList.contains('couverture')}"))
ok('pied V16 aprĂšs la racine : sceau đ«, volume 5.3, la devise de la une',
E("()=>{const f=document.querySelector('.ami-pied');return !!f && f.previousElementSibling.id==='ami-espacetemps' && f.querySelector('.ap-sceau').textContent.trim()==='đ«' && f.textContent.includes('5.3') && /Leçons dâHier/.test(f.textContent)}"))
ok('le pied maison a disparu', E("()=>document.querySelectorAll('footer.et-footer').length===0"))
ok('â Emporter dans la racine, export nommĂ© 5.3_ASTRO',
E("()=>{const e=document.querySelector('#ami-espacetemps .emporter');return !!e && !!document.getElementById('dlHtml') && [...document.scripts].some(s=>s.textContent.includes('5.3_ASTRO_Espace-Temps_aMi_'))}"))
ok('aucun lien vers un fichier .html local',
E("()=>[...document.querySelectorAll('a[href]')].every(a=>!/^[^:#]*\\.html/.test(a.getAttribute('href')))"))
ok('aucun lien cms.webnode', E("()=>!document.body.innerHTML.includes('cms.webnode')"))
# ââ les six instruments sont DANS la page âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('plus aucun <iframe>, plus aucun blob:', E("()=>document.querySelectorAll('iframe').length===0 && !document.body.innerHTML.includes('blob:')"))
ok('les six instruments sont des blocs de la page',
E("()=>['ipseite','theme','complement','synastrie','citoyennete','village'].every(k=>{const e=document.getElementById('ami-et-'+k);return e && e.classList.contains('ami-et-outil')})"))
ok('Astronomy Engine posé une seule fois',
E("()=>document.querySelectorAll('#ami-astronomy-engine').length===1 && typeof Astronomy==='object'"))
ok('les instruments portent la palette de la page (lâor de la Charte)',
E("()=>['theme','village','citoyennete'].every(k=>getComputedStyle(document.getElementById('ami-et-'+k)).getPropertyValue('--gold').trim().includes('var(--or)')||getComputedStyle(document.getElementById('ami-et-'+k)).getPropertyValue('--gold').trim()!=='')"))
# ââ la navigation : 4 portails Ă 17 volets ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('4 portails, 17 volets, un seul volet ouvert',
E("()=>document.querySelectorAll('.grp').length===4 && document.querySelectorAll('.subbar button').length===17 && document.querySelectorAll('.et-pane.active').length===1"))
ok('les portails et les volets se touchent (â„ 44 px) et ne se chevauchent pas',
E("()=>{const bs=[...document.querySelectorAll('.grp,.subbar.active button')];if(!bs.every(b=>b.getBoundingClientRect().height>=44))return false;for(let i=0;i<bs.length;i++)for(let j=i+1;j<bs.length;j++){const a=bs[i].getBoundingClientRect(),c=bs[j].getBoundingClientRect();if(a.left<c.right-1&&c.left<a.right-1&&a.top<c.bottom-1&&c.top<a.bottom-1)return false}return true}"))
E("()=>document.querySelector('.grp[data-main=resonances]').click()"); pg.wait_for_timeout(400)
ok('âĄïž Trouver ses RĂ©sonances ouvre « đ« Mon ThĂšme Astro aMi »',
E("()=>window.currentMain==='resonances' && window.currentSub==='theme' && document.querySelector('.et-pane.active').id==='pane-theme'"))
# ââ le doigt (la raison de tout ceci) âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
for k in ['theme', 'complement', 'synastrie', 'citoyennete', 'village', 'ipseite']:
E("(k)=>{const b=document.querySelector('.subbar button[data-sub=\"'+k+'\"]');b&&b.click();}", k); pg.wait_for_timeout(350)
d = E("""(k)=>{const R=document.getElementById('ami-et-'+k);R.querySelectorAll('details').forEach(x=>x.open=true);
const sel='a[href],button,input,select,textarea,[role=button],summary';let petites=0,champs=0,deb=0,n=0;
/* le <select> d'origine, réduit à un point pour que les moteurs le lisent encore, n'est pas une cible */
const w=document.documentElement.clientWidth;
R.querySelectorAll(sel).forEach(e=>{const r=e.getBoundingClientRect(),cs=getComputedStyle(e);
if(cs.display==='none'||cs.visibility==='hidden'||(!r.width&&!r.height))return;
if(e.classList.contains('ami-ville-cache'))return;n++;
if(r.height<24)petites++;
if(/^(INPUT|SELECT|TEXTAREA)$/.test(e.tagName)&&parseFloat(cs.fontSize)<16)champs++;});
R.querySelectorAll('*').forEach(e=>{const r=e.getBoundingClientRect();if(r.width<1)return;
let a=e.parentElement,ro=false;while(a&&a!==R.parentElement){const o=getComputedStyle(a).overflowX;if(o==='auto'||o==='scroll'||o==='hidden'){ro=true;break}a=a.parentElement}
if(!ro&&(r.right>w+1||r.left<-1))deb++;});
return {n,petites,champs,deb,h:Math.round(R.getBoundingClientRect().height)}}""", k)
ok(f'{k} : {d["n"]} commandes · {d["petites"]} sous 24 px · {d["champs"]} champ(s) sous 16 px · '
f'{d["deb"]} débordement · {d["h"]} px de haut',
d['petites'] == 0 and d['champs'] == 0 and d['deb'] == 0 and d['h'] > 800)
# ââ les instruments calculent âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
E("()=>{const b=document.querySelector('.subbar button[data-sub=theme]');b&&b.click();}"); pg.wait_for_timeout(400)
pg.click('#tv-theme-q'); pg.fill('#tv-theme-q','lyon'); pg.wait_for_timeout(600)
E("()=>{const b=document.querySelector('#tv-theme-l button');b&&b.click()}"); pg.wait_for_timeout(350)
E("()=>document.getElementById('tm-passeport').click()"); pg.wait_for_timeout(300)
ok('le Passeport refuse poliment tant quâil nây a pas de chiffres',
E("()=>/Calcule dâabord/.test(document.getElementById('tm-passeport-etat').textContent)"))
E("""()=>{const R=document.getElementById('ami-et-theme');
R.querySelector('#tm-birthDate').value='1985-06-15'; R.querySelector('#tm-birthTime').value='14:30';}""")
pg.wait_for_timeout(300); E("()=>document.querySelector('#ami-et-theme #tm-calculate').click()"); pg.wait_for_timeout(2400)
ok('la SynthĂšse est doublĂ©e dâune traduction « En clair » (dix blocs)',
E("()=>{const rd=document.querySelector('#ami-et-theme #reading');return !!rd && rd.querySelectorAll('.ami-clair').length===10 && /En clair/.test(rd.innerText)}"))
ok('« Positions & Aspects » porte sa phrase en clair',
E("()=>/En clair/.test(document.querySelector('#ami-et-theme .astro-table,#ami-et-theme .panel')?.parentElement?.innerText||document.getElementById('ami-et-theme').innerText)"))
# ââ la traduction complĂšte (sujet + verbe + complĂ©ment) et le lexique ââââââââââââââââââââââââââââ
ok('les phrases « En clair » nomment le SUJET : chaque planÚte est glosée',
E("()=>{const t=document.querySelector('#ami-et-theme #reading').innerText;"
"return /ta façon dâagir|ta façon de comprendre|ce qui te tient|ce qui te libĂšre|ce qui te transforme|ce qui tâagrandit|ce Ă quoi tu tiens|ce qui te relie sans mots|ta direction|ce qui te rassure/.test(t)}"))
ok('« rĂ©trograde » est traduit PLANĂTE PAR PLANĂTE, avec sa frĂ©quence mesurĂ©e',
E("()=>{const t=document.querySelector('#ami-et-theme #reading').innerText;"
"return !/Mouvements rĂ©trogrades apparents : aucun/.test(t) ? /% du temps/.test(t) && /: (le sens se cherche|le cadre se renĂ©gocie|la rupture se prĂ©pare|ce quâon projetait|la transformation travaille|on redit|on revoit|lâĂ©lan rentre)/.test(t) : true}"))
ok('la tension nomme les deux fonctions et ce que lâangle leur fait',
E("()=>{const t=document.querySelector('#ami-et-theme #reading').innerText;"
"return /se contrarient|se font face|aucune force majeure ne tire/.test(t)}"))
ok('le lexique est là : 10 fonctions, 5 angles, 8 rétrogrades, 4 matiÚres, 3 allures',
E("()=>{const d=document.getElementById('tm-lexique');return !!d && d.querySelectorAll('dt').length===32 && d.querySelectorAll('.lex-t').length===4}"))
ok('le rappel de registre est au voisinage des associations (BioMatrice, 13 Champs, Mélothésie)',
E("()=>['biomatrice','champs','melothesie'].every(s=>{const p=document.getElementById('pane-'+s);return p && p.querySelector('.ami-registre')})"))
# ââ le Passeport de Ciel ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('le Passeport de Ciel porte 13 Champs, 10 positions, ses aspects et SES LIMITES',
E("()=>{const C=window.__AMI_CIEL__;return !!C && C.format==='ami-ciel/1' && C.champs.length===13 && C.positions.length===10"
" && C.aspects.length>0 && /pas des conséquences déduites/.test(C.registres)"
" && C.lecture.rapprocher_par==='le nom du Champ' && C.lecture.jamais_par==='la coordonnée'"
" && /objets distincts/.test(C.lecture.note)}"))
ok('ThĂšme : la roue se dessine et les aspects se calculent',
E("()=>{const R=document.getElementById('ami-et-theme');return R.querySelectorAll('svg *').length>30 && /orbe/.test((R.querySelector('#aspects')||{}).innerText||'')}"))
E("()=>{const b=document.querySelector('.subbar button[data-sub=ipseite]');b&&b.click();}"); pg.wait_for_timeout(700)
ok('IpséïtĂ© : la constellation du moment sâaffiche',
E("()=>{const e=document.querySelector('#ami-et-ipseite #w-constellation');return !!e && e.innerText.trim().length>2}"))
E("()=>{const b=document.querySelector('.subbar button[data-sub=village]');b&&b.click();}"); pg.wait_for_timeout(500)
E("""()=>{const R=document.getElementById('ami-et-village');
R.querySelectorAll('input[type=checkbox]').forEach((c,i)=>{if(i<4)c.checked=true});
const btn=R.querySelector('#compare,#vi-compare'); btn&&btn.click();}"""); pg.wait_for_timeout(900)
ok('Village : la carte des apports se calcule',
E("()=>{const o=document.querySelector('#ami-et-village #output');return !!o && o.innerText.trim().length>40}"))
# ââ les renvois (les trois sections que le ThĂšme doublait) ââââââââââââââââââââââââââââââââââââââââ
ok('le ThĂšme ne porte plus sa copie de lâorigine des signes, des calendriers, du lexique',
E("()=>!document.getElementById('tm-origine-des-signes') && !document.getElementById('tm-calendriers-du-monde') && !document.getElementById('tm-glossaire-astrologique')"))
# ââ LES RETOURS DU 20/09 APRĂS-MIDI ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('un seul sommaire : le â de la page est parti', E("()=>!document.getElementById('et-toc-btn') && !document.getElementById('et-toc-panel')"))
ok('plus aucun renvoi â dans la page', E("()=>document.querySelectorAll('.ami-renvoi').length===0"))
ok('les trois sections que le ThĂšme doublait ne sont plus lĂ ',
E("()=>!document.getElementById('tm-origine-des-signes') && !document.getElementById('tm-calendriers-du-monde') && !document.getElementById('tm-glossaire-astrologique')"))
ok('« une fĂ©dĂ©ration de pratiques plutĂŽt quâun modĂšle unique » est rendue Ă CitoyennetĂ©',
E("()=>/fédération de pratiques/.test(document.getElementById('ami-et-citoyennete').innerText)"))
# les quatre champs oĂč la ville s'Ă©crit
ok('quatre champs de ville, un par lieu demandé',
E("()=>document.querySelectorAll('#ami-espacetemps .ami-ville[data-sel]').length===4"))
ok('les <select> dâorigine sont lĂ , rĂ©duits Ă un point (les moteurs les lisent toujours)',
E("()=>['birthCity','city','aCity','bCity'].every(i=>{const s=document.getElementById(i);return s && s.offsetHeight<=2})"))
for sub, q, mot, attendu in [('theme','tv-theme','lyon','Europe/Paris'), ('complement','tv-compl','dakar','Africa/Dakar'),
('synastrie','tv-syn-a','montreal','America/'), ('synastrie','tv-syn-b','tokyo','Asia/Tokyo')]:
E("(k)=>{const b=document.querySelector('.subbar button[data-sub=\"'+k+'\"]');b&&b.click()}", sub); pg.wait_for_timeout(450)
pg.click(f'#{q}-q'); pg.fill(f'#{q}-q', mot); pg.wait_for_timeout(600)
n = E("(i)=>document.querySelectorAll('#'+i+'-l button').length", q)
E("(i)=>{const b=document.querySelector('#'+i+'-l button');b&&b.click()}", q); pg.wait_for_timeout(350)
txt = E("(i)=>document.getElementById(i+'-ok').textContent", q)
ok(f'{q} : « {mot} » trouve {n} ville(s) et pose son fuseau â {txt[:60]}', n > 0 and attendu in txt)
ok('la base des villes : 33 961 entrées, dépliée une seule fois',
E("()=>{const e=document.getElementById('ami-villes-base');return !!e && e.textContent.trim().split('\\n§\\n')[2].split('\\n').length===33961}"))
# la mémoire de l'onglet
E("()=>{const b=document.querySelector('.subbar button[data-sub=inconcevable]')||document.querySelector('.grp[data-main=evolution]');b&&b.click()}"); pg.wait_for_timeout(700)
E("()=>window.scrollTo(0,4200)"); pg.wait_for_timeout(400)
ya = E("()=>Math.round(window.pageYOffset)")
E("()=>{const b=document.querySelector('.subbar button[data-sub=melothesie]')||document.querySelector('.grp[data-main=temps]');b&&b.click()}"); pg.wait_for_timeout(800)
yb = E("()=>Math.round(window.pageYOffset)")
ok(f'un volet neuf sâouvre EN HAUT, pas « bien en bas » ({ya} â {yb})', yb < ya - 500)
E("()=>window.scrollBy(0,1800)"); pg.wait_for_timeout(400)
yc = E("()=>Math.round(window.pageYOffset)")
E("()=>{const b=document.querySelector('.subbar button[data-sub=inconcevable]')||document.querySelector('.grp[data-main=evolution]');b&&b.click()}"); pg.wait_for_timeout(800)
yd = E("()=>Math.round(window.pageYOffset)")
ok(f'on revient dans un volet lĂ oĂč on lâavait laissĂ© ({ya} â {yd})', abs(yd - ya) <= 40)
E("()=>{const b=document.querySelector('.subbar button[data-sub=melothesie]');b&&b.click()}"); pg.wait_for_timeout(800)
ye = E("()=>Math.round(window.pageYOffset)")
ok(f'et dans lâautre aussi ({yc} â {ye}) â on peut comparer deux volets sans se reperdre', abs(ye - yc) <= 40)
# ââ la boĂźte Ă outils âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
E("()=>document.getElementById('ami-toc-toggle').click()"); pg.wait_for_timeout(300)
ok('â° : la carte (4 portails, 17 volets) ET les titres du volet ouvert â un seul sommaire',
E("()=>{const p=document.getElementById('ami-toc-panel');return !p.hidden && p.querySelectorAll('.toc-o').length===4 && p.querySelectorAll('.toc-s a[data-t]').length===17 && p.querySelectorAll('.toc-ici a[data-h]').length>0}"))
E("()=>{const a=[...document.querySelectorAll('#ami-toc-panel .toc-s a')].find(x=>x.dataset.t==='synastrie');a&&a.click()}"); pg.wait_for_timeout(900)
ok('une entrée de la carte ouvre son volet', E("()=>window.currentSub==='synastrie'"))
E("()=>document.getElementById('amiRech').click()"); pg.wait_for_timeout(250)
pg.fill('#rc-q', 'mélothésie'); pg.wait_for_timeout(600)
ok('đ trouve « mĂ©lothĂ©sie » et propose des passages', E("()=>document.querySelectorAll('#rc-l .rc-r').length>0"))
E("()=>document.querySelector('#rc-l .rc-r').click()"); pg.wait_for_timeout(1000)
ok('un résultat ouvre son volet et descend dessus', E("()=>window.pageYOffset>300 && !document.getElementById('ami-rech').classList.contains('on')"))
ok('đ indexe aussi les instruments (ils sont dans la page)',
(E("()=>{document.getElementById('amiRech').click();return 1}") and (pg.fill('#rc-q', 'synastrie') or True) and pg.wait_for_timeout(600) is None and E("()=>document.querySelectorAll('#rc-l .rc-r').length>0")))
E("()=>document.getElementById('rc-x').click()"); pg.wait_for_timeout(200)
E("()=>window.scrollTo(0,0)"); pg.wait_for_timeout(300)
ok('â paraĂźt quand on descend', E("()=>{window.scrollTo(0,1200);return new Promise(r=>setTimeout(()=>r(!document.getElementById('amiHaut').hidden),400))}"))
# ââ la pile qui se replie âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
hp = E("()=>document.getElementById('amiPile').offsetHeight")
E("()=>window.scrollTo(0,4000)"); pg.wait_for_timeout(400); E("()=>window.scrollBy(0,90)"); pg.wait_for_timeout(500)
ok(f'la pile ({hp} px) se replie en une ligne qui dit oĂč on est',
E("()=>{const m=document.getElementById('ami-pile-mini');return document.body.classList.contains('ami-repli') && m.offsetHeight<=48 && m.innerText.includes('âș')}"))
E("()=>window.scrollBy(0,-220)"); pg.wait_for_timeout(500)
ok('remonter la redéploie', E("()=>!document.body.classList.contains('ami-repli')"))
E("()=>window.scrollTo(0,3000)"); pg.wait_for_timeout(600)
ok('la ligne « oĂč je suis » est OPAQUE (le texte de la page ne transparaĂźt pas derriĂšre)',
E("()=>{const c=getComputedStyle(document.getElementById('ami-pile-mini')).backgroundColor;return /^rgb\\(/.test(c)&&!/rgba/.test(c)}"))
# ââ đ§ la lecture continue, sans saut, dans les deux sens âââââââââââââââââââââââââââââââââââââââââ
# on MARQUE le point sous les yeux (un attribut, pas une comparaison de texte) et on le remesure
E("()=>{const b=document.querySelector('.subbar button[data-sub=biomatrice]')||document.querySelector('.subbar button');b&&b.click();}"); pg.wait_for_timeout(1200)
# on CHOISIT le témoin (un paragraphe au milieu du volet), on l'amÚne sous les yeux, puis on note sa hauteur
av = E("""()=>{document.querySelectorAll('[data-ami-temoin]').forEach(e=>e.removeAttribute('data-ami-temoin'));
const pane=document.querySelector('#ami-espacetemps .et-pane.active'); if(!pane) return null;
const ps=[...pane.querySelectorAll('p,li,h3,blockquote')].filter(x=>x.textContent.trim().length>40 && x.getBoundingClientRect().height>10);
if(!ps.length) return null; const e=ps[Math.min(12,ps.length-1)]; e.setAttribute('data-ami-temoin','1');
window.scrollTo({top:Math.max(0,e.getBoundingClientRect().top+window.pageYOffset-420),behavior:'instant'}); return true}""")
pg.wait_for_timeout(700)
av = E("()=>{const e=document.querySelector('[data-ami-temoin]');return e?{top:Math.round(e.getBoundingClientRect().top),t:e.textContent.replace(/\\s+/g,' ').trim().slice(0,36),y:window.pageYOffset}:null}")
ok('un tĂ©moin a pu ĂȘtre posĂ© sous les yeux', av is not None, str(av))
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(1000)
ap = E("()=>{const e=document.querySelector('[data-ami-temoin]');return e?Math.round(e.getBoundingClientRect().top):null}")
ok(f'đ§ ON : le point sous les yeux ne bouge pas ({av["top"]} â {ap}) « {av["t"]} »', ap is not None and abs(ap - av['top']) <= 6)
ok('đ§ ON : les 17 volets sont dĂ©roulĂ©s', E("()=>document.querySelectorAll('.et-pane.active').length===17"))
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(1000)
ap2 = E("()=>{const e=document.querySelector('[data-ami-temoin]');return e?Math.round(e.getBoundingClientRect().top):null}")
ok(f'đ§ OFF : il ne bouge pas non plus ({ap} â {ap2})', ap2 is not None and abs(ap2 - av['top']) <= 6)
ok('đ§ OFF : un seul volet ouvert', E("()=>document.querySelectorAll('.et-pane.active').length===1"))
# ââ â le contraste, â¶ le plein Ă©cran, le zoom (qui ne vit qu'en plein Ă©cran) ââââââââââââââââââââââ
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(400)
ok('â le contraste monte les encres', E("()=>document.body.classList.contains('ami-contraste')"))
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(800)
ok('ⶠle plein écran : la barre entre dans la racine et la page défile',
E("()=>{const R=document.getElementById('ami-espacetemps');return (R.classList.contains('ami-plein')||R.classList.contains('ami-faux-fs')) && R.contains(document.getElementById('amiBarre')) && ['auto','scroll'].includes(getComputedStyle(R).overflowY)}"))
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(500)
ok('le zoom + agrandit la page (il ne vit quâen plein Ă©cran)',
E("()=>{const z=getComputedStyle(document.getElementById('ami-espacetemps')).zoom;return parseFloat(z)>1}"))
ok('le zoom ne fait pas déborder la page', E("()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth+1"))
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(400)
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(900)
ok('en sortant, la barre revient Ă sa place', E("()=>!document.getElementById('ami-espacetemps').contains(document.getElementById('amiBarre'))"))
# ââ LES SIX RETOURS DU SOIR (Diane, 20/09) âââââââââââââââââââââââââââââââââââââââââââââââââââââââ
pg.reload(); pg.wait_for_timeout(1500)
ok('la barre arrive REPLIĂE (elle ne mange plus la une)',
E("()=>document.getElementById('amiBarre').classList.contains('replie')"))
E("()=>window.scrollTo(0,900)"); pg.wait_for_timeout(500)
ok('elle ne se déplie pas toute seule quand on descend',
E("()=>document.getElementById('amiBarre').classList.contains('replie')"))
E("()=>window.scrollTo(0,0)"); pg.wait_for_timeout(500)
ok('elle ne se déplie pas non plus quand on revient en haut',
E("()=>document.getElementById('amiBarre').classList.contains('replie')"))
E("()=>document.getElementById('amiCoeur').click()"); pg.wait_for_timeout(500)
ok('le â€ïžâđ„ lâouvre â et câest le seul Ă le faire',
E("()=>!document.getElementById('amiBarre').classList.contains('replie')"))
E("()=>window.scrollTo(0,900)"); pg.wait_for_timeout(500)
ok('ouverte à la main, elle le reste au défilement (plus de saut)',
E("()=>!document.getElementById('amiBarre').classList.contains('replie')"))
E("()=>document.getElementById('amiCoeur').click()"); pg.wait_for_timeout(400)
# la hiérarchie : titre = majuscules ambre · sous-titre = gris opale
hie = E("""()=>{const b=document.querySelector('#ami-espacetemps .grp.active');
const l=getComputedStyle(b.querySelector('.grp-lbl')), p=getComputedStyle(b.querySelector('.grp-pol'));
return {lt:l.textTransform, ls:parseFloat(l.fontSize), lc:l.color,
pt:p.textTransform, ps:parseFloat(p.fontSize), pc:p.color}}""")
ok('onglet : le TITRE est en majuscules', hie['lt'] == 'uppercase', str(hie))
ok('onglet : le titre est plus gros que le sous-titre', hie['ls'] > hie['ps'], f"{hie['ls']} > {hie['ps']}")
ok('onglet : le titre porte lâambre, le sous-titre lâargent',
hie['lc'] != hie['pc'] and hie['pt'] == 'none', str(hie))
por = E("""()=>{const t=getComputedStyle(document.getElementById('et-portal-title')),
p=getComputedStyle(document.getElementById('et-portal-pol'));
return {tt:t.textTransform, ts:parseFloat(t.fontSize), tc:t.color,
pt:p.textTransform, ps:parseFloat(p.fontSize), pc:p.color}}""")
ok('partie : le titre est en majuscules ambre et plus gros que son sous-titre',
por['tt'] == 'uppercase' and por['ts'] > por['ps'] and por['pt'] == 'none', str(por))
# le bandeau se redéploie SUR PLACE
E("()=>window.scrollTo(0,4000)"); pg.wait_for_timeout(400); E("()=>window.scrollBy(0,120)"); pg.wait_for_timeout(600)
avant = E("()=>window.pageYOffset")
E("()=>document.getElementById('ami-pile-mini').click()"); pg.wait_for_timeout(900)
apres = E("()=>window.pageYOffset")
ok('le bandeau se redéploie SANS remonter la page',
abs(apres - avant) <= 2 and not E("()=>document.body.classList.contains('ami-repli')"),
f'{avant} â {apres}')
# l'Ăchelle des Ătats du Vivant
E("()=>{const b=document.querySelector('.subbar button[data-sub=echelle]');b&&b.click();}"); pg.wait_for_timeout(900)
ech = E("""()=>{const t=document.querySelector('#ami-espacetemps .ech-table');
const th=[...t.querySelectorAll('thead th')].map(x=>x.innerText.replace(/\s+/g,' ').trim());
const r1=t.querySelector('tbody tr'); const c=[...r1.children].map(x=>x.className);
const pics=[...t.querySelectorAll('.ech-pic')].map(x=>x.textContent);
const nw=t.querySelectorAll('.ech-nw');
const larg=t.querySelector('.ech-niv').getBoundingClientRect().width;
return {th:th, c:c, pics:pics, nw:nw.length,
bloc:nw.length?getComputedStyle(nw[0]).display:'', larg:Math.round(larg)}}""")
ok('lâĂchelle : đ¶ Niveau puis đĄïž Ătat de la MatiĂšre',
'đĄïž' in ech['th'][1] and 'MATIĂRE' in ech['th'][1].upper() and 'VIVANT' in ech['th'][2].upper(), str(ech['th'][:3]))
ok('lâĂchelle : les lignes suivent (Niveau · MatiĂšre · Vivant)',
ech['c'][0] == 'ech-niv' and ech['c'][1] == 'ech-mat' and ech['c'][2] == 'ech-etat', str(ech['c'][:3]))
ok('lâĂchelle : les 7 pictos dâĂ©tat sont lĂ ',
ech['pics'] == ['âš', 'đš', 'đ„đ§', 'đ«ïž', 'đ', 'đ§', 'â«ïž'], str(ech['pics']))
ok('lâĂchelle : les mots du Niveau reviennent Ă la ligne',
ech['bloc'] == 'block' and ech['nw'] >= 12, str(ech['nw']) + ' ' + ech['bloc'])
ok(f"lâĂchelle : la colonne Niveau tient en {ech['larg']} px (moins dâun quart de 390)",
ech['larg'] <= 110 if W == 390 else ech['larg'] <= 130)
ok('les Lois portent đ đïž đ± âš',
E("""()=>{const c=[...document.querySelectorAll('#ami-espacetemps .ech-loi-card')].map(x=>x.innerText);
return c.length===4 && c[0].includes('đ') && c[0].includes('đïž') && c[1].includes('đ±')
&& c[2].includes('đ') && c[3].includes('âš')}"""))
# le fil de navigation
ok('le fil est au pied des 17 chapitres',
E("()=>document.querySelectorAll('#ami-espacetemps nav.et-fil').length===17"))
fil = E("""()=>{const p=document.querySelector('#ami-espacetemps .et-pane.active');
const f=p.querySelector('nav.et-fil'); if(!f) return null;
const bs=[...f.querySelectorAll('.fil-b')].map(b=>Math.round(b.getBoundingClientRect().height));
return {ici:f.querySelector('.fil-ici').innerText.replace(/\s+/g,' ').trim(), h:bs}}""")
ok('il dit le rang : « Partie 1 · Chapitre 4 / 17 »',
'/ 17' in fil['ici'] and 'PARTIE' in fil['ici'].upper(), fil['ici'][:70])
ok('ses boutons se prennent au pouce (â„ 44 px)', all(x >= 44 for x in fil['h']), str(fil['h']))
ok('le « suivant » ouvre bien le chapitre suivant',
E("""()=>{const p=document.querySelector('#ami-espacetemps .et-pane.active');
const b=p.querySelector('.fil-suiv[data-fil]'); if(!b) return false; const cible=b.getAttribute('data-fil');
b.click(); return new Promise(r=>setTimeout(()=>r(window.currentSub===cible),500))}"""))
# ââ LES TROIS RETOURS DU 21/09 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ok('onglet : le titre fait au moins 15 px (le rang se voit)',
hie2 := E("()=>parseFloat(getComputedStyle(document.querySelector('#ami-espacetemps .grp.active .grp-lbl')).fontSize)>=15"))
ok('les sous-onglets remontent Ă 13 px au moins',
E("()=>parseFloat(getComputedStyle(document.querySelector('#ami-espacetemps .subbar.active button')).fontSize)>=13"))
E("()=>{const b=document.querySelector('.subbar button[data-sub=inconcevable]');b&&b.click();}"); pg.wait_for_timeout(700)
ordre = E("""()=>{const c8=document.querySelector('#regne-8 .rn-t b').textContent.trim(),
c9=document.querySelector('#regne-9 .rn-t b').textContent.trim();
const mx=[...document.querySelectorAll('#ami-espacetemps .mx-champ')].map(x=>x.innerText.replace(/\s+/g,' ').trim());
const m8=mx.find(t=>t.startsWith('8')), m9=mx.find(t=>t.startsWith('9'));
return {c8:c8,c9:c9,m8:m8,m9:m9}}""")
ok('la frise : RĂšgne 8 = Mammalien, RĂšgne 9 = Avial (cartes et Matrice)',
ordre['c8'] == 'Mammalien' and ordre['c9'] == 'Avial'
and 'Mammalien' in (ordre['m8'] or '') and 'Avial' in (ordre['m9'] or ''), str(ordre))
ok('la note de vérité est là , datée, avec les quatre points',
E("""()=>{const v=document.querySelector('#ami-espacetemps .rn-verite'); if(!v) return false;
const t=v.innerText; return /21 SEPTEMBRE 2026/i.test(t) && /225/.test(t) && /150/.test(t)
&& /ptérosaures/i.test(t) && /crocodile/i.test(t) && v.querySelectorAll('.rv-u li').length===4}"""))
ok('sous-onglets : lâĂ©moji est AU-DESSUS du nom',
E("""()=>{const b=document.querySelector('#ami-espacetemps .subbar.active button');
const e=b.querySelector('.sb-em').getBoundingClientRect(), r=document.createRange();
const t=[...b.childNodes].find(n=>n.nodeType===3&&n.textContent.trim()); r.selectNodeContents(t);
const tr=r.getBoundingClientRect(); return e.bottom<=tr.top+2}"""))
pile = E("()=>document.getElementById('amiPile').offsetHeight")
ok(f'la pile dâonglets tient en {pile} px (repliĂ©e Ă 44 dĂšs quâon descend)', pile < 420)
ok('onglet au repos : ambre lisible (4,5:1), plus lâor sombre',
E("()=>getComputedStyle(document.querySelector('#ami-espacetemps .grp:not(.active) .grp-lbl')).color==='rgb(230, 173, 45)'"))
# ââ la mesure d'ensemble ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
pg.reload(); pg.wait_for_timeout(1800) # page fraĂźche : on mesure la page, pas l'Ă©tat oĂč les essais l'ont laissĂ©e
bas = E("()=>{const R=document.getElementById('ami-espacetemps');const w=document.createTreeWalker(R,NodeFilter.SHOW_TEXT);let n;const out=[];while((n=w.nextNode())){if(n.textContent.trim().length<2)continue;const e=n.parentElement;if(!e||e.closest('svg,script,style'))continue;const cs=getComputedStyle(e);if(cs.display==='none'||cs.visibility==='hidden')continue;if(parseFloat(cs.fontSize)<11.5)out.push(e.tagName+'.'+(typeof e.className==='string'?e.className.split(' ')[0]:'')+'|'+parseFloat(cs.fontSize).toFixed(1));}return [...new Set(out)]}")
ok('aucun texte sous 11,5 px dans la racine', not bas, str(bas[:6]))
# et les six instruments ouverts, volets déroulés
bas2 = []
for k in ['ipseite','theme','complement','synastrie','citoyennete','village']:
E("(k)=>{const b=document.querySelector('.subbar button[data-sub=\"'+k+'\"]');b&&b.click();}", k); pg.wait_for_timeout(350)
bas2 += E("(k)=>{const R=document.getElementById('ami-et-'+k);R.querySelectorAll('details').forEach(d=>d.open=true);const w=document.createTreeWalker(R,NodeFilter.SHOW_TEXT);let n;const out=[];while((n=w.nextNode())){if(n.textContent.trim().length<2)continue;const e=n.parentElement;if(!e||e.closest('svg,script,style'))continue;const cs=getComputedStyle(e);if(cs.display==='none'||cs.visibility==='hidden')continue;if(parseFloat(cs.fontSize)<11.5)out.push(e.tagName+'|'+parseFloat(cs.fontSize).toFixed(1));}return [...new Set(out)]}", k)
ok('aucun texte sous 11,5 px dans les six instruments, volets ouverts', not bas2, str(bas2[:6]))
ok('la page ne déborde pas', E("()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth+1"))
pg.close()
# ââ 320 px : le dĂ©bordement seul
pg = b.new_page(viewport={'width': 320, 'height': 800}); pg.on('pageerror', lambda e: errs.append(str(e)[:160]))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(1500)
ok('320 px : la page ne déborde pas', pg.evaluate("()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth+1"))
pg.close(); b.close()
ok('aucune erreur JS', not errs, str(errs[:3]))
n = sum(1 for _, c, _ in R if c)
print(f'\nââ {n}/{len(R)} ââ')
for nom, c, det in R:
if not c: print(' â', nom, det)
mes_cibles.py python3 mes_cibles.py page.html [390]39 lignes
Mesure au doigt : dans chaque racine, qui est sous 44 px, quels champs feraient zoomer iOS (< 16 px), qui déborde. Sert avant/aprÚs toute reprise d'instruments pour mobile.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Mesure au doigt : dans chaque instrument, qui est sous 44 px, quels champs feraient zoomer iOS (<16 px), qui déborde."""
import sys, json, collections
from playwright.sync_api import sync_playwright
SRC = sys.argv[1]; W = int(sys.argv[2]) if len(sys.argv)>2 else 390
RACINES = ['ami-et-ipseite','ami-et-theme','ami-et-complement','ami-et-synastrie','ami-et-citoyennete','ami-et-village','ami-espacetemps']
JS = r"""
(rid)=>{ const R=document.getElementById(rid); if(!R) return null;
R.querySelectorAll('details').forEach(d=>d.open=true);
R.querySelectorAll('[hidden]').forEach(e=>e.hidden=false);
R.querySelectorAll('.tab-content,.tab-panel,.pane,.panel,.sec,.step').forEach(p=>p.classList.add('active','on','visible'));
const sel='a[href],button,input,select,textarea,label,[role=button],[onclick],summary,[data-sub],[data-tab],[tabindex]';
const petites=new Map(), champs=new Map(); let n=0, deb=[];
function sig(el){const c=(el.className&&typeof el.className==='string')?el.className.trim().split(/\s+/).slice(0,2).join('.'):'';
return el.tagName.toLowerCase()+(c?'.'+c:'')+(el.type?'[type='+el.type+']':'');}
R.querySelectorAll(sel).forEach(el=>{ const r=el.getBoundingClientRect(); const cs=getComputedStyle(el);
if(cs.display==='none'||cs.visibility==='hidden'||(!r.width&&!r.height)) return; n++;
const k=sig(el);
if(r.height<44||r.width<24){ const o=petites.get(k)||{k,n:0,h:99,w:99,t:''}; o.n++; o.h=Math.min(o.h,+r.height.toFixed(1)); o.w=Math.min(o.w,+r.width.toFixed(1)); o.t=o.t||(el.textContent||el.placeholder||'').trim().slice(0,22); petites.set(k,o);}
if(/^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)){ const fs=parseFloat(cs.fontSize); if(fs<16){const o=champs.get(k)||{k,n:0,fs:fs}; o.n++; champs.set(k,o);} }
});
const rr=R.getBoundingClientRect();
R.querySelectorAll('*').forEach(el=>{const r=el.getBoundingClientRect(); if(r.width&&r.right>rr.right+2) deb.push(sig(el)+' +'+Math.round(r.right-rr.right));});
return {n, petites:[...petites.values()].sort((a,b)=>b.n-a.n), champs:[...champs.values()], haut:Math.round(rr.height), deb:[...new Set(deb)].slice(0,12)}; }
"""
with sync_playwright() as p:
br=p.chromium.launch(); pg=br.new_context(viewport={'width':W,'height':844}).new_page()
errs=[]; pg.on('pageerror', lambda e: errs.append(str(e)[:90]))
pg.goto('file://'+__import__('os').path.abspath(SRC)); pg.wait_for_timeout(2500)
for rid in RACINES:
d = pg.evaluate(JS, rid)
if not d: print(rid,'â absent'); continue
print('â'*72); print(f'{rid} · {d["n"]} cibles · hauteur {d["haut"]} px')
for o in d['petites'][:14]: print(f' {o["n"]:4}à {o["k"]:<34} h={o["h"]} w={o["w"]} « {o["t"]} »')
for o in d['champs']: print(f' champ {o["n"]}Ă {o["k"]} font {o["fs"]} px (iOS zoome)')
if d['deb']: print(' déborde :', ', '.join(d['deb']))
br.close()
print('erreurs JS :', errs or 'aucune')
chaine_et.sh bash chaine_et.sh (et.html + les six astro_*.html à cÎté)23 lignes
Rejoue la chaĂźne de 5.3 Astro d'un trait : p1 â p2 â p3 â p4 â p19 â p2_plancher (390 et 1280) â p5 â p6 (116 mutations) â p15 'et'.
â§ Copier le script
#!/bin/sh
# La chaĂźne de 5.3 Astro · Espace-Temps â V14 â V17 (20 septembre 2026)
# (fait_villes.py fabrique villes_monde.txt ; p7 lit aussi clair_gloses.js, clair_synthese.js, ville.css, ville.js ;
# p8 lit ciel_build.js et passeport.js)
# Les six instruments entrent dans la page, la coque V16 se pose, et tout se mesure.
set -e
python3 p1_et.py et.html et_p1.html # les six cadres â six blocs ; une seule Astronomy Engine
python3 p2_et.py et_p1.html et_p2.html # ASTRO en h1 ; les doublons du ThÚme Îtés ; LE DOIGT (44 px, champs ℠16 px)
python3 p3_et.py et_p2.html et_p3.html # les treize pierres ; les instruments lisent la palette de la page
python3 p4_et.py et_p3.html et_p4.html # coque V16 : barre, bande, pile, conclusion, â Emporter, pied, â â° đ
python3 p19_plein_ecran_v16.py et_p4.html et_p19.html ami-espacetemps
python3 p2_plancher.py et_p19.html 390 ami-espacetemps # (écrit dans et_p19.html)
python3 p2_plancher.py et_p19.html 1280 ami-espacetemps
python3 p5_et.py et_p19.html et_p5.html # les encres, la largeur, les fils qui sortaient
python3 p7_et.py et_p5.html et_p7.html # (20/09 aprÚs-midi) la ville s'écrit, la lecture se traduit
python3 p8_et.py et_p7.html et_p8.html # (20/09 soir) le Passeport de Ciel, pour l'AstroGlobe
python3 p9_et.py et_p8.html et_p9.html # les six retours du soir (20/09)
python3 p10_et.py et_p9.html et_p10.html # hiérarchie, frise inversée, note de vérité (21/09)
python3 p11_et.py et_p10.html et_p11.html # l'émoji au-dessus dans les sous-onglets (21/09)
python3 p12_et.py et_p11.html et_p12.html # encres 4,5:1 (21/09)
python3 p6_et_mutations.py et_p12.html # 214 mutations
python3 p15_cadre_webnode.py et_p12.html et_webnode.html et
fait_villes.py python3 fait_villes.py villes_monde.txt (pip install geonamescache babel)33 lignes
Fabrique la base de villes embarquĂ©e : GeoNames â„ 15 000 habitants (33 961 entrĂ©es, CC BY 4.0), noms de pays en français (CLDR/babel). Encodage compact â fuseaux et pays listĂ©s une fois, noms triĂ©s et front-codĂ©s, nombres en base 36 : 777 Ko au lieu de 1,4 Mo.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Fabrique la base de villes embarquée dans 5.3 Astro (Diane, 20/09 : « il faudrait qu'on puisse l'écrire
car y'en a beaucoup trop⊠et ça dépend beaucoup de ça donc faut faire un truc plus précis »).
Source : GeoNames via geonamescache (CC BY 4.0) â toutes les villes de 15 000 habitants et plus, 33 961 entrĂ©es ;
noms de pays en français via babel (CLDR). Encodage compact : fuseaux et pays listés une fois, puis une ligne par
ville « <k><reste du nom>|lat|lon|pays|fuseau|milliers d'habitants », oĂč <k> est le nombre de lettres partagĂ©es
avec la ligne précédente (les noms sont triés) et les nombres sont en base 36. 785 Ko au lieu de 1,4 Mo.
Usage : python3 fait_villes.py [sortie.txt]"""
import sys, geonamescache
from babel import Locale
OUT = sys.argv[1] if len(sys.argv) > 1 else 'villes_monde.txt'
A = '0123456789abcdefghijklmnopqrstuvwxyz'
def b36(n):
n = int(n); neg = n < 0; n = abs(n); s = ''
while n: s = A[n % 36] + s; n //= 36
return ('-' if neg else '') + (s or '0')
g = geonamescache.GeonamesCache()
FR = Locale('fr').territories
villes = [v for v in g.get_cities().values() if v['population'] >= 15000]
tz = sorted({v['timezone'] for v in villes}); ti = {t: i for i, t in enumerate(tz)}
cc = sorted({v['countrycode'] for v in villes}); ci = {c: i for i, c in enumerate(cc)}
lig = sorted(villes, key=lambda v: (v['name'], -v['population']))
out = []; prec = ''
for v in lig:
nom = v['name']; k = 0
while k < len(prec) and k < len(nom) and prec[k] == nom[k] and k < 35: k += 1
out.append(f"{A[k]}{nom[k:]}|{b36(round(v['latitude']*100))}|{b36(round(v['longitude']*100))}"
f"|{b36(ci[v['countrycode']])}|{b36(ti[v['timezone']])}|{b36(v['population']//1000)}")
prec = nom
s = ('\n'.join(tz) + '\n§\n' + '\n'.join(FR.get(c, c) for c in cc) + '\n§\n' + '\n'.join(out))
open(OUT, 'w', encoding='utf-8').write(s)
print(f'â {OUT} · {len(villes)} villes · {len(tz)} fuseaux · {len(cc)} pays · {len(s.encode())//1024} Ko')
p7_et.py python3 p7_et.py et_p5.html et_p7.html (villes_monde.txt, ville.css, ville.js à cÎté)137 lignes
5.3 Astro, retours du 20/09 aprÚs-midi : la ville s'écrit dans les quatre instruments qui demandent un lieu (le <select> reste, réduit à un point, pour que les moteurs le lisent encore) ; les dix lignes de la SynthÚse symbolique doublées d'une traduction « En clair » (gloses des matiÚres et des allures) ; la phrase en clair sous Positions & Aspects ; « une fédération de pratiques plutÎt qu'un modÚle unique » rendue à Citoyenneté.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P7 · 5.3 Astro â LES RETOURS DE DIANE (20 septembre, aprĂšs-midi).
1. LA VILLE S'ĂCRIT. « Pour choisir la ville il faudrait qu'on puisse l'Ă©crire, car y'en a beaucoup trop et sinon
c'est pas pratique, et ça dĂ©pend beaucoup de ça donc faut faire un truc plus prĂ©cis. » â un champ de recherche
sur 33 961 villes du monde (GeoNames ℠15 000 habitants, CC BY 4.0), embarquées dans le fichier, hors ligne
comme le reste : on tape trois lettres, on choisit, la latitude, la longitude et le fuseau se posent tout seuls.
Le <select> d'origine reste dans la page, caché : tout le moteur du ThÚme continue de le lire, on ne touche
pas à ses calculs. « Autre lieu · coordonnées à la main » reste accessible d'un bouton.
2. LA LECTURE SE TRADUIT. « Ăa signifie rien pour les gens qui savent pas lire â comme moi ! Tu pourrais faire
en sorte qu'on ait la lecture symbolique ET la traduction, comme dans la Charte MathĂ©matique oĂč on a traduit
les formules en mots ? » â chaque ligne de la SynthĂšse symbolique rĂ©visable reçoit sa phrase « En clair ».
La lecture symbolique n'est pas remplacée : elle est doublée. Idem pour « Données détaillées · Positions & Aspects ».
3. LA PHRASE RENDUE à CITOYENNETà : « le meilleur systÚme national ressemblerait à une fédération de pratiques
plutĂŽt qu'Ă un modĂšle unique » â elle venait de l'ancienne Demi Page et n'avait pas Ă©tĂ© reprise.
Usage : python3 p7_et.py entree.html sortie.html (villes_monde.txt à cÎté)
"""
import re, sys, html as H
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
VILLES = open('villes_monde.txt', encoding='utf-8').read()
def rep(old, new, n=1, why=''):
global h
c = h.count(old); assert c == n, f'â {why or old[:60]!r} : {c} trouvĂ©(s), {n} attendu(s)'
h = h.replace(old, new); J.append(f'{why or old[:52]} Ă{n}')
# ââ 1 · LA VILLE S'ĂCRIT â dans les QUATRE instruments qui demandent un lieu ââââââââââââââââââââââââââââââ
CHAMPS = [('birthCity', 'customPlace', 'tv-theme', 'Ville de naissance'), # đ« Mon ThĂšme Astro
('city', 'custom', 'tv-compl', 'Lieu'), # âŻïž ComplĂ©mentaritĂ©
('aCity', 'aCustom', 'tv-syn-a', 'Lieu'), # đ Synastrie · premiĂšre personne
('bCity', 'bCustom', 'tv-syn-b', 'Lieu')] # đ Synastrie · seconde personne
AIDE = ('33 961 villes du monde sont dans ce fichier. Si ton village nây est pas, prends la ville la plus proche : '
'Ă 20 km, lâAscendant bouge dâun tiers de degrĂ©.')
for sel, cust, q, lib in CHAMPS:
i = h.find('<select id="' + sel + '"'); assert i > 0, sel
deb = h.rfind('<label', 0, i); assert deb > 0, sel
fin = h.find('</label>', h.find('</select>', i)) + len('</label>')
ancien = h[deb:fin]
assert ancien.count('<option') > 30 and 'custom' in ancien, sel
champ = (f'<div class="ami-ville" id="{q}" data-sel="{sel}" data-cust="{cust}">\n'
f'<label for="{q}-q">{lib}</label>\n'
f'<input id="{q}-q" type="search" role="combobox" aria-expanded="false" aria-controls="{q}-l" aria-autocomplete="list"\n'
f' autocomplete="off" spellcheck="false" placeholder="Ăcris les premiĂšres lettres â Lyon, Dakar, MontrĂ©alâŠ">\n'
f'<div id="{q}-l" class="ami-ville-l" role="listbox" aria-label="Villes proposées" hidden></div>\n'
f'<p class="ami-ville-ok" id="{q}-ok" aria-live="polite">{AIDE}</p>\n'
f'<button type="button" class="ami-ville-main" id="{q}-main">đ Entrer les coordonnĂ©es Ă la main</button>\n'
f'</div>\n'
f'<select id="{sel}" class="ami-ville-cache" aria-hidden="true" tabindex="-1">\n'
f'<option selected="" value="">â Choisir une ville â</option>\n'
f'<option value="" id="{q}-choisie">â</option>\n'
f'<option value="custom">Autre lieu · coordonnées personnalisées</option>\n'
f'</select>')
h = h[:deb] + champ + h[fin:]
J.append('la ville sâĂ©crit dans les quatre instruments qui demandent un lieu (ThĂšme, ComplĂ©mentaritĂ©, Synastrie Ă2)')
CSS_VILLE = open('ville.css', encoding='utf-8').read()
i = h.find('<style id="ami-doigt">'); assert i > 0
h = h[:i] + '<style id="ami-ville-css">' + CSS_VILLE + '</style>\n' + h[i:]
J.append('feuille de la ville qui sâĂ©crit et de la traduction')
BASE = '<script type="text/plain" id="ami-villes-base">\n' + VILLES.replace('</', '<\\/') + '\n</script>\n'
JS_VILLE = '<script id="ami-ville-js">\n' + open('ville.js', encoding='utf-8').read() + '\n</script>\n'
i = h.rfind('</body>'); assert i > 0
h = h[:i] + BASE + JS_VILLE + h[i:]
J.append(f'base des villes ({len(VILLES) // 1024} Ko) + les quatre champs qui cherchent')
# ââ 2 · LA LECTURE SE TRADUIT ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
GLOSE = '\n' + open('clair_gloses.js', encoding='utf-8').read()
rep(""" const fmt=new Intl.DateTimeFormat('fr-FR',{timeZone:tz,dateStyle:'long',timeStyle:'short'});
const reading=`""",
" const fmt=new Intl.DateTimeFormat('fr-FR',{timeZone:tz,dateStyle:'long',timeStyle:'short'});\n" + GLOSE +
" const reading=`", 1, 'les gloses (matiĂšre, allure) et la fonction clair()')
ANCIENNE = """ <h3>đȘ SynthĂšse symbolique rĂ©visable</h3>
<p><b>Référentiel symbolique principal :</b> ${referenceLabel}. La table conserve simultanément les positions tropicales, sidérales et constellationnelles.</p>
<p><b>Noyau sélectionné :</b> ${sun.glyph} ${sun.info.sign.name} cherche à ${sun.info.sign.verb} ; ${moon.glyph} ${moon.info.sign.name} décrit une maniÚre possible de rechercher la sécurité en ${moon.info.sign.verb}.</p>
<p><b>Double lecture :</b> Soleil tropical ${sun.tropical.sign.icon} ${sun.tropical.sign.name} · Soleil sidĂ©ral ${sun.sidereal.sign.icon} ${sun.sidereal.sign.name} ; Lune tropicale ${moon.tropical.sign.icon} ${moon.tropical.sign.name} · Lune sidĂ©rale ${moon.sidereal.sign.icon} ${moon.sidereal.sign.name}. AyanÄáčĆa utilisĂ© : ${ayanamsa.toFixed(2)}°.</p>
<p><b>Mouvements rétrogrades apparents :</b> ${retro.length?retro.join(' · '):'aucun parmi les planÚtes calculées'}.</p>
<p><b>Dominantes :</b> ${dominantElement} · ${dominantMode}. ${flow?`Ressource fluide : ${flow.a.name} ${flow.glyph} ${flow.b.name}.`:'Aucun aspect fluide majeur dans les orbes retenues.'}</p>
<p><b>Tension principale :</b> ${hard?`${hard.a.name} ${hard.glyph} ${hard.b.name} · orbe ${hard.delta.toFixed(1)}°`:'aucun carré ou opposition majeur dans les orbes retenues.'}</p>
<p><b>Biais prédominants possibles à vérifier :</b> ${[...new Set(biases)].join(' · ')}.</p>
<p><b>Trois Champs en résonance :</b> ${top.map(x=>x.name).join(' · ')}.</p>
<p><b>Question aMi centrale :</b> ${top[0].question}</p>
<p><b>Acte réversible :</b> ${top[0].act}</p>"""
NOUVELLE = open('clair_synthese.js', encoding='utf-8').read().rstrip('\n')
rep(ANCIENNE, NOUVELLE, 1, 'la SynthĂšse symbolique : dix lignes doublĂ©es dâune traduction « En clair »')
rep('<div class="panel-title"><div><div class="kicker">DonnĂ©es dĂ©taillĂ©es</div><h2>đȘ Positions & Aspects</h2></div><p>Les hypothĂšses restent reliĂ©es aux calculs visibles.</p></div>',
'<div class="panel-title"><div><div class="kicker">DonnĂ©es dĂ©taillĂ©es</div><h2>đȘ Positions & Aspects</h2></div>'
'<p>Les hypothÚses restent reliées aux calculs visibles.</p></div>\n'
'<div class="ami-clair"><p class="ac-fr"><b>En clair :</b> ce tableau est la matiĂšre premiĂšre â lâendroit du ciel oĂč Ă©tait chaque astre, '
'au degrĂ© prĂšs, Ă ta date et Ă ton lieu. Tout ce qui est dit plus haut en sort. Si un chiffre change, la lecture change : rien nâest ajoutĂ© en douce, '
'et tu peux tout refaire toi-mĂȘme.</p></div>', 1,
'« Positions & Aspects » : une phrase en clair sous le titre')
# ââ 2 ter · LE RAPPEL DE REGISTRE PRĂS DES ASSOCIATIONS (retour d'Isil, fiche du 20/09, point 12) âââââââââ
# « Le volume distingue déjà observation, convention et lecture symbolique : c'est une base à conserver. »
# Ce qui manquait : le rappel au VOISINAGE des associations (la BioMatrice, les 13 Champs, la Mélothésie),
# lĂ oĂč un corps et un ciel se croisent â pas seulement dans « đĄïž Statut de la Carte », en tĂȘte du volume.
REGISTRE = ('<p class="ami-registre">đĄïž <b>Registres.</b> Calcul astronomique, convention culturelle et lecture '
'symbolique aMi désignent des registres différents. Les correspondances neurochimiques proposées ici '
'<b>ne sont pas des consĂ©quences dĂ©duites des positions cĂ©lestes</b> â elles rapprochent deux langages, '
'elles nâen tirent pas une preuve.</p>\n')
n_reg = 0
for sub in ('biomatrice', 'champs', 'melothesie'):
i = h.find(f'id="pane-{sub}"')
assert i > 0, sub
i = h.find('>', i) + 1
h = h[:i] + REGISTRE + h[i:]
n_reg += 1
J.append(f'le rappel de registre posĂ© au voisinage des associations Ă{n_reg} (BioMatrice, 13 Champs, MĂ©lothĂ©sie) â retour dâIsil')
# ââ 3 · LA PHRASE RENDUE Ă CITOYENNETĂ âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
i = h.find('id="ct-le-meilleur-systeme"')
if i < 0:
m = re.search(r'<h[23][^>]*>\s*đŻ Le Meilleur SystĂšme\s*</h[23]>', h)
assert m, 'la section « Le Meilleur SystÚme »'
i = m.end()
else:
i = h.find('>', i) + 1
PHRASE = ('\n<p class="ami-federation"><b>La formule combinée :</b> le meilleur systÚme national ressemblerait à une '
'<strong>fĂ©dĂ©ration de pratiques</strong> plutĂŽt quâĂ un modĂšle unique â đïž local fort · đłïž proportionnelle · '
'đČ tirage au sort · đ dĂ©libĂ©ration · đ§Ÿ transparence · đ± droits du Vivant.</p>\n')
h = h[:i] + PHRASE + h[i:]
J.append('« une fĂ©dĂ©ration de pratiques plutĂŽt quâun modĂšle unique » rendue Ă CitoyennetĂ©')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J))
print(f'â {OUT} · {len(h) // 1024} Ko')
ville.css · ville.js lus par p7_et.py1 lignes
Le champ de ville, lisible Ă part : liste Ă 48 px, recherche sans accents, prĂ©fixes d'abord puis contenus, tri par population, clavier (â â EntrĂ©e Ăchap), pose de latitude/longitude/fuseau, repli « coordonnĂ©es Ă la main ». La base ne se dĂ©plie qu'au premier contact (44 ms mesurĂ©es).
â§ Copier le script
(script absent du dossier outils/)
p4_charte_pierres_collant.py python3 p4_charte_pierres_collant.py c9b.html c10.html91 lignes
Charte, retours du 26/09 soir : rend une ligne de pierre aux trois parties « pont » (rubis, moldavite, saphir â lues dans les variables dĂ©jĂ dĂ©clarĂ©es), et pose le pont de la navbar de l'hĂŽte : mesure de --top-site par elementsFromPoint, dĂ©calage de toutes les barres collĂ©es et du scroll-padding-top.
â§ Copier le script
# -*- coding: utf-8 -*-
"""P4 · LA CHARTE â LES PIERRES DES TROIS PONTS ET LE COLLANT SOUS LA NAVBAR DU SITE (26/09/2026).
1 · [stated] « il faut l'ajuster aux autres en mode pierre + couleur » : les trois parties « pont » avaient
perdu leur ligne quand on a ĂŽtĂ© « pont âžïž+đ · rouge 5° ». Elles reprennent la forme des cinq autres â
la pierre et sa couleur. La Charte les nomme dĂ©jĂ ailleurs : âžïžđ rouge = le rubis · đ đ vert = la
moldavite · âžïžđ bleu = le saphir. On ne les invente pas, on les remet.
2 · [stated] « j'ai ma navbarre webnode sticky, et la navbarre de la page passe en dessous, et la barre des
onglets aussi un peu ». Le pont du collant du 26/09 a rendu le COLLANT ; il ne savait pas encore qu'il y
avait quelque chose AU-DESSUS. Il mesure maintenant ce qui occupe le haut de la fenĂȘtre et dĂ©cale les
barres du bloc d'autant. (Rencontre n'a pas le problĂšme : elle vit dans un cadre, et son Pont calcule
dĂ©jĂ contre la fenĂȘtre visible.)
Usage : python3 p4_charte_pierres_collant.py entree.html sortie.html"""
import sys
SRC, OUT = sys.argv[1], sys.argv[2]
h = open(SRC, encoding='utf-8').read(); J = []
def un(motif, neuf, quoi, compte=1):
global h
n = h.count(motif)
assert n == compte, f'{quoi} : {n} occurrence(s), {compte} attendue(s)'
h = h.replace(motif, neuf); J.append(quoi)
# ââ 1 · les trois pierres ââââââââââââââââââââââââââââââââââââââââââââââââââ
PIERRES = [('pho', 'le rubis · rouge'), ('alg', 'la moldavite · vert'), ('cod', 'le saphir · bleu')]
for ident, ligne in PIERRES:
i = h.find(f'<section class="pan" id="pan-{ident}"')
assert i > 0, f'panneau {ident} introuvable'
j = h.find('</p>', h.find('<p class="po-s">', i)) + len('</p>')
assert j > i, f'sous-titre du panneau {ident} introuvable'
assert '<p class="po-n">' not in h[i:j+120], f'{ident} a déjà une ligne de pierre'
h = h[:j] + f'<p class="po-n">{ligne}</p>' + h[j:]
J.append(f'partie {ident} : « {ligne} »')
# ââ 2 · le collant se pose SOUS ce qui occupe le haut de la fenĂȘtre ââââââââ
FEUILLE = """
<style id="ami-charte-collant-sous-navbar">
/* ââ LE COLLANT SE POSE SOUS LA BARRE DU SITE (26/09/2026) ââââââââââââââââââââââââââââââââââââââââââ
Diane : « j'ai ma navbarre Webnode sticky, et la navbarre de la page passe en dessous, et la barre des
onglets aussi un peu. » Un bloc ne peut pas savoir à l'avance ce que son hÎte met au-dessus de lui :
il le MESURE, et dĂ©cale ses propres barres d'autant. --top-site est Ă©crit par le script ci-dessous. ââ */
#ami-charte .bar{top:var(--top-site,0px)!important}
#ami-charte .ami-topbar{top:var(--top-site,0px)!important}
#ami-charte > .tab-bar,#ami-charte #tv>.tab-bar,#ami-charte .tvx>.tab-bar,
#ami-charte .lxq #ami-lexique .tab-bar{top:calc(var(--top-site,0px) + var(--top-h,0px))!important}
#ami-charte .sub-host{top:calc(var(--top-site,0px) + var(--nav-h,0px))!important}
#ami-charte #amiPile{top:var(--top-site,0px)!important}
/* et ce vers quoi on saute ne se cache pas non plus derriĂšre elle */
#ami-charte{scroll-padding-top:calc(var(--top-site,0px) + var(--nav-h,0px) + 1rem)}
</style>
<script id="ami-charte-mesure-du-haut">
/* ââ CE QUI OCCUPE LE HAUT DE LA FENĂTRE (26/09/2026) âââââââââââââââââââââââââââââââââââââââââââââââ
On regarde qui se trouve rĂ©ellement au sommet de la fenĂȘtre â elementsFromPoint au pixel 1 â et on
retient le bas du plus bas des Ă©lĂ©ments FIXES ou COLLĂS qui n'appartiennent pas au bloc. C'est plus
sûr que de deviner un sélecteur : ça marche quel que soit l'habillage du site, et ça suit une barre
qui rĂ©trĂ©cit au dĂ©filement. ââ */
(function(){'use strict';
var R=document.getElementById('ami-charte'); if(!R) return;
var dernier=-1;
function mesure(){
var bas=0, x=Math.round(window.innerWidth/2);
[1,3].forEach(function(y){
(document.elementsFromPoint(x,y)||[]).forEach(function(e){
if(!e || e===document.documentElement || e===document.body) return;
if(R.contains(e) || e.contains(R)) return; /* le bloc, et ce qui l'enveloppe, ne comptent pas */
var c=getComputedStyle(e);
if(c.position!=='fixed' && c.position!=='sticky') return;
var r=e.getBoundingClientRect();
if(r.height<=0 || r.height>260) return; /* un voile plein écran n'est pas une barre */
if(r.width < window.innerWidth*0.6) return; /* une barre du haut traverse la fenĂȘtre */
if(r.top>12) return; /* elle est POSĂE en haut */
if(r.bottom>bas) bas=Math.round(r.bottom);
});
});
if(bas!==dernier){ dernier=bas; document.documentElement.style.setProperty('--top-site', bas+'px'); }
}
mesure();
window.addEventListener('load', mesure);
window.addEventListener('resize', mesure);
var t=null;
window.addEventListener('scroll', function(){ if(t) return; t=setTimeout(function(){ t=null; mesure(); },150); }, {passive:true});
[400,1200,2500].forEach(function(ms){ setTimeout(mesure, ms); });
})();
</script>
"""
fin = h.rfind('</body>'); assert fin > 0
h = h[:fin] + FEUILLE + h[fin:]
J.append('le collant se pose sous ce qui occupe le haut de la fenĂȘtre (--top-site, mesurĂ©)')
open(OUT, 'w', encoding='utf-8').write(h)
print('\n'.join('â ' + x for x in J)); print(f'â {OUT} · {len(h.encode())//1024} Ko')
hote_webnode_navbar.html ouvert par le contrĂŽle Playwright20 lignes
HĂŽte d'essai qui reproduit agdistys.fr : navbar collante de 92 px PLUS les enveloppes overflow-x:hidden. Sert Ă mesurer si les barres du bloc se posent sous la navbar ou glissent dessous.
â§ Copier le script
<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>HĂŽte d'essai â Webnode avec navbar collĂ©e</title><style>
body{margin:0;font:16px/1.6 system-ui;background:#fff;color:#111}
/* la navbar du site : collée en haut, comme celle d'Agdistys */
#navsite{position:sticky;top:0;z-index:5000;height:92px;display:flex;align-items:center;
justify-content:center;background:#151515;color:#eee;font-size:20px}
#page{overflow-x:hidden}
.zone{max-width:100%;overflow-x:hidden}
footer{padding:3rem 1rem;background:#eee;text-align:center}
</style></head><body>
<div id="navsite">Agdistys âą Designer d'une Vie Meilleure</div>
<div id="page"><div class="zone"><main id="zone"></main></div></div>
<footer><p>Pied du site</p></footer>
<script>fetch('c10.html').then(r=>r.text()).then(t=>{
const corps = t.slice(t.indexOf('<body>')+6, t.lastIndexOf('</body>'));
document.getElementById('zone').innerHTML = corps;
[...document.getElementById('zone').querySelectorAll('script')].forEach(s=>{const n=document.createElement('script');[...s.attributes].forEach(a=>n.setAttribute(a.name,a.value));n.textContent=s.textContent;s.replaceWith(n)});
});</script>
</body></html>
p1_pied_v3.py python3 p1_pied_v3.py v2_complet.html pied_v3.html328 lignes
Le pied de page du SITE (sommaire â° + recherche đ + retour â, collĂ© Ă la fin de toutes les pages Webnode) passĂ© en V3 : reconnaissance de #amiSommaire, lecture du document du cadre pour le sommaire et la recherche, hautSite() qui mesure la navbarre, rattrapage du saut, et le Merci en serif avec la ligne en trois temps (ici · Google · Tipeee).
â§ Copier le script
# -*- coding: utf-8 -*-
"""Pied de page du site â V3 (26 septembre 2026).
Trois choses, chacune mesurĂ©e avant d'ĂȘtre Ă©crite :
1. LE â° NE SE DOUBLE PLUS. Les volumes aMi nomment leur sommaire #amiSommaire,
le pied cherchait #ami-toc-toggle : sur agdistys.fr/ami-charte les deux â°
se posaient AU MĂME PIXEL (mesurĂ© en direct : site y=1026, bloc y=1026 x=1704).
Le â et la đ Ă©taient dĂ©jĂ reconnus (#amiHaut, #amiRech) et ne se doublaient pas.
2. LE PIED VOIT DANS LE CADRE. Sur une page oĂč le volume est dans un cadre
(/ami-rencontre/ : cadre de 52 739 px), les outils du volume sont Ă©pinglĂ©s Ă
la fenĂȘtre DU CADRE â mesurĂ© en direct : â Ă y=0 du cadre, đ Ă y=52 689,
â et đ§ Ă y=13. Autrement dit : dĂšs qu'on a descendu, le lecteur n'a plus
ni sommaire, ni recherche, ni contraste. Le pied du site, lui, est dans la
page mĂšre : il peut lire le document du cadre (mĂȘme origine, srcdoc) et
descendre la page mĂšre au bon endroit. Donc le â° et la đ du site
deviennent le sommaire et la recherche DU VOLUME.
3. LES RĂSULTATS NE SE CACHENT PLUS SOUS LA NAVBARRE. La hauteur de ce qui
occupe le haut de l'écran est mesurée (position collante ou fixe, largeur
> 60 % de l'écran, hauteur †260 px) et retirée du point d'arrivée.
Et le Merci : « Merci pour Nous » en serif, et la ligne en trois temps
(ici · Google · Tipeee).
"""
import re, sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:70])
h = h.replace(avant, apres)
n += 1
# ââ 1 · l'entĂȘte du bloc âââââââââââââââââââââââââââââââââââââââââââââââ
pose(""" SOMMAIRE AUTOMATIQUE + RECHERCHE + RETOUR EN HAUT · aMi V2
19 septembre 2026 â la V1 (sommaire + â) reçoit la loupe đ des volumes.""",
""" SOMMAIRE AUTOMATIQUE + RECHERCHE + RETOUR EN HAUT · aMi V3
26 septembre 2026 â le pied voit dans le cadre, et ne double plus le â°.""")
pose(""" · il ne double pas les outils d'un volume aMi : s'il trouve dĂ©jĂ
un â (#bTop / #amiHaut), un â° (#ami-toc-toggle) ou une loupe
(#amiRech) dans la page, il ne pose que ce qui manque.""",
""" · il ne double pas les outils d'un volume aMi : s'il trouve dĂ©jĂ
un â (#bTop / #amiHaut), un â° (#ami-toc-toggle / #amiSommaire) ou
une loupe (#amiRech) dans la page, il ne pose que ce qui manque.
CE QU'IL FAIT DE NEUF EN V3
· DANS UN CADRE â quand le volume est posĂ© dans un cadre (les pages
« WEBNODE »), ses propres â â° đ sont Ă©pinglĂ©s Ă la fenĂȘtre DU
CADRE, donc hors de vue dĂšs qu'on a descendu. Le pied du site lit
alors le document du cadre : son â° liste les vraies parties du
volume, sa đ cherche dans tout le volume, et l'arrivĂ©e se fait en
descendant la page mĂšre. C'est le seul endroit d'oĂč ça marche.
· SOUS LA NAVBARRE â la hauteur de ce qui occupe le haut de l'Ă©cran
est mesurée, jamais devinée : un résultat n'arrive plus caché
derriĂšre la navbarre du site.""")
# ââ 2 · reconnaĂźtre le â° des volumes âââââââââââââââââââââââââââââââââââ
pose(""" var dejaToc = document.getElementById('ami-toc-toggle');""",
""" var dejaToc = document.getElementById('ami-toc-toggle') || document.getElementById('amiSommaire');""")
# ââ 3 · le cadre : oĂč chercher âââââââââââââââââââââââââââââââââââââââââ
pose(""" /* ââ OĂč chercher âââââââââââââââââââââââââââââââââââââââââââââââ */
function zone(){
var v = [].slice.call(document.querySelectorAll('.pane'))
.filter(function(p){ return !p.hidden && p.offsetParent !== null; });
if (v.length) return v;
return [document.body];
}""",
""" /* ââ LE CADRE ââââââââââââââââââââââââââââââââââââââââââââââââââââ
Un volume « WEBNODE » vit dans un cadre srcdoc, donc de la mĂȘme
origine : son document est lisible. Ses propres outils, eux, sont
Ă©pinglĂ©s Ă la fenĂȘtre du cadre (haute de 50 000 px) et sortent de la
vue dĂšs qu'on descend. C'est donc ICI qu'ils doivent revivre. */
function cadre(){
var fs = [].slice.call(document.querySelectorAll('iframe'));
for (var i = 0; i < fs.length; i++) {
var f = fs[i];
if (f.closest('.amiToc-tiroir, .amiRech')) continue;
var d = null;
try { d = f.contentDocument; } catch (e) { continue; }
if (!d || !d.body) continue;
if (f.getBoundingClientRect().height < 400) continue;
if ((d.body.textContent || '').replace(/\\s+/g, ' ').trim().length < 600) continue;
return { f: f, d: d };
}
return null;
}
/* Le haut de l'Ă©cran occupĂ© par le site (navbarre collante) â mesurĂ©. */
function hautSite(){
var bas = 0, x = Math.round(window.innerWidth / 2);
[1, 3].forEach(function(y){
var l = document.elementsFromPoint(x, y) || [];
[].slice.call(l).forEach(function(e){
if (!e || e === document.documentElement || e === document.body) return;
if (e.closest('.amiToc-out, .amiToc-tiroir, .amiRech')) return;
var c = getComputedStyle(e);
if (c.position !== 'fixed' && c.position !== 'sticky') return;
var r = e.getBoundingClientRect();
if (r.height <= 0 || r.height > 260) return;
if (r.width < window.innerWidth * 0.6) return;
if (r.top > 12) return;
if (r.bottom > bas) bas = Math.round(r.bottom);
});
});
return bas;
}
/* Le haut d'un Ă©lĂ©ment dans la page MĂRE, qu'il vienne d'ici ou du cadre. */
function hautEcran(el){
var t = el.getBoundingClientRect().top;
if (el.ownerDocument === document) return t;
var C = cadre();
return C ? t + C.f.getBoundingClientRect().top : t;
}
/* ââ OĂč chercher âââââââââââââââââââââââââââââââââââââââââââââââ */
function zone(){
var C = cadre();
if (C) {
var vc = [].slice.call(C.d.querySelectorAll('.pane'))
.filter(function(p){ return !p.hidden && p.offsetParent !== null; });
return vc.length ? vc : [C.d.body];
}
var v = [].slice.call(document.querySelectorAll('.pane'))
.filter(function(p){ return !p.hidden && p.offsetParent !== null; });
if (v.length) return v;
return [document.body];
}""")
# ââ 4 · aller Ă un endroit, d'ici ou du cadre, sous la navbarre ââââââââ
pose(""" function pile(){
var h = 0;
['.ami-barre', '.tab-bar', '#amiPile', '.tab-bar-full'].forEach(function(sel){
var e = document.querySelector(sel);
if (e && /sticky|fixed/.test(getComputedStyle(e).position) && getComputedStyle(e).display !== 'none') h += Math.round(e.getBoundingClientRect().height);
});
return h;
}
function vers(h, marge){
var y = h.getBoundingClientRect().top + window.pageYOffset - pile() - (marge || 14);
window.scrollTo({ top: y < 0 ? 0 : y, behavior: 'smooth' });
}""",
""" function pile(){
var h = hautSite();
['.ami-barre', '.tab-bar', '#amiPile', '.tab-bar-full'].forEach(function(sel){
var e = document.querySelector(sel);
if (e && /sticky|fixed/.test(getComputedStyle(e).position) && getComputedStyle(e).display !== 'none') h += Math.round(e.getBoundingClientRect().height);
});
return h;
}
function vers(h, marge){
var y = hautEcran(h) + window.pageYOffset - pile() - (marge || 14);
window.scrollTo({ top: y < 0 ? 0 : y, behavior: 'smooth' });
}""")
# ââ 5 · le titre le plus proche, dans le bon document ââââââââââââââââââ
pose(""" var e = el;
while (e && e !== document.body) {""",
""" var e = el, corps = el.ownerDocument.body;
while (e && e !== corps) {""")
# ââ 6 · le volet, dans le bon document ââââââââââââââââââââââââââââââââ
pose(""" var pane = el.closest('.pane, .tab-section, .tab-content, .tab-panel, .pt-pane, [role="tabpanel"]');
if (!pane) return null;
var id = pane.id, k = pane.dataset.c || pane.dataset.t || pane.dataset.tab;
var btn = null;
if (id) btn = document.querySelector(""",
""" var pane = el.closest('.pane, .tab-section, .tab-content, .tab-panel, .pt-pane, [role="tabpanel"]');
if (!pane) return null;
var doc = el.ownerDocument;
var id = pane.id, k = pane.dataset.c || pane.dataset.t || pane.dataset.tab;
var btn = null;
if (id) btn = doc.querySelector(""")
pose(""" if (!btn && k) btn = document.querySelector('.tab-btn[data-c=""",
""" if (!btn && k) btn = doc.querySelector('.tab-btn[data-c=""")
# ââ 7 · l'index : le texte du cadre s'il y en a un ââââââââââââââââââââ
pose(""" function bati(){
IDX = [];
var w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT), n;
while ((n = w.nextNode())) {
var s = n.textContent.replace(/\\s+/g, ' ').trim(); if (s.length < 3) continue;
var p = n.parentElement; if (!p || p.closest(EXCL)) continue;
if (p.matches(TITRES) || p.closest(TITRES)) continue;
IDX.push({ t: s, tl: s.toLowerCase(), el: p });
}""",
""" function bati(){
IDX = [];
var C = cadre();
var racine = C ? C.d.body : document.body;
var w = (C ? C.d : document).createTreeWalker(racine, NodeFilter.SHOW_TEXT), n;
while ((n = w.nextNode())) {
var s = n.textContent.replace(/\\s+/g, ' ').trim(); if (s.length < 3) continue;
var p = n.parentElement; if (!p || p.closest(EXCL)) continue;
if (p.matches(TITRES) || p.closest(TITRES)) continue;
IDX.push({ t: s, tl: s.toLowerCase(), el: p });
}""")
# ââ 8 · « oĂč suis-je » : mesurer dans la page mĂšre ââââââââââââââââââââ
pose(""" ancres.forEach(function(x){ if (x.h.getBoundingClientRect().top <= seuil) ici = x; x.a.classList.remove('ici'); });""",
""" ancres.forEach(function(x){ if (hautEcran(x.h) <= seuil) ici = x; x.a.classList.remove('ici'); });""")
# ââ 9 · refaire quand le cadre a fini de grandir ââââââââââââââââââââââ
pose(""" construis(); bati(); auDefile();
window.addEventListener('load', refais);
[300, 900, 2000].forEach(function(ms){ setTimeout(refais, ms); });""",
""" construis(); bati(); auDefile();
window.addEventListener('load', refais);
[300, 900, 2000].forEach(function(ms){ setTimeout(refais, ms); });
/* Un cadre arrive aprĂšs la page et grandit ensuite (le pont rĂšgle sa
hauteur) : on refait le sommaire quand son document est lĂ . */
(function(){
var C0 = cadre(); if (!C0) { [3500, 6000].forEach(function(ms){ setTimeout(refais, ms); }); return; }
C0.f.addEventListener('load', function(){ setTimeout(refais, 400); });
[1500, 3500, 6000].forEach(function(ms){ setTimeout(refais, ms); });
})();""")
# ââ 10 · le Merci : serif, et la ligne en trois temps âââââââââââââââââ
pose(""" var TIPEEE = 'https://fr.tipeee.com/agdistys';""",
""" var TIPEEE = 'https://fr.tipeee.com/agdistys';
/* La fiche Google. Ce lien-ci OUVRE la fiche (le bouton « Rédiger un avis »
y est) ; le lien qui ouvre DIRECTEMENT le formulaire se copie dans la
Fiche d'Ă©tablissement Google â « Demander des avis » â il a la forme
https://g.page/r/xxxxxxxx/review. Une seule ligne Ă changer ici. */
var AVIS = 'https://share.google/wVIE5HqQFUiZVRDrp';""")
# ââ 10 bis · LE MERCI SE SĂPARE DU SOUTIEN ââââââââââââââââââââââââââââ
# [stated] 26/09 : « quand on clique ça dit juste merci, et ensuite quand on
# clique sur "un avis/témoignage sur Google · un soutien libre sur Tipeee"
# c'est sur ces deux que ça renvoie vers Tipeee ou Google ».
# Donc : le grand bouton ne va NULLE PART â il dit merci, il compte, il
# remercie. Les deux liens de la ligne du dessous portent les deux gestes
# qui coûtent quelque chose (un avis, un soutien) : on ne les confond plus
# avec le merci gratuit.
pose(""" 'a{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:48px;max-width:100%;padding:10px 21px;border:1px solid #e7c778;border-radius:999px;background:#11100d;color:#ffe4a0;text-decoration:none;font:500 18px/1.4 system-ui,-apple-system,sans-serif;box-shadow:0 3px 14px #0003;transition:background .15s}' +
'a:hover{background:#292318}a:focus-visible{outline:3px solid #fff4d6;outline-offset:4px}' +
'.count{font-variant-numeric:tabular-nums;font-size:13px;font-weight:400;color:#cfbd94}' +
'small{display:block;margin-top:7px;color:#fff4d6;font:normal 13px/1.5 system-ui,-apple-system,sans-serif}' +
'.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}' +
'</style><a target="_blank" rel="noopener noreferrer" aria-label="Dire merci et ouvrir Tipeee dans un nouvel onglet">' +
'<span>🪞 Merci pour Nous 🌱</span><span class="count"></span></a>' +
'<small>Un merci ici · un soutien libre sur Tipeee.</small><span class="sr" role="status" aria-live="polite"></span>';""",
""" 'button.merci{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:48px;max-width:100%;padding:11px 24px;border:1px solid #e7c778;border-radius:999px;background:#11100d;color:#ffe4a0;cursor:pointer;box-shadow:0 3px 14px #0003;transition:background .15s;' +
'font:400 23px/1.3 Cormorant Garamond,Cormorant,Georgia,Times New Roman,serif;letter-spacing:.01em}' +
'button.merci:hover{background:#292318}' +
'button.merci:focus-visible,a:focus-visible{outline:3px solid #fff4d6;outline-offset:4px}' +
'button.merci[disabled]{cursor:default;opacity:.92}' +
'.count{font-variant-numeric:tabular-nums;font-size:13px;font-weight:400;color:#cfbd94;font-family:system-ui,-apple-system,sans-serif}' +
'small{display:block;margin-top:9px;color:#fff4d6;font:normal 13px/1.6 system-ui,-apple-system,sans-serif}' +
'small a{color:#ffe4a0;text-decoration:none;border-bottom:1px solid rgba(231,199,120,.45);padding-bottom:1px}' +
'small a:hover{border-bottom-color:#ffe4a0}' +
'.dit{display:block;margin-top:7px;min-height:1.6em;color:#cfbd94;font:normal 13px/1.6 system-ui,-apple-system,sans-serif}' +
'.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}' +
'</style><button class="merci" type="button">' +
'<span>🪞 Merci pour Nous 🌱</span><span class="count"></span></button>' +
'<small>Un merci Ici · <a class="avis" target="_blank" rel="noopener noreferrer">un avis ou un témoignage sur Google</a>' +
' · <a class="tip" target="_blank" rel="noopener noreferrer">un soutien libre sur Tipeee</a>.</small>' +
'<span class="dit" aria-hidden="true"></span><span class="sr" role="status" aria-live="polite"></span>';""")
pose(""" var link = root.querySelector('a'), count = root.querySelector('.count'), status = root.querySelector('.sr');
link.href = TIPEEE;""",
""" var link = root.querySelector('button.merci'), count = root.querySelector('.count'), status = root.querySelector('.sr');
var dit = root.querySelector('.dit');
var lTip = root.querySelector('a.tip'); if (lTip) lTip.href = TIPEEE;
var lAvis = root.querySelector('a.avis'); if (lAvis) lAvis.href = AVIS;
function annonce(t){ status.textContent = t; if (dit) dit.textContent = t; }""")
# le merci ne navigue plus : il compte, il répond, il se tait ensuite
pose(""" function thank() {
// Do not preventDefault or await navigation: Tipeee opens even if counting fails.
if (!enabled || pending) return;
read();
if (sent) { status.textContent = 'Ton merci est dĂ©jĂ enregistrĂ©. Tipeee sâouvre.'; return; }""",
""" function thank() {
/* Le merci ne va nulle part : il compte, et il répond. Les deux liens
de la ligne du dessous portent l'avis et le soutien. */
if (pending) return;
if (!enabled) { annonce('Merci ! (le compteur est en sommeil)'); return; }
read();
if (sent) { annonce('Ton merci est déjà là . Merci encore.'); return; }""")
pose(""" display(data.value); sent = true; save();
status.textContent = 'Merci ! Ton merci est enregistré.';
})
.catch(function () { status.textContent = 'Tipeee reste accessible ; le compteur est momentanément indisponible.'; })""",
""" display(data.value); sent = true; save();
annonce('Merci ! Ton merci est enregistré.');
})
.catch(function () { annonce('Ton merci est bien reçu ; le compteur est momentanément indisponible.'); })""")
pose(""" link.addEventListener('click', thank);
link.addEventListener('auxclick', function (e) { if (e.button === 1) thank(); });""",
""" link.addEventListener('click', thank);""")
# ââ 11 · le rattrapage du saut ââââââââââââââââââââââââââââââââââââââââ
# Mesuré : dans un cadre, ouvrir la bonne partie change TOUTE la hauteur
# au-dessus de la cible (arrivée à -243 px au lieu de 0). On ne prédit pas
# la nouvelle place : on remesure aprĂšs coup, deux fois.
pose(""" function vers(h, marge){
var y = hautEcran(h) + window.pageYOffset - pile() - (marge || 14);
window.scrollTo({ top: y < 0 ? 0 : y, behavior: 'smooth' });
}""",
""" function vers(h, marge){
var y = hautEcran(h) + window.pageYOffset - pile() - (marge || 14);
window.scrollTo({ top: y < 0 ? 0 : y, behavior: 'smooth' });
}
/* Ouvrir une partie change la hauteur au-dessus de la cible : on remesure. */
function versRattrape(h, marge){
vers(h, marge);
[420, 1000].forEach(function(ms){ setTimeout(function(){ vers(h, marge); }, ms); });
}""")
pose(""" a.addEventListener('click', function(e){ e.preventDefault(); vers(h); ouvre(false); });""",
""" a.addEventListener('click', function(e){ e.preventDefault(); versRattrape(h, 14); ouvre(false); });""")
pose(""" var cible = e.el.closest('p, li, td, th, h1, h2, h3, h4, blockquote, figure, div') || e.el;
vers(cible, 64);""",
""" var cible = e.el.closest('p, li, td, th, h1, h2, h3, h4, blockquote, figure, div') || e.el;
versRattrape(cible, 64);""")
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', n, 'opĂ©rations,', len(h) // 1024, 'Ko')
p5_charte_collant_generique.py python3 p5_charte_collant_generique.py c10.html c11.html129 lignes
Charte V11 : remplace la liste de sĂ©lecteurs de la V10 par une passe gĂ©nĂ©rique â tout ce qui colle en haut dans la racine se dĂ©cale de la hauteur de ce que l'hĂŽte occupe, en gardant sa hauteur d'origine ; en-tĂȘtes de tableaux et collants de rouleau Ă©cartĂ©s.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Charte V11 â le collant se dĂ©cale POUR DE BON, et on le vĂ©rifie en ayant descendu.
CE QUI N'ALLAIT PAS DANS LA V10
J'avais écrit une LISTE de noms (.bar, .ami-topbar, .tab-bar, .sub-host, #amiPile)
et la barre de la Charte s'appelle nav.ami-barre#amiBarre : elle est passée entre
les mailles. Pire : ma vérification mesurait AU CHARGEMENT, quand rien ne colle
encore â la barre Ă©tait Ă 92 parce que la page n'avait pas bougĂ©, pas parce que
la rÚgle marchait. Mesuré aprÚs 3 000 px, la vérité :
barre aMi chargement y=92 · descendu y=0 (sous la navbarre)
â€ïžâđ„ chargement y=171 · descendu y=8 (couvert)
â đ§ â¶ chargement y=97 · descendu y=5 (couverts)
D'oĂč les trois symptĂŽmes de Diane d'un seul coup : la barre passe dessous, et
le bouton qui replie les onglets â comme le contraste et la lecture fluide â
devient inatteignable.
CE QUE FAIT LA V11
Plus de liste. Le bloc cherche TOUT ce qui colle en haut chez lui, retient la
hauteur que chacun demandait au départ, et y ajoute la hauteur de ce que l'hÎte
occupe en haut de l'écran. Une barre ajoutée demain sera prise sans que je repasse.
Sont Ă©cartĂ©s : les en-tĂȘtes de tableaux (ils collent dans leur propre rouleau,
pas dans la fenĂȘtre) et tout ce qui vit dans un conteneur qui dĂ©file.
"""
import sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
# ââ on retire le bloc V10 (la liste de noms + son script de mesure) ââââââââââââ
i = h.find('id="ami-charte-collant-sous-navbar"')
assert i > 0, 'bloc V10 introuvable'
a = h.rfind('<', 0, i)
j = h.find('</script>', h.find('id="ami-charte-mesure-du-haut"')) + len('</script>')
assert j > a, 'fin du bloc V10 introuvable'
NEUF = r'''<script id="ami-charte-decale-ce-qui-colle">
/* ââ CE QUI COLLE SE POSE SOUS CE QUE L'HĂTE OCCUPE (V11, 26/09/2026) ââââââââââ
Diane : « j'ai ma navbarre Webnode sticky, et la navbarre de la page passe en
dessous. » La V10 visait une liste de noms et a raté nav.ami-barre. Ici on ne
nomme plus rien : on prend tout ce qui colle en haut DANS le bloc, on retient
la hauteur qu'il demandait, et on y ajoute celle de l'hĂŽte. ĂcartĂ©s : les
en-tĂȘtes de tableaux et tout ce qui colle Ă l'intĂ©rieur d'un rouleau â ceux-lĂ
ne collent pas Ă la fenĂȘtre, les dĂ©caler les ferait sauter. */
(function(){'use strict';
var R = document.getElementById('ami-charte'); if(!R) return;
var dernier = -1;
function hautDeLHote(){
var bas = 0, x = Math.round(window.innerWidth / 2);
[1, 3].forEach(function(y){
var l = document.elementsFromPoint(x, y) || [];
[].slice.call(l).forEach(function(e){
if (!e || e === document.documentElement || e === document.body) return;
if (R.contains(e) || e.contains(R)) return; /* pas nous, ni notre parent */
var c = getComputedStyle(e);
if (c.position !== 'fixed' && c.position !== 'sticky') return;
var r = e.getBoundingClientRect();
if (r.height <= 0 || r.height > 260) return; /* une barre, pas un décor */
if (r.width < window.innerWidth * 0.6) return; /* large, donc elle barre l'écran */
if (r.top > 12) return; /* posée tout en haut */
if (r.bottom > bas) bas = Math.round(r.bottom);
});
});
return bas;
}
function dansUnRouleau(e){
var p = e.parentElement;
while (p && p !== R && p !== document.body) {
var c = getComputedStyle(p);
if (/auto|scroll/.test(c.overflowY) || /auto|scroll/.test(c.overflowX)) return true;
p = p.parentElement;
}
return false;
}
function collants(){
var out = [];
var tous = [R].concat([].slice.call(R.querySelectorAll('*')));
tous.forEach(function(e){
var c = getComputedStyle(e);
if (c.position !== 'sticky' && c.position !== 'fixed') return;
if (/^(TH|THEAD|TR|TD|CAPTION)$/.test(e.tagName)) return; /* en-tĂȘte de tableau */
if (dansUnRouleau(e)) return; /* colle dans son rouleau */
if (e.dataset.amiHaut === undefined) { /* la hauteur d'origine, une fois */
var t = parseFloat(c.top);
if (!isFinite(t)) return; /* top:auto : il ne colle pas en haut */
if (t > 260) return; /* posé bas (un pied collant) */
e.dataset.amiHaut = String(Math.round(t));
}
out.push(e);
});
return out;
}
function pose(){
var hote = hautDeLHote();
if (hote === dernier) return;
dernier = hote;
document.documentElement.style.setProperty('--top-site', hote + 'px');
collants().forEach(function(e){
e.style.setProperty('top', (parseInt(e.dataset.amiHaut, 10) + hote) + 'px', 'important');
});
R.style.setProperty('scroll-padding-top', (hote + 96) + 'px');
}
pose();
window.addEventListener('load', pose);
window.addEventListener('resize', function(){ dernier = -1; pose(); });
var t = null;
window.addEventListener('scroll', function(){
if (t) return;
t = setTimeout(function(){ t = null; pose(); }, 150);
}, {passive:true});
/* le bloc se déplie encore un moment aprÚs l'arrivée : on repasse */
[400, 1200, 2500, 5000].forEach(function(ms){ setTimeout(function(){ dernier = -1; pose(); }, ms); });
})();
</script>'''
h = h[:a] + NEUF + h[j:]
# le marqueur de version dans la page
h = h.replace('<html', '<!-- Charte aMi · collant gĂ©nĂ©rique V11 â 26 septembre 2026 -->\n<html', 1)
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', len(h) // 1024, 'Ko')
p6_charte_v12.py python3 p6_charte_v12.py c11.html c12.html113 lignes
Charte V12 : le collant ne gĂšle plus sa hauteur (on relit la feuille Ă chaque passe), nom des onglets en 600, Codons du Langage en tanzanite.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Charte V12 â trois retours de Diane du 27/09 au matin.
1 · LE GROS TROU ENTRE LA NAVBARRE ET LES ONGLETS. Mesuré : la barre des 8
parties (div.bar) se posait Ă 221 px quand la barre aMi finit Ă 140 â
81 px de vide par lequel le texte défilait. CAUSE : ma V11 FIGEAIT la
hauteur d'origine de chaque collant. Or celle de div.bar s'écrit
min(var(--nav-h),9rem), et --nav-h est la hauteur VIVANTE de la barre aMi :
129 px dépliée, 48 px repliée. J'ai gelé le 129 du chargement, puis j'ai
ajouté 92 : 221. La barre se repliait, le trou restait.
RĂPARĂ : on ne gĂšle plus rien. Ă chaque passe on rend la parole Ă la
feuille de style (on retire notre top), on lit ce qu'elle dit MAINTENANT,
et on y ajoute la hauteur de l'hĂŽte. Le calcul du document reste vivant.
2 · LE NOM DES ONGLETS EN GRAS. En serif clair sur fond noir, au téléphone,
il disparaĂźt. On passe .ot en 600.
3 · LES CODONS DU LANGAGE EN TANZANITE. âžïž grenat + đ chrysocolle : le seul
bleu de la palette qui garde le violet du grenat.
"""
import sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:80])
h = h.replace(avant, apres); n += 1
# ââ 1 · on ne gĂšle plus la hauteur d'origine ââââââââââââââââââââââââââââââââââ
pose(""" if (e.dataset.amiHaut === undefined) { /* la hauteur d'origine, une fois */
var t = parseFloat(c.top);
if (!isFinite(t)) return; /* top:auto : il ne colle pas en haut */
if (t > 260) return; /* posé bas (un pied collant) */
e.dataset.amiHaut = String(Math.round(t));
}
out.push(e);""",
""" out.push(e);""")
pose(""" function pose(){
var hote = hautDeLHote();
if (hote === dernier) return;
dernier = hote;
document.documentElement.style.setProperty('--top-site', hote + 'px');
collants().forEach(function(e){
e.style.setProperty('top', (parseInt(e.dataset.amiHaut, 10) + hote) + 'px', 'important');
});
R.style.setProperty('scroll-padding-top', (hote + 96) + 'px');
}""",
""" /* On ne retient AUCUNE hauteur : celle que la feuille de style demande peut
changer d'un instant Ă l'autre (min(var(--nav-h),9rem) suit la barre qui se
replie). à chaque passe on lui rend la parole, on écoute, puis on ajoute. */
function pose(){
var hote = hautDeLHote();
document.documentElement.style.setProperty('--top-site', hote + 'px');
var barres = collants();
barres.forEach(function(e){ e.style.removeProperty('top'); });
var dits = barres.map(function(e){ return parseFloat(getComputedStyle(e).top); });
barres.forEach(function(e, i){
var t = dits[i];
if (!isFinite(t) || t > 260) return; /* top:auto, ou posé bas : pas notre affaire */
e.style.setProperty('top', Math.round(t + hote) + 'px', 'important');
});
R.style.setProperty('scroll-padding-top', (hote + 96) + 'px');
dernier = hote;
}""")
pose(""" window.addEventListener('resize', function(){ dernier = -1; pose(); });
var t = null;
window.addEventListener('scroll', function(){
if (t) return;
t = setTimeout(function(){ t = null; pose(); }, 150);
}, {passive:true});
/* le bloc se déplie encore un moment aprÚs l'arrivée : on repasse */
[400, 1200, 2500, 5000].forEach(function(ms){ setTimeout(function(){ dernier = -1; pose(); }, ms); });""",
""" window.addEventListener('resize', pose);
var t = null;
/* la barre se replie EN DĂFILANT : il faut repasser pendant le dĂ©filement,
sinon les onglets restent posés à la hauteur de la barre dépliée. */
window.addEventListener('scroll', function(){
if (t) return;
t = setTimeout(function(){ t = null; pose(); }, 120);
}, {passive:true});
/* le bloc se déplie encore un moment aprÚs l'arrivée : on repasse */
[400, 1200, 2500, 5000].forEach(function(ms){ setTimeout(pose, ms); });""")
# ââ 2 · le nom des onglets en gras âââââââââââââââââââââââââââââââââââââââââââ
pose('</head>',
"""<style id="ami-charte-onglets-lisibles">
/* Diane, 27/09 : « le titre des onglets en serif faut mettre en gras sinon sur
téléphone on voit rien. » Un serif clair, fin, sur du noir, à 13 px : les
pleins et les déliés se ferment. On épaissit le nom, pas la légende. */
#ami-charte .bar .ong .ot{font-weight:600;letter-spacing:.005em}
</style>
</head>""")
# ââ 3 · Les Codons du Langage : saphir â tanzanite âââââââââââââââââââââââââââ
pose('style="--tc:var(--saphir);--tcl:var(--saphir);--tc-rgb:0,119,238;--tcx:var(--saphir-ct)"',
'style="--tc:var(--tanzanite);--tcl:var(--tanzanite);--tc-rgb:107,99,255;--tcx:var(--tanzanite-ct)"', 2)
pose('#ami-charte .pan[data-o="cod"]{--tc:var(--saphir-gras)!important;--tc-rgb:0,99,219!important}',
'#ami-charte .pan[data-o="cod"]{--tc:var(--tanzanite-gras)!important;--tc-rgb:87,84,238!important}')
pose('#ami-charte .pan[data-o="cod"]{--tcl:var(--saphir-gras)!important}',
'#ami-charte .pan[data-o="cod"]{--tcl:var(--tanzanite-gras)!important}')
pose('<p class="po-n">le saphir · bleu</p>', '<p class="po-n">la tanzanite · bleu-violet</p>')
h = h.replace('collant gĂ©nĂ©rique V11 â 26 septembre 2026',
'collant vivant V12 â 27 septembre 2026', 1)
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', n, 'opĂ©rations,', len(h) // 1024, 'Ko')
p2_pied_v4.py python3 p2_pied_v4.py pied_v3.html pied_v4.html161 lignes
Pied du site V4 : numérotation et reprise de place sur une version plus ancienne ; le gras d'un paragraphe devient un titre ; le niveau du sommaire vient de la taille réelle ; étiquette = premiÚre ligne de innerText ; hiérarchie visible dans le tiroir.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Pied de page du site â V4 (27 septembre 2026).
TROIS CHOSES, chacune mesurĂ©e avant d'ĂȘtre Ă©crite.
1 · LA PAGE QUI N'AVAIT PAS DE LOUPE. agdistys.fr/la-tour-de-babel porte DEUX
blocs : la V1 (« SOMMAIRE AUTOMATIQUE + RETOUR EN HAUT · aMi V1 »), collée
dans la page elle-mĂȘme, et la V3 du pied du site. La V1 arrive la premiĂšre,
pose window.__amiToc = true, et la V3 se retire poliment : « collé deux fois,
une seule vie ». Mesuré en ligne : deux boutons de classe « amiToc-b » seule,
dans l'ordre â° puis â (l'ordre de la V1), et AUCUN .amiRech dans la page.
RĂPARĂ : la V4 se numĂ©rote. Si elle trouve une version plus ancienne dĂ©jĂ
installĂ©e, elle ne se retire plus : elle prend la place â elle retire les
boutons, le tiroir et la boĂźte de recherche de l'ancienne, et s'installe.
Diane n'a plus Ă retrouver dans quelles pages traĂźne une vieille copie.
2 · LE SOMMAIRE NE MONTRAIT AUCUNE HIĂRARCHIE. Diane : « il ne prend pas en
compte la taille des titres donc tout est Ă la mĂȘme taille. Faudrait pouvoir
voir la diffĂ©rence avec les titres en gras â moi ça correspond Ă mes
intitulés car je me sers quasi jamais de la fonction "titre". »
Donc deux choses : le GRAS d'un paragraphe devient un titre (c'est sa façon
d'Ă©crire), et le niveau ne vient plus de la balise mais de la TAILLE RĂELLE
du titre dans la page â ce que l'Ćil voit, le sommaire le voit.
3 · LES TITRES COLLĂS. « La FrĂ©quence GĂ©omĂ©trique des Motset plus encore » :
textContent recolle deux blocs sans espace. On prend la PREMIĂRE LIGNE de
innerText, qui suit le rendu.
"""
import sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:80])
h = h.replace(avant, apres); n += 1
# ââ l'entĂȘte ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
pose(""" SOMMAIRE AUTOMATIQUE + RECHERCHE + RETOUR EN HAUT · aMi V3
26 septembre 2026 â le pied voit dans le cadre, et ne double plus le â°.""",
""" SOMMAIRE AUTOMATIQUE + RECHERCHE + RETOUR EN HAUT · aMi V4
27 septembre 2026 â il remplace les vieilles copies, et le sommaire
retrouve ses niveaux (le gras compte comme un titre).""")
# ââ 1 · la reprise de place âââââââââââââââââââââââââââââââââââââââââââââââââââ
pose(""" if (window.__amiToc) return; /* collé deux fois : une seule vie */
window.__amiToc = true;""",
""" /* ââ QUI TIENT LA PLACE ? âââââââââââââââââââââââââââââââââââââââââ
Une vieille copie collée dans une page (la V1, par exemple) arrive avant
le pied du site et posait __amiToc : le pied se retirait, et la page
restait sans loupe. Désormais on se numérote : à version égale ou plus
récente on laisse la place, sinon ON LA PREND. */
var MOI = 4;
if (window.__amiTocV >= MOI) return; /* déjà nous, ou plus neuf */
if (window.__amiToc && !(window.__amiTocV >= MOI)) { /* une plus ancienne : on la remplace */
['.amiToc-out', '.amiToc-tiroir', '.amiRech'].forEach(function(s){
[].slice.call(document.querySelectorAll(s)).forEach(function(e){ e.remove(); });
});
if (window.console) console.log('[aMi · pied] une version plus ancienne était posée : la V' + MOI + ' prend la place.');
}
window.__amiToc = true;
window.__amiTocV = MOI;""")
# ââ 2 · les candidats au sommaire : le gras compte, la taille dĂ©cide âââââââââ
pose(""" var R = document.documentElement;
var TITRES = 'h1, h2, h3, h4, [data-toc], .fam, .sechead';
var NIVEAU = { H1: 2, H2: 2, H3: 3, H4: 3 };""",
""" var R = document.documentElement;
var TITRES = 'h1, h2, h3, h4, [data-toc], .fam, .sechead';
var NIVEAU = { H1: 2, H2: 2, H3: 3, H4: 3 };
/* ââ CE QUI FAIT UN TITRE ââââââââââââââââââââââââââââââââââââââââââ
Diane écrit ses intitulés en GRAS, pas avec la fonction « titre ».
Un gras devient un titre quand il tient seul sa ligne : il porte
presque tout le texte de son paragraphe, il est court, et il n'est
pas un mot mis en avant au milieu d'une phrase. */
function grasTitres(z){
var out = [];
[].slice.call(z.querySelectorAll('strong, b')).forEach(function(g){
if (g.closest('.amiToc-tiroir, .amiRech, .amiToc-out, table, nav, header, footer')) return;
var p = g.parentElement;
if (!p) return;
var t = (g.textContent || '').replace(/\\s+/g, ' ').trim();
if (t.length < 3 || t.length > 90) return;
var tp = (p.textContent || '').replace(/\\s+/g, ' ').trim();
if (!tp.length) return;
/* Rien d'écrit AVANT lui dans le paragraphe : un gras au milieu d'une
phrase n'est pas un intitulĂ©. Et il doit peser dans son paragraphe â
un chapĂŽ en gras suivi d'une longue phrase n'en est pas un non plus. */
var avant = '';
for (var k = 0; k < p.childNodes.length; k++) {
var nd = p.childNodes[k];
if (nd === g || (nd.contains && nd.contains(g))) break;
avant += (nd.textContent || '');
}
if (avant.replace(/\s+/g, '').length > 0) return;
if (t.length / tp.length < 0.35) return;
if (p.matches(TITRES)) return; /* déjà un vrai titre */
if (!/^(P|DIV|LI|SPAN|SECTION|ARTICLE)$/.test(p.tagName)) return;
out.push(g);
});
return out;
}
/* L'Ă©tiquette : la PREMIĂRE LIGNE de ce qu'on voit. textContent recolle
deux blocs sans espace (« des Motset plus encore ») ; innerText suit le
rendu, donc la coupe est celle de l'Ćil. */
function etiquette(el){
var t = (el.innerText || el.textContent || '').split('\\n')[0];
return t.replace(/\\s+/g, ' ').trim();
}
/* Le niveau vient de la TAILLE RĂELLE, pas de la balise : ce que l'Ćil
distingue, le sommaire le distingue. La plus grande taille fait les
groupes ; en dessous, ce sont des enfants. */
function candidats(z){
var l = [].slice.call(z.querySelectorAll(TITRES)).concat(grasTitres(z));
l = l.filter(function(e, i){ return l.indexOf(e) === i; });
l.sort(function(a, b){
var p = a.compareDocumentPosition(b);
if (p & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
if (p & Node.DOCUMENT_POSITION_PRECEDING) return 1;
return 0;
});
var max = 0;
l.forEach(function(e){
var s = parseFloat(getComputedStyle(e).fontSize) || 0;
e.__amiT = s; if (s > max) max = s;
});
l.forEach(function(e){
e.__amiN = (e.tagName === 'H1' || e.tagName === 'H2') ? 2
: (e.__amiT >= max - 0.6 ? 2 : 3);
});
return l;
}""")
pose(""" [].slice.call(z.querySelectorAll(TITRES)).forEach(function(h){
if (h.closest('.amiToc-tiroir, .amiRech')) return;
if (h.querySelector(TITRES)) return;
var t = (h.textContent || '').replace(/\\s+/g, ' ').trim();""",
""" candidats(z).forEach(function(h){
if (h.closest('.amiToc-tiroir, .amiRech')) return;
if (h.querySelector(TITRES)) return;
var t = etiquette(h);""")
pose(""" var niv = NIVEAU[h.tagName] || (h.hasAttribute('data-toc') ? 2 : 3);""",
""" var niv = h.__amiN || NIVEAU[h.tagName] || (h.hasAttribute('data-toc') ? 2 : 3);""")
# ââ 3 · le tiroir montre ses niveaux âââââââââââââââââââââââââââââââââââââââââ
pose(""".amiToc-tiroir a.n3{font-size:.78rem;color:var(--gris,#888898)}""",
""".amiToc-tiroir a.n3{font-size:.78rem;font-weight:300;color:var(--gris,#888898)}
/* La hiérarchie se voit : un groupe pÚse plus que ses enfants (27/09). */
.amiToc-g > a{font-size:.95rem;font-weight:500;color:var(--nacre,#f2ede2);letter-spacing:.005em}
.amiToc-g > a:hover{color:var(--or-pale,#e6d090)}
.amiToc-enfants > a{padding-left:.75rem}""")
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', n, 'opĂ©rations,', len(h) // 1024, 'Ko')
p7_charte_v13.py python3 p7_charte_v13.py c12.html c13.html126 lignes
Charte V13 : épaissit au rendu le Cormorant italique léger, rend les mots à définition à la couleur de leur texte (pointillé seul), et donne à la barre des huit parties un bouton qui l'enroule.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Charte V13 â trois retours de Diane du 27/09 au soir.
1 · LE SERIF ITALIQUE SE LIT MAL. « La Poésie, Mon Art de Vivre » : du Cormorant
italique, clair, fin, sur du noir â les dĂ©liĂ©s disparaissent. On l'Ă©paissit.
Pas par une liste de classes (la V11 a montré ce que ça coûte) : on cherche
au rendu tout ce qui est en serif ET en italique, et on lui donne du corps.
2 · LES MOTS Ă DĂFINITION NE CHANGENT PLUS DE COULEUR. [stated] « je veux juste
qu'ils soient juste soulignés, pas qu'ils changent de couleur en ambre. »
Le pointillĂ© reste â c'est lui qui dit « il y a une dĂ©finition dessous » â
mais il prend la couleur du texte, et le mot garde la sienne.
3 · LES ONGLETS S'ENROULENT. Le â€ïžâđ„ ne replie que la barre d'architecture ;
la barre des huit parties, elle, n'avait aucun moyen de se refermer â trois
rangées au téléphone, et rien pour les rendre. On lui donne son bouton :
enroulée, elle ne montre que la partie ouverte ; déroulée, les huit.
"""
import sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:80])
h = h.replace(avant, apres); n += 1
BLOC = r'''<style id="ami-charte-lisibilite-v13">
/* ââ 1 · LE SERIF ITALIQUE PREND DU CORPS ââââââââââââââââââââââââââââââââ
Posé par le script ci-dessous sur ce qui est VRAIMENT rendu en Cormorant
italique : on ne devine pas une liste de classes, on regarde. */
#ami-charte .ami-it-lisible{font-weight:600!important}
/* ââ 2 · LE MOT Ă DĂFINITION EST JUSTE SOULIGNĂ ââââââââââââââââââââââââââ
Diane : « je veux juste qu'ils soient juste soulignés, pas qu'ils changent
de couleur en ambre. » Le pointillé garde son métier (il annonce la
définition au survol) mais il emprunte la couleur du texte. */
#ami-charte .df,
#ami-charte #tv .df,
#ami-charte .tvx .df{color:inherit!important;font-weight:inherit!important;
border-bottom:1px dotted currentColor!important;opacity:1}
/* ââ 3 · LA BARRE DES HUIT PARTIES S'ENROULE âââââââââââââââââââââââââââââ
Le â€ïžâđ„ ne replie que la barre d'architecture. Celle-ci n'avait rien. */
#ami-charte .bar{position:relative}
#ami-charte .bar .plier{position:absolute;top:2px;right:4px;z-index:3;
width:30px;height:30px;padding:0;line-height:28px;text-align:center;
background:rgba(9,9,14,.72);border:1px solid var(--or-somb,#7a611a);border-radius:50%;
color:var(--or,#c8a84a);font:400 13px/28px Jost,-apple-system,sans-serif;
cursor:pointer;transition:color .16s,border-color .16s,transform .2s}
#ami-charte .bar .plier:hover{color:var(--or-pale,#e6d090);border-color:var(--or,#c8a84a)}
#ami-charte .bar .plier:focus-visible{outline:2px solid var(--or,#c8a84a);outline-offset:2px}
#ami-charte .bar.enroulee .ong:not(.on){display:none}
#ami-charte .bar.enroulee{padding-right:40px}
#ami-charte .bar.enroulee .ong.on{margin:0 auto}
#ami-charte .bar.enroulee .plier{transform:rotate(180deg)}
</style>
<script id="ami-charte-lisibilite-v13-js">
/* Ce bloc vit dans le <head> : le corps n'existe pas encore. On attend. */
function amiCharteV13(){
'use strict';
var R = document.getElementById('ami-charte'); if(!R) return;
/* 1 · le serif italique : on le cherche au rendu, on ne le devine pas. */
function epaissisLItalique(){
var n = 0;
var tous = R.querySelectorAll('p,li,dd,dt,blockquote,em,i,span,div,figcaption,h1,h2,h3,h4,h5,h6,small,td,th');
[].forEach.call(tous, function(e){
if (e.classList.contains('ami-it-lisible')) return;
if (e.closest('.amiToc-tiroir, .amiRech, .plier')) return;
var c = getComputedStyle(e);
if (c.fontStyle !== 'italic') return;
/* PIĂGE : « Jost, -apple-system, sans-serif » CONTIENT « serif ». On ĂŽte
d'abord les « sans-serif », sinon toute la page passe en gras (mesuré :
2 631 éléments au lieu d'une centaine). */
var fam = (c.fontFamily || '').replace(/sans-serif/gi, '');
if (!/Cormorant|Garamond|Georgia|Times|(^|[\s,"'])serif/i.test(fam)) return;
if (parseInt(c.fontWeight, 10) >= 500) return;
var t = (e.textContent || '').trim();
if (!t || t.length > 400) return; /* un long paragraphe reste léger */
e.classList.add('ami-it-lisible'); n++;
});
return n;
}
epaissisLItalique();
[600, 1800, 3500].forEach(function(ms){ setTimeout(epaissisLItalique, ms); });
R.addEventListener('click', function(){ setTimeout(epaissisLItalique, 120); });
/* 3 · la barre des huit parties s'enroule */
var bar = R.querySelector('.bar');
if (bar && !bar.querySelector('.plier')) {
var b = document.createElement('button');
b.type = 'button'; b.className = 'plier';
b.setAttribute('aria-controls', 'ami-charte-barre-parties');
b.setAttribute('aria-expanded', 'true');
b.title = 'Enrouler les huit parties';
b.setAttribute('aria-label', 'Enrouler les huit parties');
b.textContent = 'âŽ';
bar.id = bar.id || 'ami-charte-barre-parties';
b.addEventListener('click', function(){
var ferme = bar.classList.toggle('enroulee');
b.setAttribute('aria-expanded', ferme ? 'false' : 'true');
b.title = b.getAttribute('aria-label') === null ? '' : (ferme ? 'Dérouler les huit parties' : 'Enrouler les huit parties');
b.setAttribute('aria-label', ferme ? 'Dérouler les huit parties' : 'Enrouler les huit parties');
});
bar.appendChild(b);
/* en changeant de partie quand elle est enroulée, on montre la nouvelle */
bar.addEventListener('click', function(e){
var o = e.target.closest ? e.target.closest('.ong') : null;
if (o && bar.classList.contains('enroulee')) setTimeout(function(){ /* .on suit */ }, 0);
});
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', amiCharteV13);
else amiCharteV13();
</script>
</head>'''
pose('</head>', BLOC)
h = h.replace('collant vivant V12 â 27 septembre 2026',
'lisibilitĂ© et enroulement V13 â 27 septembre 2026', 1)
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', len(h)//1024, 'Ko')
p8_charte_v14.py python3 p8_charte_v14.py c13.html c14.html306 lignes
Charte V14 : goutte pleine, marges du tĂ©lĂ©phone, â â âą, barre enroulĂ©e sur une ligne, tableaux dĂ©pilĂ©s en fiches et sans dĂ©filement vertical, tableau du mot en trois colonnes avec ce qui a grandi, lexique non collant et recomptĂ©, â° avec symbole et « tout dĂ©plier ».
â§ Copier le script
# -*- coding: utf-8 -*-
"""Charte V14 â les douze retours de Diane du 27/09 au soir, plus le fond.
Chaque point est numéroté comme elle les a dits. Ce qui est mesuré est écrit
avec son chiffre ; ce qui ne l'est pas est dit tel quel.
"""
import re, sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:80])
h = h.replace(avant, apres); n += 1
# âââ 3 · LES CARRĂS DEVIENNENT DES PUCES RONDES ââââââââââââââââââââââââââââââ
# â sert de puce 956 fois dans la page et 156 fois en ::before. Diane :
# « tous les carrĂ©s deviennent des ronds (des puces) : ⹠». On garde le â
# LĂ OĂ IL EST UN SIGNE DE LA CHARTE : le registre â Essentialiste.
h = h.replace('â Essentialiste', '\x00ESS\x00')
h = h.replace('class="r-ess">â', 'class="r-ess">\x00ESS2\x00')
avant_puces = h.count('â')
h = h.replace('content:"â"', 'content:"âą"').replace("content:'â'", "content:'âą'")
h = h.replace('â', 'âą')
h = h.replace('\x00ESS\x00', 'â Essentialiste').replace('\x00ESS2\x00', 'â')
n += 1
print(f' 3 · {avant_puces} losanges â passĂ©s en puces âą (le registre â Essentialiste est gardĂ©)')
# âââ 9 · LA TABLE DE TRADUCTION PREND SON NOM ââââââââââââââââââââââââââââââââ
# [stated] « on pourrait plutĂŽt appeler ça "La Table de Traduction â La Pierre
# & Sa Couleur" ^^ ». Le titre vit deux fois : dans le sommaire et sur la table.
AV = 'La table de traduction â l\u2019ancien nom, la pierre'
AP = 'La Table de Traduction â La Pierre & Sa Couleur'
combien = h.count(AV)
assert combien >= 1, 'titre de la table de traduction introuvable'
h = h.replace(AV, AP); n += 1
print(f' 9 · titre de la table de traduction changé en {combien} endroit(s)')
# âââ 8 + LE FOND · « LE MOT » EN TROIS COLONNES, ET CE QUI A GRANDI ââââââââââ
# Diane : « on divise la premiĂšre colonne en 2 » â le mot et son mot de secours
# vont ensemble, c'est la mĂȘme idĂ©e Ă deux Ăąges. Et elle ajoute le fond :
# l'ĂcoLogie sous l'ĂcoFĂ©minisme, l'Internet sous la MĂ©taConScience.
TABLE_AVANT = h[h.find('<table class="t">\n<thead><tr><th>le mot</th>'):]
TABLE_AVANT = TABLE_AVANT[:TABLE_AVANT.find('</table>') + 8]
assert TABLE_AVANT.startswith('<table'), 'table « le mot » introuvable'
TABLE_APRES = '''<table class="t t-mot">
<thead><tr><th>le mot · et son mot de secours</th><th>ce que lâusage en a fait</th><th>ce qui sâest perdu â et ce qui a grandi</th></tr></thead>
<tbody>
<tr class="tc" style="--c1:var(--amazonite)">
<td class="nom"><span class="m-1">đ§ <span translate="no">ConScience</span></span><span class="m-fl">â</span><span class="m-2"><span translate="no">MĂ©taConScience</span></span></td>
<td>lâusage appelle « conscience » lâinstinct de survie â câest-Ă -dire la <span translate="no"><span class="df" tabindex="0" data-d="proto- â le premier, lâavant â et ConScience. Lâinstinct de survie : ce quâon nomme lâEgo. Câest le socle, et il est nĂ©cessaire : câest sur lui que lâĂco vient grandir, quand lâEgo est assez serein pour apprendre. · đ đ Lexique du Vrai">ProtoConScience</span></span>.</td>
<td class="fin">la <span translate="no">ConScience</span> est <em>la Science du Nous</em>, celle des <span translate="no">CoNaissances</span> partagĂ©es. <b>Le mot de secours nomme ce que le premier disait dĂ©jĂ .</b><span class="m-gr"><b>Ce qui a grandi depuis :</b> la NoosphĂšre MatĂ©rialisĂ©e â <b>lâInternet</b>.</span></td></tr>
<tr class="tc" style="--c1:var(--grenat)">
<td class="nom"><span class="m-1">â FĂ©minisme</span><span class="m-fl">â</span><span class="m-2"><span translate="no"><span class="df" tabindex="0" data-d="Un mot de secours, pas un mot de plus. Le fĂ©minisme est par essence de lâĂ©cofĂ©minisme â le prĂ©fixe ne rajoute pas une doctrine : il rĂ©pare une rĂ©putation. · đ đ Lexique du Vrai">ĂcoFĂ©minisme</span></span></span></td>
<td>employĂ© sans sa dĂ©finition, jusquâĂ ce que la connotation rende le mot difficile Ă porter.</td>
<td class="fin"><b>le fĂ©minisme est par essence de lâ<span translate="no">ĂcoFĂ©minisme</span></b> â <em>le prĂ©fixe nâajoute rien : il rĂ©pare une rĂ©putation.</em><span class="m-gr"><b>Ce qui a grandi depuis :</b> lâEnvironnement Conceptuel â <b>lâĂcoLogie</b>.</span></td></tr>
</tbody></table>
<p class="m-loi">Le mot de secours ne renie pas le premier : <b>il redit ce que le premier disait dĂ©jĂ </b> â Ă la taille que nous avons prise depuis. Entre-temps le monde a grandi, et le mot avec lui : câest le mĂȘme sens, devenu assez vaste pour contenir ce qui nâexistait pas encore quand on lâa dit la premiĂšre fois.</p>'''
h = h.replace(TABLE_AVANT, TABLE_APRES); n += 1
# âââ LE BLOC DE STYLE ET DE SCRIPT âââââââââââââââââââââââââââââââââââââââââââ
BLOC = r'''<style id="ami-charte-v14">
/* ââ 1 · LA GOUTTE SE REMPLIT, LE FOND NE S'ALLUME PAS ââââââââââââââââââ
La Charte l'écrivait déjà dans son propre tableau des états ⠫ goutte en
trait â goutte pleine » â mais la rĂšgle n'Ă©tait appliquĂ©e qu'au â. */
#ami-charte #amiLecture[aria-pressed="true"]{background:transparent!important;box-shadow:none!important}
#ami-charte #amiLecture[aria-pressed="true"] svg{fill:currentColor!important}
#ami-charte #amiLecture svg{fill:none;stroke:currentColor;stroke-width:1.6}
/* ââ 2 · LE TĂLĂPHONE REND DE LA LARGEUR AU TEXTE âââââââââââââââââââââââ
Mesuré à 390 : 24 px de marge de chaque cÎté laissaient 326 px au texte.
Ă 12 px il en reste 350 â quatorze caractĂšres de plus par ligne. */
@media (max-width:560px){
#ami-charte main,#ami-charte .pan,#ami-charte .une{padding-left:12px!important;padding-right:12px!important}
#ami-charte .ec,#ami-charte .bk,#ami-charte .enc{padding-left:.7rem!important;padding-right:.7rem!important}
}
/* ââ 5 · LES TROIS BARRES SE TOUCHENT âââââââââââââââââââââââââââââââââââ
Un liseré d'un pixel entre deux barres collées laisse passer le texte. */
#ami-charte .bar{margin-top:-1px}
#ami-charte .sub-host,#ami-charte .sbar{margin-top:-1px}
/* ââ 4 · ENROULĂE, LA BARRE TIENT SUR UNE LIGNE âââââââââââââââââââââââââ
« âïž La Communication · ce que c'est », centrĂ©. */
#ami-charte .bar.enroulee{display:flex!important;align-items:center;justify-content:center;
min-height:0;padding:.35rem 40px .35rem 12px}
#ami-charte .bar.enroulee .ong.on{display:flex!important;flex-direction:row!important;
align-items:baseline;gap:.5rem;width:auto;max-width:100%;margin:0;padding:.15rem .4rem;
border:0;background:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#ami-charte .bar.enroulee .ong.on .og{font-size:1em;margin:0}
#ami-charte .bar.enroulee .ong.on .ot{font-size:.95rem}
#ami-charte .bar.enroulee .ong.on .os::before{content:"· ";opacity:.75}
#ami-charte .bar.enroulee .ong.on .os{font-size:.72rem;letter-spacing:.06em}
/* ââ 7 · AUCUN TABLEAU NE DĂFILE VERTICALEMENT ââââââââââââââââââââââââââ
Mesuré : cinq rouleaux verticaux, dont un de 658 px pour 8 259 px de
contenu. Un tableau qui défile dans la page ne se lit pas : il se devine. */
#ami-charte .tw,#ami-charte .tw.longue,#ami-charte .scroll{max-height:none!important}
/* overflow-x:auto force overflow-y à « auto » : on ne peut pas mélanger les
deux axes. Le script rend donc « visible » aux rouleaux dont le tableau
tient déjà , et le téléphone les libÚre tous (les tableaux s'y dépilent). */
@media (max-width:560px){
#ami-charte .tw,#ami-charte .tw.longue,#ami-charte .scroll{overflow:visible!important}
/* .ec table.tbl porte min-width:640px â c'est LUI qui tenait les deux
derniers tableaux Ă 640 px dans un conteneur de 320. */
#ami-charte table.t-fiches{width:100%!important;max-width:100%!important;
min-width:0!important;table-layout:auto!important}
#ami-charte table.t-fiches tr{max-width:100%}
}
/* ââ 6 + 7 · AU TĂLĂPHONE, UN TABLEAU TROP LARGE SE DĂPILE EN FICHES ââââ
Posé par le script sur les seuls tableaux qui débordent vraiment. */
@media (max-width:560px){
#ami-charte table.t-fiches{display:block;width:100%}
#ami-charte table.t-fiches thead{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
#ami-charte table.t-fiches tbody,#ami-charte table.t-fiches tr,#ami-charte table.t-fiches td{display:block;width:auto}
#ami-charte table.t-fiches tr{border:1px solid rgba(200,168,74,.22);border-radius:10px;
margin:.7rem 0;padding:.5rem .7rem;background:rgba(255,255,255,.02)}
#ami-charte table.t-fiches td{border:0!important;padding:.35rem 0!important;text-align:left!important}
#ami-charte table.t-fiches td+td{border-top:1px solid rgba(200,168,74,.12)!important}
#ami-charte table.t-fiches td[data-l]::before{content:attr(data-l);display:block;
font:500 .64rem/1.5 Jost,-apple-system,sans-serif;letter-spacing:.14em;text-transform:uppercase;
color:var(--gris,#888898);margin-bottom:.15rem}
#ami-charte table.t-fiches td:empty{display:none}
/* Un td en display:block peut ENCORE forcer la largeur si son contenu
refuse de se couper (nowrap, un long jeton de code). Mesuré : deux
tableaux restaient Ă 640 px dans un conteneur de 320. */
#ami-charte table.t-fiches td,#ami-charte table.t-fiches th{white-space:normal!important;
overflow-wrap:anywhere;word-break:break-word}
#ami-charte table.t-fiches td *{white-space:normal!important;max-width:100%}
}
/* ââ 8 · « LE MOT » ET SON MOT DE SECOURS, DANS LA MĂME CASE ââââââââââââ*/
#ami-charte .t-mot td.nom{line-height:1.45}
#ami-charte .t-mot .m-1,#ami-charte .t-mot .m-2{display:block}
#ami-charte .t-mot .m-2{font-weight:600;color:var(--or-pale,#e6d090)}
#ami-charte .t-mot .m-fl{display:block;font-size:.8em;opacity:.55;line-height:1.1}
#ami-charte .m-gr{display:block;margin-top:.5rem;padding-top:.45rem;
border-top:1px dashed rgba(200,168,74,.28);font-size:.94em}
#ami-charte .m-loi{margin:.9rem 0 1.4rem;padding:.9rem 1rem;
border-left:3px solid var(--or-somb,#7a611a);border-radius:0 8px 8px 0;
background:rgba(200,168,74,.05);font-size:.98em;line-height:1.65}
/* ââ 10 · LES OUTILS DU LEXIQUE NE COLLENT PLUS âââââââââââââââââââââââââ*/
#ami-charte .lxq #ami-lexique .tools{position:static!important;top:auto!important}
/* ââ 12 · LE â° : SON SYMBOLE, ET « TOUT DĂPLIER » âââââââââââââââââââââââ*/
#ami-charte .ami-toc .toc-tout{display:block;width:calc(100% - 1rem);margin:.2rem .5rem .6rem;
padding:.4rem .6rem;background:rgba(200,168,74,.08);border:1px solid rgba(200,168,74,.28);
border-radius:7px;color:var(--or,#c8a84a);cursor:pointer;
font:500 .72rem/1.4 Jost,-apple-system,sans-serif;letter-spacing:.14em;text-transform:uppercase}
#ami-charte .ami-toc .toc-tout:hover{background:rgba(200,168,74,.16);color:var(--or-pale,#e6d090)}
#ami-charte .ami-toc .toc-em{margin-right:.4rem}
/* « ILes 8 Arts Libéraux » : le chiffre romain collait au titre. */
#ami-charte .ami-toc .tc-n{margin-right:.5rem;opacity:.72;font-variant-numeric:tabular-nums}
#ami-charte .ami-toc .ami-toc-t .toc-em{font-size:1.05em}
</style>
<script id="ami-charte-v14-js">
function amiCharteV14(){
'use strict';
var R = document.getElementById('ami-charte'); if(!R) return;
/* 6+7 · les tableaux qui débordent se dépilent en fiches au téléphone.
On ne touche QUE ceux qui débordent vraiment, et on leur donne l'étiquette
de leur colonne pour que la fiche reste lisible. */
function prepareLesTableaux(){
[].forEach.call(R.querySelectorAll('table'), function(t){
if (t.dataset.amiFiche) return;
var tetes = [].map.call(t.querySelectorAll('thead th'), function(x){
return (x.textContent || '').replace(/\s+/g, ' ').trim();
});
if (tetes.length < 3) return; /* deux colonnes tiennent toujours */
t.dataset.amiFiche = '1';
[].forEach.call(t.querySelectorAll('tbody tr'), function(tr){
[].forEach.call(tr.children, function(td, i){
if (tetes[i] && !td.hasAttribute('data-l')) td.setAttribute('data-l', tetes[i]);
});
});
});
}
function dePileCeQuiDeborde(){
if (window.innerWidth > 560) {
[].forEach.call(R.querySelectorAll('table.t-fiches'), function(t){ t.classList.remove('t-fiches'); });
return;
}
[].forEach.call(R.querySelectorAll('table'), function(t){
if (t.classList.contains('t-fiches')) return;
if (!t.querySelector('thead th')) return;
var large = t.scrollWidth > t.clientWidth + 2 || t.getBoundingClientRect().width > window.innerWidth - 8;
if (large) t.classList.add('t-fiches');
});
}
function libereLesRouleaux(){
[].forEach.call(R.querySelectorAll('.tw, .scroll'), function(w){
var t = w.querySelector('table'); if (!t) return;
var tient = t.scrollWidth <= w.clientWidth + 2;
w.style.overflow = tient ? 'visible' : '';
if (tient) w.style.maxHeight = 'none';
});
}
prepareLesTableaux(); dePileCeQuiDeborde(); libereLesRouleaux();
window.addEventListener('resize', function(){ function libereLesRouleaux(){
[].forEach.call(R.querySelectorAll('.tw, .scroll'), function(w){
var t = w.querySelector('table'); if (!t) return;
var tient = t.scrollWidth <= w.clientWidth + 2;
w.style.overflow = tient ? 'visible' : '';
if (tient) w.style.maxHeight = 'none';
});
}
prepareLesTableaux(); dePileCeQuiDeborde(); libereLesRouleaux(); });
[700, 2000, 4000].forEach(function(ms){ setTimeout(function(){ function libereLesRouleaux(){
[].forEach.call(R.querySelectorAll('.tw, .scroll'), function(w){
var t = w.querySelector('table'); if (!t) return;
var tient = t.scrollWidth <= w.clientWidth + 2;
w.style.overflow = tient ? 'visible' : '';
if (tient) w.style.maxHeight = 'none';
});
}
prepareLesTableaux(); dePileCeQuiDeborde(); libereLesRouleaux(); }, ms); });
R.addEventListener('click', function(){ setTimeout(function(){ function libereLesRouleaux(){
[].forEach.call(R.querySelectorAll('.tw, .scroll'), function(w){
var t = w.querySelector('table'); if (!t) return;
var tient = t.scrollWidth <= w.clientWidth + 2;
w.style.overflow = tient ? 'visible' : '';
if (tient) w.style.maxHeight = 'none';
});
}
prepareLesTableaux(); dePileCeQuiDeborde(); libereLesRouleaux(); }, 200); });
/* 10 · le compte du lexique. Mesuré : 175 entrées dans la page, « 169 »
affiché. On redit le vrai nombre. */
function recompteLeLexique(){
var lx = R.querySelectorAll('#ami-lexique .lx, #ami-lexique .fiche');
if (!lx.length) return;
var vus = 0;
[].forEach.call(lx, function(e){ if (e.offsetParent !== null || e.getClientRects().length) vus++; });
[].forEach.call(R.querySelectorAll('#ami-lexique *'), function(e){
if (e.children.length) return;
var t = (e.textContent || '').trim();
var m = t.match(/^(\d+)\s+(TERMES? AFFICH\S+)\s*\/\s*(\d+)$/i);
if (!m) return;
e.textContent = vus + ' ' + m[2] + ' / ' + lx.length;
});
}
recompteLeLexique();
[900, 2600].forEach(function(ms){ setTimeout(recompteLeLexique, ms); });
/* 12 · le Ⱐ: chaque partie porte son symbole, le numéro se décolle,
et un « tout déplier » coiffe le tiroir. */
function soigneLeSommaire(){
var d = R.querySelector('.ami-toc'); if (!d) return;
/* le symbole de la partie ouverte, pris sur son onglet */
var actif = R.querySelector('.bar .ong.on .og');
var em = actif ? (actif.textContent || '').trim() : '';
/* Le tiroir liste les chapitres de la partie ouverte : le symbole va donc
sur le NOM de la partie, en tĂȘte du tiroir â et sur toute entrĂ©e qui
désigne une partie, s'il y en a. */
var titre = d.querySelector('.ami-toc-t');
if (em && titre && !titre.querySelector('.toc-em')) {
var s0 = document.createElement('span'); s0.className = 'toc-em'; s0.textContent = em;
titre.insertBefore(s0, titre.firstChild);
}
[].forEach.call(d.querySelectorAll('a[data-o], button[data-o]'), function(a){
if (a.querySelector('.toc-em')) return;
var ong = R.querySelector('.bar .ong[data-o="' + a.dataset.o + '"] .og');
if (!ong) return;
var s1 = document.createElement('span'); s1.className = 'toc-em';
s1.textContent = (ong.textContent || '').trim();
a.insertBefore(s1, a.firstChild);
});
if (!d.querySelector('.toc-tout')) {
var b = document.createElement('button');
b.type = 'button'; b.className = 'toc-tout'; b.textContent = 'â tout dĂ©plier';
b.addEventListener('click', function(){
var plis = d.querySelectorAll('[aria-expanded]');
var ouvrir = b.dataset.etat !== 'ouvert';
[].forEach.call(plis, function(p){
if (p === b) return;
var estOuvert = p.getAttribute('aria-expanded') === 'true';
if (estOuvert !== ouvrir) p.click();
});
b.dataset.etat = ouvrir ? 'ouvert' : 'ferme';
b.textContent = ouvrir ? 'â tout replier' : 'â tout dĂ©plier';
});
d.insertBefore(b, d.firstChild);
}
}
var som = document.getElementById('amiSommaire');
if (som) som.addEventListener('click', function(){ setTimeout(soigneLeSommaire, 160); });
setTimeout(soigneLeSommaire, 1500);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', amiCharteV14);
else amiCharteV14();
</script>
</head>'''
pose('</head>', BLOC)
h = h.replace('lisibilitĂ© et enroulement V13 â 27 septembre 2026',
'les douze retours V14 â 27 septembre 2026', 1)
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', n, 'opĂ©rations,', len(h)//1024, 'Ko')
p9_charte_v15.py python3 p9_charte_v15.py c14.html c15.html228 lignes
Charte V15 : marges rendues par une passe mesurĂ©e, pictos sans halo (goutte ambre, cercle plein, â discret, zoom ambre), sous-titre de barre au-dessus du seuil en contraste, ligne de une sur deux lignes sans version, date par la mĂ©ta, et la navigation prĂ©cĂ©dente/suivante au bout de chaque partie.
â§ Copier le script
# -*- coding: utf-8 -*-
"""Charte V15 â les neuf retours de Diane du 27/09, 21h.
Ce qui est écrit ici a été mesuré d'abord. Les deux mesures qui commandent
tout le reste :
· LES MARGES S'EMPILENT. Ă 390 px, six niveaux se succĂšdent â
main 374 (12) â .pan 350 (12) â .ec 326 (11) â section 272 (16+20)
â article 230 (19) â p 189. Le texte le plus intĂ©rieur tient sur
189 px, soit vingt-quatre signes par ligne : c'est la « colonne »
que Diane voit. Ailleurs, le mĂȘme texte a 326 px. D'oĂč son mot juste :
« y'a des endroits oĂč la marge fait plus le tĂ©lĂ©phone, et y'a des
endroits ça fait colonne donc c'est pas tout partout ».
· LE SOUS-TITRE DE LA BARRE TOMBE SOUS LE SEUIL. En mode contraste,
« La Création du Sanctuaire » est à 7,6:1 et « Les Outils Réflectifs :
Diagnostic PrĂ©ventif » Ă 3,9:1 â sous les 4,5:1 de la Charte. Diane
l'a vu Ă l'Ćil nu.
"""
import sys
src, dst = sys.argv[1], sys.argv[2]
h = open(src, encoding='utf-8').read()
n = 0
def pose(avant, apres, combien=1):
global h, n
assert h.count(avant) == combien, (h.count(avant), avant[:80])
h = h.replace(avant, apres); n += 1
# âââ 1 · SA PHRASE, DANS SES MOTS ââââââââââââââââââââââââââââââââââââââââââââ
pose('Ă la taille que nous avons prise depuis',
'à la taille que nous avons développée depuis')
# âââ 8 · LA DATE ET LA VERSION âââââââââââââââââââââââââââââââââââââââââââââââ
pose('<span data-ami="date">26 septembre 2026</span>',
'<span data-ami="date">27 septembre 2026</span>')
# PIĂGE : un script recopie les <meta ami:*> dans les [data-ami] au chargement.
# Changer le texte ne suffit pas â c'est la mĂ©ta qui fait foi.
pose('<meta name="ami:date" content="2026-09-26">',
'<meta name="ami:date" content="2026-09-27">', 2)
# âââ 8 bis · LA LIGNE DE LA UNE : DEUX LIGNES, SANS LA VERSION ââââââââââââââ
# Elle est écrite trois fois (la une, le titre du document, l'export).
AV = ('<div class="c-vol">Protocole de <span translate="no">ConScience</span> '
'<span translate="no">aMi</span> â Charte de Communication · '
'<span data-ami="version">V1</span></div>')
AP = ('<div class="c-vol">Protocole de <span translate="no">ConScience</span> '
'<span translate="no">aMi</span><span class="c-vol-br"></span>'
'Charte de Communication</div>')
assert h.count(AV) == 1, h.count(AV)
h = h.replace(AV, AP); n += 1
print(' 8 · ligne de la une : la version Îtée (elle est déjà au pied), coupure posée')
BLOC = r'''<style id="ami-charte-v15">
/* ââ 2 · LES MARGES NE S'EMPILENT PLUS AU TĂLĂPHONE âââââââââââââââââââââ
Mesuré à 390 : six niveaux de retrait menaient un paragraphe à 189 px.
On garde UN retrait â celui de la partie â et les conteneurs intĂ©rieurs
se contentent d'un filet. Aucun bloc de texte ne descend plus sous 300 px. */
@media (max-width:560px){
#ami-charte .pan section,#ami-charte .pan article,#ami-charte .pan .ec,
#ami-charte .pan .enc,#ami-charte .pan .bk,#ami-charte .pan .portail,
#ami-charte .pan blockquote,#ami-charte .pan .card,#ami-charte .pan .cards > *,
#ami-charte .pan [class*="-intro"],#ami-charte .pan [class*="-grid"] > *{
padding-left:.5rem!important;padding-right:.5rem!important;
margin-left:0!important;margin-right:0!important}
#ami-charte .pan blockquote{padding-left:.8rem!important}
#ami-charte .pan [class*="-grid"]{gap:.6rem!important}
/* l'encart « Emporter » avait déjà la bonne mesure : on s'aligne sur lui */
#ami-charte .pan .emp{padding-left:.9rem!important;padding-right:.9rem!important}
}
/* ââ 3 · LE PLEIN ĂCRAN NE S'ALLUME PLUS ââââââââââââââââââââââââââââââââ
En plein Ă©cran le bouton devient â : un halo dorĂ© au coin d'une page de
lecture attire l'Ćil plus que le texte. Le signe suffit. */
#ami-charte #amiPlein.actif,#ami-charte #tv #amiPlein.actif,#ami-charte .tvx #amiPlein.actif{
background:transparent!important;border-color:rgba(200,168,74,.32)!important;
box-shadow:none!important;color:var(--or)!important}
#ami-charte #amiPlein.actif:hover{background:rgba(200,168,74,.10)!important;border-color:var(--or)!important}
/* ââ 4 · 5 · 6 · LES PICTOS SE COLORENT, LE FOND RESTE NOIR âââââââââââââ
Diane : « pas de halo coloré dans le cercle, juste le picto qui se colore ».
Et la goutte en ambre, comme le reste â elle paraissait blanche. */
#ami-charte .coin[aria-pressed="true"],#ami-charte .ami-coin[aria-pressed="true"]{
background:transparent!important;box-shadow:none!important;
border-color:var(--or)!important;color:var(--or)!important}
#ami-charte #amiLecture[aria-pressed="true"] svg{fill:var(--or)!important;stroke:var(--or)!important}
#ami-charte #amiContraste svg circle{fill:none;stroke:currentColor}
#ami-charte #amiContraste[aria-pressed="true"] svg circle{fill:var(--or)!important;stroke:var(--or)!important}
#ami-charte #amiContraste[aria-pressed="true"] svg path{fill:var(--or)!important;stroke:var(--or)!important}
#ami-charte .coin-zoom .zv{color:var(--or)!important}
/* ââ 7 · LE SOUS-TITRE DE LA BARRE REPASSE LE SEUIL âââââââââââââââââââââ
Mesuré en contraste : titre 7,6:1, sous-titre 3,9:1. La Charte demande
4,5:1. Le sous-titre prend la teinte claire de sa famille. */
#ami-charte.ami-contraste .ami-barre .porte .sub,
#ami-charte.ami-contraste .ami-barre .sub,
#ami-charte.ami-contraste .ami-barre .p-ti + .sub{color:#a9a6b8!important;opacity:1}
/* visé : au-dessus du seuil de 4,5:1, mais SOUS le titre, pour que la
hiérarchie tienne. Le titre est à 7,6:1 ; le sous-titre vise ~6:1. */
/* ââ 8 · LA LIGNE DE LA UNE TIENT SUR DEUX LIGNES âââââââââââââââââââââââ*/
#ami-charte .c-vol .c-vol-br{display:inline}
#ami-charte .c-vol .c-vol-br::before{content:" â "}
@media (max-width:560px){
#ami-charte .c-vol .c-vol-br{display:block;height:0}
#ami-charte .c-vol .c-vol-br::before{content:""}
#ami-charte .c-vol{line-height:1.7}
}
/* ââ 9 · LA PAGE PRĂCĂDENTE ET LA SUIVANTE, AU BOUT DE CHAQUE PARTIE ââââ
Convention prise avec 5.3 Astro : au bout d'un onglet, on passe au
suivant sans remonter. En lecture fluide, tout est déjà à la suite :
la navigation disparaĂźt. */
#ami-charte .nav-pp{display:flex;gap:.6rem;align-items:stretch;justify-content:space-between;
margin:2.4rem 0 1rem;padding-top:1.2rem;border-top:1px solid rgba(200,168,74,.18)}
#ami-charte .nav-pp button{flex:1 1 0;min-height:52px;padding:.55rem .8rem;
background:rgba(255,255,255,.02);border:1px solid rgba(200,168,74,.24);border-radius:10px;
color:var(--gris-cl);cursor:pointer;text-align:left;
font:400 .82rem/1.35 Jost,-apple-system,sans-serif;transition:border-color .16s,color .16s}
#ami-charte .nav-pp button:hover{border-color:var(--or);color:var(--nacre)}
#ami-charte .nav-pp button:focus-visible{outline:2px solid var(--or);outline-offset:2px}
#ami-charte .nav-pp .pp-s{display:block;font-size:.62rem;letter-spacing:.16em;
text-transform:uppercase;color:var(--gris);margin-bottom:.2rem}
#ami-charte .nav-pp .pp-n{display:block;color:var(--or-pale);font-weight:500}
#ami-charte .nav-pp .pp-suiv{text-align:right}
#ami-charte .nav-pp button[disabled]{opacity:.32;cursor:default}
body.ami-une-partie #ami-charte .nav-pp{display:flex}
#ami-charte.ami-fluide .nav-pp{display:none!important}
</style>
<script id="ami-charte-v15-js">
function amiCharteV15(){
'use strict';
var R = document.getElementById('ami-charte'); if(!R) return;
/* 2 · LES MARGES, MESURĂES PLUTĂT QUE DEVINĂES.
Une liste de classes en rate toujours une : on parcourt la partie ouverte
et on rend la largeur Ă tout conteneur qui prend plus de 10 px de retrait
horizontal â sauf les tableaux et les figures, qui ont leurs raisons. */
function rendsLaLargeur(){
if (window.innerWidth > 560) {
[].forEach.call(R.querySelectorAll('[data-ami-marge]'), function(e){
e.style.paddingLeft = ''; e.style.paddingRight = '';
e.style.marginLeft = ''; e.style.marginRight = '';
e.removeAttribute('data-ami-marge');
});
return;
}
var pans = R.querySelectorAll('.pan:not([hidden])');
[].forEach.call(pans, function(pan){
[].forEach.call(pan.querySelectorAll('div,section,article,aside,ul,ol,blockquote,figure'), function(e){
if (e.closest('table, .tw, .scroll, .ami-barre, .bar, .nav-pp')) return;
if (e.classList.contains('emp')) return; /* l'encart Emporter est déjà juste */
var c = getComputedStyle(e);
var pl = parseFloat(c.paddingLeft) || 0, pr = parseFloat(c.paddingRight) || 0;
var ml = parseFloat(c.marginLeft) || 0, mr = parseFloat(c.marginRight) || 0;
if (pl <= 10 && pr <= 10 && ml <= 4 && mr <= 4) return;
e.dataset.amiMarge = '1';
if (pl > 10) e.style.paddingLeft = '8px';
if (pr > 10) e.style.paddingRight = '8px';
if (ml > 4) e.style.marginLeft = '0px';
if (mr > 4) e.style.marginRight = '0px';
});
});
}
rendsLaLargeur();
window.addEventListener('resize', rendsLaLargeur);
[800, 2400].forEach(function(ms){ setTimeout(rendsLaLargeur, ms); });
R.addEventListener('click', function(){ setTimeout(rendsLaLargeur, 220); });
/* 9 · au bout de chaque partie, la précédente et la suivante */
function poseLaNavigation(){
var ongs = [].slice.call(R.querySelectorAll('.bar .ong[data-o]'));
if (ongs.length < 2) return;
ongs.forEach(function(o, i){
var cle = o.dataset.o;
var pan = R.querySelector('.pan[data-o="' + cle + '"]');
if (!pan || pan.querySelector('.nav-pp')) return;
var nav = document.createElement('nav');
nav.className = 'nav-pp';
nav.setAttribute('aria-label', 'Aller à la partie précédente ou suivante');
[[-1, 'pp-prec', 'â partie prĂ©cĂ©dente'], [1, 'pp-suiv', 'partie suivante â']].forEach(function(d){
var v = ongs[i + d[0]];
var b = document.createElement('button');
b.type = 'button'; b.className = d[1];
if (!v) { b.disabled = true; b.innerHTML = '<span class="pp-s">' + d[2] + '</span>'; }
else {
var em = (v.querySelector('.og') || {}).textContent || '';
var nom = (v.querySelector('.ot') || {}).textContent || '';
b.innerHTML = '<span class="pp-s">' + d[2] + '</span><span class="pp-n">' +
em.trim() + ' ' + nom.trim() + '</span>';
b.addEventListener('click', function(){
v.click();
var bar = R.querySelector('.bar');
var y = (bar ? bar.getBoundingClientRect().bottom : 0) + window.pageYOffset;
setTimeout(function(){ window.scrollTo({top: Math.max(0, y - 40), behavior: 'smooth'}); }, 90);
});
}
nav.appendChild(b);
});
pan.appendChild(nav);
});
}
poseLaNavigation();
[900, 2600].forEach(function(ms){ setTimeout(poseLaNavigation, ms); });
/* la lecture fluide efface la navigation : tout est déjà à la suite */
function suisLaLecture(){
var d = document.getElementById('amiLecture');
var fluide = d && d.getAttribute('aria-pressed') === 'true';
R.classList.toggle('ami-fluide', !!fluide);
}
suisLaLecture();
var dl = document.getElementById('amiLecture');
if (dl) dl.addEventListener('click', function(){ setTimeout(suisLaLecture, 60); });
setTimeout(suisLaLecture, 1200);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', amiCharteV15);
else amiCharteV15();
</script>
</head>'''
pose('</head>', BLOC)
h = h.replace('les douze retours V14 â 27 septembre 2026',
'les neuf retours du soir V15 â 27 septembre 2026', 1)
open(dst, 'w', encoding='utf-8').write(h)
print('â', dst, 'â', n, 'opĂ©rations,', len(h)//1024, 'Ko')
đ§” La passe des volumes â 26 octobre 2026
Dans l'ordre oĂč ils se rejouent sur un volume : on AUDITE, puis on pose les meubles, puis les dĂ©finitions, puis le collant â et on VĂRIFIE aprĂšs chaque pose. Le dictionnaire des dĂ©finitions (317 mots relevĂ©s dans le corpus) vit Ă cĂŽtĂ©, dans outils/passe/dictionnaire.json : trop long pour ĂȘtre collĂ© ici, et il se rĂ©gĂ©nĂšre. Les archives d'avant-passe sont dans CONTROLE QUALITE/ARCHIVES/.
audit.js node audit.js volume.html126 lignes
RelĂšve AU RENDU ce qu'un volume porte des points de la passe : barre du haut, pied, pager, accessibilitĂ©, « Emporter » et son repli, bande d'esprit critique et ses groupes, rangĂ©es d'onglets et leurs poignĂ©es, chapitres repliĂ©s, dĂ©finitions, titres, et les tĂ©moins (ancres mortes, dĂ©bordements, erreurs JS). Lecture seule. Sait descendre dans un CADRE HĂTE.
â§ Copier le script
/* audit.js â relĂšve, AU RENDU, ce que chaque volume porte des points de la
passe à la Charte. Lecture seule : il n'écrit jamais dans le volume.
Usage : node audit.js <fichier.html> â une ligne JSON sur la sortie. */
const {chromium}=require('playwright');
(async()=>{
const file=process.argv[2];
const b=await chromium.launch();
const p=await b.newPage({viewport:{width:1280,height:900}});
/* le conteneur n'a pas le droit d'aller chercher les polices : chaque appel
attend puis echoue, ~2 s perdues par page. On les coupe net. */
await p.route('**/*',route=>{
const u=route.request().url();
if(/^file:/.test(u)) return route.continue();
return route.abort();
});
const errs=[];
p.on('pageerror',e=>errs.push(String(e).slice(0,160)));
p.on('console',m=>{if(m.type()==='error'&&!/ERR_TUNNEL|ERR_NAME|net::/.test(m.text()))errs.push('console: '+m.text().slice(0,140));});
try{
await p.goto('file://'+file,{waitUntil:'load',timeout:120000});
}catch(e){ console.log(JSON.stringify({fichier:file,erreur:'chargement : '+String(e).slice(0,120)})); await b.close(); return; }
await p.waitForTimeout(2200);
/* Certains volumes sont des CADRES HOTES : tout le document vit dans un
<iframe srcdoc>. Mesure : 0.1 Apokalypse (#ami-apo-cadre-hote) renvoyait
zero partout. On releve donc dans le cadre le plus garni, pas dans l'hote. */
let cible=p.mainFrame(), mieux=-1;
for(const f of p.frames()){
let k=0; try{ k=await f.evaluate(()=>document.querySelectorAll('*').length); }catch(e){ k=-1; }
if(k>mieux){ mieux=k; cible=f; }
}
const cadre = cible!==p.mainFrame();
const r=await cible.evaluate(()=>{
const Q=s=>Array.from(document.querySelectorAll(s));
const n=s=>Q(s).length;
const txt=e=>(e.textContent||'').replace(/\s+/g,' ').trim();
/* ââ la racine du volume : le grand div #ami-⊠s'il existe ââ */
const racine=document.querySelector('div[id^="ami-"]')||document.body;
/* ââ 1 · la barre du haut ââ */
const barre=document.querySelector('nav.ami-barre');
/* ââ 2 · le pied ââ */
const pieds=Q('footer');
const pied=document.querySelector('footer.ami-pied')||pieds[0]||null;
/* ââ 3 · pager suivant / prĂ©cĂ©dent ââ */
const pager=Q('a,button').filter(e=>{
const t=txt(e).toLowerCase();
return /(^|[^a-z])(précédent|precedent|suivant|page précédente|page suivante)([^a-z]|$)/.test(t)
|| /^[âââ”â¶âčâș«»]\s*\S/.test(t);
});
/* ââ 4 · accessibilitĂ© ââ */
const acces=Q('.access-tools,[class*="acces"],[aria-label*="Confort"],[aria-label*="confort"]');
const accesBtn=acces.length?acces[0].querySelectorAll('button').length:0;
/* ââ 5 · emporter ââ */
const emp=document.querySelector('.emporter,[class*="emporter"]');
const empPli=emp?(emp.tagName==='DETAILS'||!!emp.querySelector('details')):false;
/* ââ 6 · bandeau d'esprit critique ââ */
const bande=document.querySelector('.ami-bande-pli');
const bandeGroupes=bande?bande.querySelectorAll('.br-g,.br-pli-g,details>div>div[class^="br-"]').length:0;
const bandeH=bande?Math.round(bande.getBoundingClientRect().height):0;
/* ââ 7 · les rangĂ©es d'onglets, et si elles se replient ââ */
const rangs=[];
Q('[role="tablist"],#tabbar,.tabbar,.bar,.sbar,.nav-wrap .tablist,.tab-nav').forEach(e=>{
if(rangs.some(x=>x.el===e||x.el.contains(e)||e.contains(x.el))) return;
const bb=e.getBoundingClientRect();
const onglets=e.querySelectorAll('button,a,[role="tab"]').length;
const enveloppe=e.closest('.nav-wrap')||e.parentElement;
const poignee=!!(enveloppe&&(enveloppe.querySelector('.plier,.nav-toggle')||e.querySelector('.plier')));
rangs.push({el:e,cls:e.className||e.id,onglets,h:+bb.height.toFixed(0),poignee});
});
/* ââ 8 · les chapitres se replient-ils ââ */
const h2=Q('h2'), h3=Q('h3');
const plie=e=>!!e.closest('summary');
const h2Plies=h2.filter(plie).length, h3Plies=h3.filter(plie).length;
/* ââ 9 · les dĂ©finitions ââ */
const defs=Q('.df[data-d],[data-d],[data-def],.ami-def');
let pointilles=0;
Q('span,abbr,em,b,strong').slice(0,4000).forEach(e=>{
const c=getComputedStyle(e);
if(/dotted/.test(c.textDecorationStyle)||/dotted/.test(c.borderBottomStyle)) pointilles++;
});
/* ââ 10 · les titres ââ */
const titres=[...Q('h1'),...h2,...h3].map(e=>({t:e.tagName,x:txt(e).slice(0,140)}));
/* ââ 11 · tĂ©moins ââ */
const anc=Q('a[href^="#"]');
const morts=anc.filter(a=>{const h=a.getAttribute('href').slice(1);return h&&!document.getElementById(h)});
const vw=document.documentElement.clientWidth;
let over=0;
racine.querySelectorAll('*').forEach(e=>{const bb=e.getBoundingClientRect();if(bb.width>0&&bb.right>vw+2)over++;});
return {
racineId:racine.id||null,
hauteur:Math.round(document.documentElement.scrollHeight),
barre:!!barre, barreH:barre?Math.round(barre.getBoundingClientRect().height):0,
pied:!!pied, piedCls:pied?(pied.className||'(sans classe)'):null, pieds:pieds.length,
pager:pager.length, pagerEx:pager.slice(0,3).map(e=>txt(e).slice(0,40)),
acces:acces.length, accesBtn,
emporter:!!emp, emporterPli:empPli,
bande:!!bande, bandeGroupes, bandeH,
rangs:rangs.map(({el,...o})=>o),
h1:n('h1'), h2:h2.length, h3:h3.length, h2Plies, h3Plies,
details:n('details'), ouverts:n('details[open]'),
defs:defs.length, pointilles,
liens:anc.length, morts:morts.length,
over, svg:n('svg'), img:n('img'),
titres
};
});
r.fichier=file; r.cadre=cadre; r.elements=mieux; r.erreursJS=errs.length; r.erreursEx=errs.slice(0,3);
console.log(JSON.stringify(r));
await b.close();
})();
passe.py python3 passe.py [dossier]48 lignes
Lance audit.js sur tous les volumes d'un dossier, Ă©carte ceux que Diane fait elle-mĂȘme, applique sa rĂšgle de majuscules aux titres relevĂ©s, et Ă©crit audit.json.
â§ Copier le script
# -*- coding: utf-8 -*-
"""passe.py â l'audit de la passe Ă la Charte, volume par volume, AU RENDU.
Lecture seule. Usage : python3 passe.py <dossier> [> rapport.md]"""
import json, subprocess, sys, os, glob
from regle_majuscules import ecarts, a_emoji
DOSSIER = sys.argv[1] if len(sys.argv) > 1 else '/mnt/user-data/uploads/Protocole de Conscience/0.Protocole'
# Diane les fait elle-mĂȘme, ou une autre session les tient :
HORS = ('0.0_Initiation', '7_QuinteSSence', '4.2_EVEIL', '0.1_APOKALYPSE',
'0.3_LUNION', '1.2_MIROIR', '2.2_ASTROGLOBE', '0.0.0_Charte')
def audit(f):
r = subprocess.run(['node', 'audit.js', f], capture_output=True, text=True, timeout=600)
if r.returncode != 0 or not r.stdout.strip():
return {'fichier': f, 'erreur': (r.stderr or '')[-300:]}
return json.loads(r.stdout.strip().splitlines()[-1])
def main():
fichiers = sorted(glob.glob(os.path.join(DOSSIER, '*.html')))
res = []
for f in fichiers:
nom = os.path.basename(f)
dedans = not nom.startswith(HORS)
d = audit(f)
d['nom'] = nom
d['dans_la_passe'] = dedans
# les titres : sa rĂšgle de majuscules, et l'emoji de tĂȘte
ti = d.get('titres', [])
d['titres_n'] = len(ti)
mauvais, sans_emoji = [], 0
for x in ti:
e = ecarts(x['x'])
if e:
mauvais.append({'t': x['t'], 'x': x['x'][:90], 'e': e[:4]})
if x['t'] in ('H2', 'H3') and not a_emoji(x['x']):
sans_emoji += 1
d['titres_hors_regle'] = len(mauvais)
d['titres_exemples'] = mauvais[:6]
d['titres_sans_emoji'] = sans_emoji
d.pop('titres', None)
res.append(d)
print('⊠' + nom, file=sys.stderr)
json.dump(res, open('audit.json', 'w'), ensure_ascii=False, indent=1)
print('Ă©crit audit.json â', len(res), 'fichiers', file=sys.stderr)
if __name__ == '__main__':
main()
rapport.py python3 rapport.py > rapport.md50 lignes
Tourne audit.json en deux tableaux : ce que chaque volume porte, et le détail des témoins.
â§ Copier le script
# -*- coding: utf-8 -*-
import json, os, sys
d = json.load(open('audit.json'))
# on ne garde que la version la plus récente de chaque volume (préfixe x.y_NOM)
VOLUMES = ['0.1_APOKALYPSE_travail_passe1_boucle_couverture.html',
'0.2_PSYCHATRICE_aMi_V25_WEBNODE.html','0_Index_aMi.html',
'1.1_EGO_Evidence-Engine_aMi_V31_WEBNODE.html',
'1.3_ANDROGYNIE_travail_passe8_bande_et_sans_yin.html',
'2.1_ECO_Bonus-Plaisir_aMi_V5_WEBNODE.html','2.3_ENCYCLOPEDIE_Culture_aMi_(V17).html',
'3.1_Rencontre_Tu-Sais-Apprivoiser_aMi_V13_WEBNODE.html',
'3.2_JUSTICE_Code-Peinal_aMi_V12_WEBNODE.html','3.3_COSMOLOGIE_aMi_V14_WEBNODE.html',
'4.1_DECRYPTAGE_Tuto-Ouroboros_aMi_V8_WEBNODE.html','4.3_COSMOGONIE_Connexion_V37.html',
'5.1_SOUVENIR_Memorire_aMi_V3.html','5.2_LEGS_Corpus_aMi_V2_WEBNODE.html',
'5.3_ASTRO_Espace-Temps_aMi_V21_WEBNODE.html',
'6.0_KARMA_travail_passe10_avant_de_chiffrer.html']
par = {x['nom']: x for x in d}
manquants=[v for v in VOLUMES if v not in par]
if manquants: print('ABSENTS DE L AUDIT :', manquants, file=sys.stderr)
def oui(b): return 'â
' if b else 'â'
lignes=[]
for v in VOLUMES:
x=par.get(v)
if not x or 'erreur' in x:
lignes.append('| '+v.split('_')[0]+' | **non lu** | | | | | | | | | | |'); continue
rangs=x['rangs']; poign=sum(1 for r in rangs if r['poignee'])
ong = ('%d rangĂ©e%s'%(len(rangs),'s' if len(rangs)>1 else '')+(' · %d repliable%s'%(poign,'s' if poign>1 else '') if poign else ' · 0 repliable')) if rangs else 'â'
chap = '%d/%d h2 · %d/%d h3'%(x['h2Plies'],x['h2'],x['h3Plies'],x['h3'])
lignes.append('| %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s |' % (
v.split('_')[0], oui(x['barre']), oui(x['pied']),
('%d'%x['pager']) if x['pager'] else 'â',
('%d bt'%x['accesBtn']) if x['acces'] else 'â',
(oui(True)+(' repli' if x['emporterPli'] else ' **sans repli**')) if x['emporter'] else 'â',
('%d gr.'%x['bandeGroupes']) if x['bande'] else 'â',
ong, chap,
('%d'%x['defs']) if x['defs'] else '**0**',
'%d/%d'%(x['titres_hors_regle'],x['titres_n'])))
print('| Volume | barre | pied | pager | accÚs | Emporter | bande | onglets | chapitres repliés | défs | titres hors rÚgle |')
print('|---|---|---|---|---|---|---|---|---|---|---|')
print('\n'.join(lignes))
print()
print('### Détail des témoins')
print('| Volume | hauteur | h2 | h3 | replis | liens | morts | débord. | erreurs JS | titres sans emoji |')
print('|---|---|---|---|---|---|---|---|---|---|')
for v in VOLUMES:
x=par.get(v)
if not x or 'erreur' in x: continue
print('| %s | %d px | %d | %d | %d | %d | %s | %d | %d | %d |'%(v.split('_')[0],
x['hauteur'],x['h2'],x['h3'],x['details'],x['liens'],
('**%d**'%x['morts']) if x['morts'] else '0', x['over'], x['erreursJS'], x['titres_sans_emoji']))
regle_majuscules.py import regle_majuscules48 lignes
SA rÚgle de majuscules, relevée sur ses 622 titres de Charte : mots-outils en bas, mots pleins en haut, jamais un mot à majuscule interne, pas de point final. Rend la liste des écarts d'un titre.
â§ Copier le script
# -*- coding: utf-8 -*-
"""La rÚgle de majuscules de Diane, relevée sur ses 622 titres de Charte
(r115, 25/10) : les mots-outils restent bas, les mots qui portent le sens
montent, et JAMAIS un mot qui a déjà une majuscule interne (ConScience,
aMi, eXpression, MIROIR). Pas de point final dans un titre."""
import re
BAS = set("""de des du d le la les l un une au aux Ă et ou ni mais car donc or
en dans sur sous par pour sans avec vers chez entre contre selon depuis pendant
que qui quoi dont oĂč quand comme si ce cet cette ces se s ne n est sont Ă©tĂ© ĂȘtre
a ai as ont avait avaient sera seront peut peuvent pouvait doit doivent va vont
fait font pas plus moins trĂšs trop bien y en on il elle ils elles je j tu nous
vous me m te t lui leur leurs son sa ses mon ma mes ton ta tes notre nos votre
vos qu c jusqu lorsqu puisqu quelqu aussi encore déjà toujours jamais ainsi
cela ça ceci celui celle ceux celles tout toute tous toutes mĂȘme mĂȘmes
""".split())
MOT = re.compile(r"[A-Za-zĂ-ĂĂ-öÞ-Ăż]+(?:['â][A-Za-zĂ-ĂĂ-öÞ-Ăż]+)*")
def interne(mot):
"""un mot qui porte déjà une majuscule ailleurs qu'au début : intouchable"""
return any(c.isupper() for c in mot[1:])
def ecarts(titre):
"""rend la liste des mots qui ne suivent pas sa rĂšgle"""
t = titre.strip()
out = []
if t.endswith('.') and not t.endswith('...'):
out.append(('point final', t[-20:]))
mots = MOT.findall(t)
for i, m in enumerate(mots):
if interne(m):
continue # ConScience, aMi, eXpressionâŠ
bas = m.split("'")[0].split("â")[0].lower() if ("'" in m or "â" in m) else m.lower()
outil = bas in BAS or m.lower() in BAS
if i == 0:
continue # le premier mot monte toujours
if outil and m[0].isupper():
out.append(('outil en majuscule', m))
elif (not outil) and m[0].islower() and len(m) > 1:
out.append(('mot plein en minuscule', m))
return out
EMOJI = re.compile('[\U0001F000-\U0001FAFFâ-⯿â-âżïžâąÂ©Âź]')
def a_emoji(titre):
return bool(EMOJI.search(titre[:6]))
appliquer.py python3 appliquer.py source.html sortie.html92 lignes
Pose LE BLOC DE LA PASSE : chapitres repliables, poignĂ©e de repli des rangĂ©es d'onglets, « Emporter » repliable au rond + / â, trois boutons d'accessibilitĂ©. Les meubles montent AU CHARGEMENT, comme la Charte fabrique ses 232 replis. Sait Ă©crire dans un cadre hĂŽte, dans sa convention d'Ă©chappement.
â§ Copier le script
# -*- coding: utf-8 -*-
"""appliquer.py â pose LE BLOC DE LA PASSE Ă la fin d'un volume.
Il n'enlĂšve rien : il ajoute un <style> et un <script> qui montent les
meubles AU CHARGEMENT, comme la Charte le fait pour ses 232 replis.
Pour annuler : retirer les deux blocs id="passe-âŠ".
DEUX FORMES DE VOLUME, mesurées le 26/10 sur les seize :
â six sont des pages ordinaires ;
â dix sont des CADRES HĂTES : tout le document vit dans l'attribut
srcdoc d'un <iframe>, oĂč seules les guillemets sont Ă©chappĂ©es
("). Un bloc posĂ© dans l'hĂŽte n'y touche jamais rien â mesurĂ©
sur 2.1 ECO : le bloc se posait, et le rendu ne bougeait pas d'un
pixel. Il faut donc écrire DANS l'attribut, échappé comme lui.
Usage : python3 appliquer.py <source.html> <sortie.html>"""
import io, sys, os, re
MARQUE = 'passe-bloc-2026-10-26'
ICI = os.path.dirname(os.path.abspath(__file__))
def bloc():
css = io.open(os.path.join(ICI, 'bloc_passe.css.tpl'), encoding='utf-8').read()
js = io.open(os.path.join(ICI, 'bloc_passe.js.tpl'), encoding='utf-8').read()
return ('\n<style id="%s-style">\n%s\n</style>\n<script id="%s-js">\n%s\n</script>\n'
% (MARQUE, css, MARQUE, js))
def dans_attribut(t):
"""échappe pour vivre dans un attribut délimité par des guillemets"""
return t.replace('&', '&').replace('"', '"')
def borne_srcdoc(s):
"""rend (début, fin) de la VALEUR de l'attribut srcdoc, ou None"""
m = re.search(r'\bsrcdoc\s*=\s*"', s)
if not m:
return None
a = m.end()
b = s.find('"', a) # toutes les guillemets internes sont "
if b < 0:
return None
return a, b
def pose(src, dst):
s = io.open(src, encoding='utf-8').read()
if MARQUE in s:
return None, 'déjà posé'
B = bloc()
bornes = borne_srcdoc(s)
if bornes:
a, b = bornes
inner = s[a:b]
# DEUX CONVENTIONS d'échappement dans les srcdoc du corpus, mesurées :
# 2.1 ECO n'Ă©chappe que les guillemets â </body> y est littĂ©ral ;
# 4.1 DĂ©cryptage Ă©chappe aussi les chevrons â </body>.
# On écrit DANS la convention du volume, sinon le bloc n'est pas lu.
i = inner.rfind('</body>')
esc = dans_attribut
if i < 0:
i = inner.rfind('</body>')
if i >= 0:
esc = lambda t: (t.replace('&', '&').replace('<', '<')
.replace('>', '>').replace('"', '"'))
if i < 0:
i = inner.rfind('</html>')
if i < 0:
i = inner.rfind('</html>')
if i >= 0:
esc = lambda t: (t.replace('&', '&').replace('<', '<')
.replace('>', '>').replace('"', '"'))
if i < 0:
return None, 'cadre hÎte sans fin de corps : rien touché'
inner = inner[:i] + esc(B) + inner[i:]
s = s[:a] + inner + s[b:]
forme = 'cadre hĂŽte'
else:
i = s.rfind('</body>')
if i < 0:
i = s.rfind('</html>')
s = (s[:i] + B + s[i:]) if i >= 0 else (s + B)
forme = 'page ordinaire'
io.open(dst, 'w', encoding='utf-8').write(s)
return (len(s), forme), None
if __name__ == '__main__':
r, err = pose(sys.argv[1], sys.argv[2])
print(err or ('posĂ© (%s) â %d caractĂšres' % (r[1], r[0])))
bloc_passe.css.tpl (gabarit)67 lignes
La forme des meubles posĂ©s par appliquer.py â valeurs relevĂ©es au rendu de la Charte V38.
â§ Copier le script
/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
LE BLOC DE LA PASSE â la forme des meubles posĂ©s au chargement.
Les valeurs viennent du RENDU de la Charte V38 (md5 2224518f67de),
pas d'un souvenir : rond 30 Ă 30, bord 1 px #7a611a, fond
rgba(9,9,14,.72), glyphe ⎠en #ffc545 à 13 px, à 4 px du bord.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */
/* ââ 1 · les chapitres repliĂ©s âââââââââââââââââââââââââââââââââââ */
details.passe-pli{display:block;margin:0;border-top:1px solid rgba(255,197,69,.14)}
/* le résumé n'impose rien : il hérite l'alignement du chapitre, sinon un
titre centré se retrouve collé à gauche (mesuré sur 2.3 Encyclopédie) */
details.passe-pli>summary{cursor:pointer;list-style:none;position:relative;
padding:.55rem 1.9rem .55rem 0;text-align:inherit}
details.passe-pli>summary::-webkit-details-marker{display:none}
details.passe-pli>summary::after{content:'\25BE';position:absolute;right:.2rem;top:.75rem;
color:#ffc545;opacity:.72;font-size:.9em;transition:transform .2s}
details.passe-pli[open]>summary::after{transform:rotate(180deg)}
details.passe-pli>summary>h2,details.passe-pli>summary>h3{margin:0!important;display:block}
details.passe-pli>summary:hover>h2,details.passe-pli>summary:hover>h3{color:#ffe08a}
.passe-corps{padding:.1rem 0 .9rem}
/* ââ 2 · la poignĂ©e de repli d'une rangĂ©e d'onglets ââââââââââââââ */
.passe-plier{position:absolute;right:4px;top:2px;z-index:3;
width:30px;height:30px;display:grid;place-items:center;
border:1px solid #7a611a;border-radius:50%;
background:rgba(9,9,14,.72);color:#ffc545;
font:400 13px/1 'Jost',Arial,sans-serif;cursor:pointer;
padding:0;transition:transform .2s}
.passe-plier:hover{background:rgba(255,197,69,.12)}
.passe-enroulee>.passe-plier{transform:rotate(180deg)}
/* enroulée : seul l'onglet actif reste visible, sur une ligne */
.passe-enroulee>button:not(.passe-plier),
.passe-enroulee>a,
.passe-enroulee>[role="tab"]{display:none!important}
.passe-enroulee>.active,.passe-enroulee>.on,
.passe-enroulee>[aria-selected="true"],
.passe-enroulee>[data-active="true"]{display:flex!important;flex:1 1 auto}
/* ââ 3 · « Emporter » qui se replie, rond + / â ââââââââââââââââââ */
details.passe-emp{display:block}
details.passe-emp>summary{cursor:pointer;list-style:none;position:relative;padding-right:3rem}
details.passe-emp>summary::-webkit-details-marker{display:none}
details.passe-emp>summary::after{content:'+';position:absolute;right:.6rem;top:50%;
transform:translateY(-50%);width:1.65rem;height:1.65rem;
border:1px solid #ffc545;border-radius:50%;display:grid;place-items:center;
color:#ffc545;font:400 1.1rem/1 'Jost',Arial,sans-serif;background:rgba(255,197,69,.06)}
details.passe-emp[open]>summary::after{content:'\2212'}
details.passe-emp>summary>h2,details.passe-emp>summary>h3{display:inline-block;margin:0}
.passe-emp-c{padding-top:.4rem}
/* ââ 4 · les trois boutons d'accessibilitĂ© âââââââââââââââââââââââ */
.passe-acces{position:fixed;right:10px;bottom:14px;z-index:140;
display:flex;flex-direction:column;gap:.4rem}
.passe-acces button{width:34px;height:34px;border-radius:50%;
border:1px solid #7a611a;background:rgba(9,9,14,.80);color:#ffc545;
font:600 14px/1 'Jost',Arial,sans-serif;cursor:pointer;
display:grid;place-items:center;padding:0}
.passe-acces button:hover{background:rgba(255,197,69,.14)}
.passe-acces button[aria-pressed="true"]{background:rgba(255,197,69,.22);color:#fff3cf}
@media print{.passe-acces{display:none}}
/* les trois réglages, posés sur <html> */
html.passe-contraste{filter:contrast(1.18) saturate(1.06)}
html.passe-grand{font-size:118%}
html.passe-calme *,html.passe-calme *::before,html.passe-calme *::after{
animation:none!important;transition:none!important}
bloc_passe.js.tpl (gabarit)165 lignes
Le script des meubles. Ne replie QUE les h3 (replier les h2 avalait les titres de panneau), et laisse les devises d'une ligne tranquilles.
â§ Copier le script
/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
LE BLOC DE LA PASSE â meubles de la Charte posĂ©s AU CHARGEMENT.
MĂȘme grammaire que la Charte elle-mĂȘme, dont la source ne montre
qu'un seul <details> quand le rendu en fabrique 232.
Rien n'est retiré du volume : on ajoute, on enveloppe, on replie.
Pour tout annuler : supprimer ce <script> et son <style>.
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */
(function(){'use strict';
var D=document;
function prem(s,r){return (r||D).querySelector(s);}
function tous(s,r){return Array.prototype.slice.call((r||D).querySelectorAll(s));}
function texte(e){return (e.textContent||'').replace(/\s+/g,' ').trim();}
/* ââ la racine du volume âââââââââââââââââââââââââââââââââââââââ */
var R = prem('div[id^="ami-"]') || D.body;
/* ââ 1 · LES CHAPITRES SE REPLIENT âââââââââââââââââââââââââââââââ
Un chapitre = un titre, et tout ce qui le suit jusqu'au titre de
mĂȘme rang ou de rang supĂ©rieur. On n'enveloppe QUE des frĂšres :
jamais on ne dĂ©place un nĆud d'un parent Ă un autre. */
function replieTitres(rang){
var faits=0;
tous(rang, R).forEach(function(h){
if(h.closest('summary')) return; // déjà replié
if(h.closest('.passe-pli')) return;
if(h.closest('.passe-emp')||h.closest('.emporter')) return; // le bandeau a son propre rond
if(h.closest('.ami-bande-pli')||h.closest('summary')) return; // la bande d'esprit critique est déjà un meuble
if(h.closest('table')||h.closest('figure')) return; // un titre de cellule n'est pas un chapitre
var p=h.parentElement; if(!p) return;
if(p.tagName==='SUMMARY'||p.tagName==='DETAILS') return;
/* le titre doit ĂȘtre un enfant direct d'un conteneur de flux */
var corps=[], n=h.nextSibling, stop=false;
while(n){
if(n.nodeType===1){
var t=n.tagName;
if(t==='H1') break;
if(rang==='h2' && (t==='H2')) break;
if(rang==='h3' && (t==='H2'||t==='H3')) break;
if(t==='SCRIPT'||t==='STYLE'){ n=n.nextSibling; continue; }
}
corps.push(n); n=n.nextSibling;
}
if(!corps.length) return;
var dur=corps.filter(function(x){return x.nodeType===1;});
if(!dur.length) return;
/* Un chapitre a un corps. Mesuré sur 2.3 Encyclopédie : un titre suivi
d'une seule ligne en italique est une devise, pas un chapitre â la
replier la faisait disparaßtre de la page sans rien donner en échange.
Seuil : deux éléments, ou 220 caractÚres. */
var poids=corps.reduce(function(n,x){return n+((x.textContent||'').trim().length);},0);
if(dur.length<2 && poids<220) return;
var det=D.createElement('details'); det.className='passe-pli';
var som=D.createElement('summary');
var box=D.createElement('div'); box.className='passe-corps';
p.insertBefore(det,h);
som.appendChild(h); det.appendChild(som);
corps.forEach(function(x){box.appendChild(x);});
det.appendChild(box);
faits++;
});
return faits;
}
/* ââ 2 · LA POIGNĂE DE REPLI DES RANGĂES D'ONGLETS ââââââââââââââââ
Grammaire relevĂ©e AU RENDU de la Charte V38 : bouton rond 30Ă30,
bord 1 px #7a611a, fond rgba(9,9,14,.72), glyphe ⎠#ffc545 13 px,
posé en absolu à 4 px du bord droit, pivoté quand c'est replié ;
la rangée enroulée ne garde que l'onglet actif. */
function poignees(){
var vus=[], n=0;
tous('[role="tablist"],#tabbar,.tabbar,.bar,.sbar,.tab-nav,.ami-volume-tabs,.apk-mainbar', R).forEach(function(e){
if(vus.some(function(v){return v===e||v.contains(e)||e.contains(v);})) return;
if(e.querySelector('.passe-plier')) return;
var ong=tous('button,a,[role="tab"]',e);
if(ong.length<2) return;
vus.push(e);
if(getComputedStyle(e).position==='static') e.style.position='relative';
var b=D.createElement('button');
b.type='button'; b.className='passe-plier';
b.setAttribute('aria-expanded','true');
b.title='Enrouler les onglets'; b.setAttribute('aria-label','Enrouler les onglets');
b.textContent='âŽ';
b.addEventListener('click',function(ev){
ev.preventDefault(); ev.stopPropagation();
var ferme=e.classList.toggle('passe-enroulee');
b.setAttribute('aria-expanded',String(!ferme));
b.title=ferme?'Dérouler les onglets':'Enrouler les onglets';
b.setAttribute('aria-label',b.title);
});
e.appendChild(b); n++;
});
return n;
}
/* ââ 3 · « EMPORTER » SE REPLIE âââââââââââââââââââââââââââââââââââ
Le rond « + » / « â » dĂ©cidĂ© le 25/10 pour les encarts â et donc
PAS de seconde flĂšche. */
function emporter(){
var s=prem('.emporter',R)||prem('[class*="emporter"]',R);
if(!s) return 0;
if(s.tagName==='DETAILS'||s.querySelector('details')) return 0; // déjà replié
var h=s.querySelector('h2,h3,.emp-t'); if(!h) return 0;
var det=D.createElement('details'); det.className='passe-emp';
var som=D.createElement('summary');
var box=D.createElement('div'); box.className='passe-emp-c';
var reste=[]; var n=h.nextSibling;
while(n){ reste.push(n); n=n.nextSibling; }
s.insertBefore(det,h);
som.appendChild(h); det.appendChild(som);
reste.forEach(function(x){box.appendChild(x);});
det.appendChild(box);
return 1;
}
/* ââ 4 · LES BOUTONS D'ACCESSIBILITĂ ââââââââââââââââââââââââââââââ
Trois, et trois seulement : le contraste, la taille du texte, et
l'arrĂȘt des animations. Chacun se souvient de son Ă©tat. */
function acces(){
if(prem('.passe-acces',R)) return 0;
var boite=D.createElement('div');
boite.className='passe-acces'; boite.setAttribute('role','group');
boite.setAttribute('aria-label','Confort de lecture');
var outils=[
['contraste','â','Renforcer le contraste','passe-contraste'],
['taille','A','Agrandir le texte','passe-grand'],
['calme','â','ArrĂȘter les animations','passe-calme']
];
outils.forEach(function(o){
var b=D.createElement('button');
b.type='button'; b.className='pa-'+o[0]; b.textContent=o[1];
b.title=o[2]; b.setAttribute('aria-label',o[2]);
b.setAttribute('aria-pressed','false');
b.addEventListener('click',function(){
var on=D.documentElement.classList.toggle(o[3]);
b.setAttribute('aria-pressed',String(on));
try{localStorage.setItem(o[3],on?'1':'0');}catch(x){}
});
try{ if(localStorage.getItem(o[3])==='1'){ D.documentElement.classList.add(o[3]); b.setAttribute('aria-pressed','true'); } }catch(x){}
boite.appendChild(b);
});
(R.firstElementChild?R.insertBefore(boite,R.firstElementChild):R.appendChild(boite));
return 1;
}
/* ââ on pose, et on dit ce qu'on a posĂ© ââââââââââââââââââââââââ */
function pose(){
var bilan={chapitres:0,onglets:0,emporter:0,acces:0};
/* ORDRE : « Emporter » d'abord. Mesuré : quand les chapitres passaient en
premier, leur repli enveloppait le h2 d'« Emporter », et le bandeau
recevait le triangle des chapitres au lieu de son rond « + / â ». */
try{ bilan.emporter = emporter(); }catch(e){}
/* On ne replie QUE les h3. Mesuré sur 2.3 Encyclopédie : replier les h2
avalait les titres de panneau â « La Communication » perdait son
centrage, son sous-titre et sa ligne en italique passaient sous le pli.
Dans les volumes qu'elle a validés, le chapitre est un h3 ; le h2 est
le titre de la section. Les volumes dont les chapitres sont des h2
(3.3 CosmoLogie, 2.3 EncyclopĂ©die) attendent son Ćil. */
try{ bilan.chapitres = replieTitres('h3'); }catch(e){}
try{ bilan.onglets = poignees(); }catch(e){}
try{ bilan.acces = acces(); }catch(e){}
window.__passe = bilan;
}
if(D.readyState==='loading') D.addEventListener('DOMContentLoaded',pose); else pose();
})();
defs.py python3 defs.py source.html sortie.html64 lignes
Pose LES DĂFINITIONS : chaque mot du dictionnaire sur sa PREMIĂRE OCCURRENCE DE CHAQUE SECTION, en pointillĂ©, avec UNE SEULE BULLE pour toute la page. N'embarque que les mots prĂ©sents dans le volume.
â§ Copier le script
# -*- coding: utf-8 -*-
"""defs.py â pose LE BLOC DES DĂFINITIONS sur un volume.
Le dictionnaire est filtré : seuls les mots qui apparaissent vraiment
dans le volume voyagent avec lui.
Usage : python3 defs.py <source.html> <sortie.html>"""
import io, json, os, re, sys, html
MARQUE = 'passe-defs-2026-10-26'
ICI = os.path.dirname(os.path.abspath(__file__))
DICO = json.load(io.open(os.path.join(ICI, 'dictionnaire.json'), encoding='utf-8'))
from appliquer import borne_srcdoc, dans_attribut
def bloc(mots):
css = io.open(os.path.join(ICI, 'bloc_defs.css.tpl'), encoding='utf-8').read()
js = io.open(os.path.join(ICI, 'bloc_defs.js.tpl'), encoding='utf-8').read()
js = js.replace('__DICO__', json.dumps(mots, ensure_ascii=False))
return ('\n<style id="%s-style">\n%s\n</style>\n<script id="%s-js">\n%s\n</script>\n'
% (MARQUE, css, MARQUE, js))
def pose(src, dst):
s = io.open(src, encoding='utf-8').read()
if MARQUE in s:
return None, 'déjà posé'
# le texte oĂč chercher : la source dĂ©codĂ©e des entitĂ©s, cadre compris
# PIĂGE : dans un cadre hĂŽte qui Ă©chappe les chevrons, retirer les
# balises AVANT de dĂ©coder ne retire rien â et le volume semble ne
# contenir aucun mot du dictionnaire (mesuré sur 4.1 Décryptage).
# On décode d'abord, on retire les balises ensuite.
plat = re.sub(r'<[^>]+>', ' ', html.unescape(s))
mots = {m: d['def'] for m, d in DICO.items() if m in plat}
if not mots:
return None, 'aucun mot du dictionnaire dans ce volume'
B = bloc(mots)
bornes = borne_srcdoc(s)
if bornes:
a, b = bornes
inner = s[a:b]
i = inner.rfind('</body>')
esc = dans_attribut
if i < 0:
i = inner.rfind('</body>')
if i >= 0:
esc = lambda t: (t.replace('&', '&').replace('<', '<')
.replace('>', '>').replace('"', '"'))
if i < 0:
return None, 'cadre hĂŽte sans fin de corps'
inner = inner[:i] + esc(B) + inner[i:]
s = s[:a] + inner + s[b:]
forme = 'cadre hĂŽte'
else:
i = s.rfind('</body>')
s = (s[:i] + B + s[i:]) if i >= 0 else (s + B)
forme = 'page ordinaire'
io.open(dst, 'w', encoding='utf-8').write(s)
return (len(mots), forme), None
if __name__ == '__main__':
r, err = pose(sys.argv[1], sys.argv[2])
print(err or ('%d mots embarqués (%s)' % r))
bloc_defs.js.tpl (gabarit)119 lignes
Le script des définitions. Ne touche ni un lien, ni du code, ni un mot déjà défini.
â§ Copier le script
/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
LES DĂFINITIONS â les mots du Lexique et les mots difficiles, posĂ©s
sur leur PREMIĂRE OCCURRENCE DE CHAQUE SECTION, en pointillĂ©, avec
leur bulle.
Le dictionnaire n'est pas inventĂ© : il est relevĂ© dans SON corpus â
toutes les paires mot â dĂ©finition dĂ©jĂ Ă©crites dans les volumes,
la Charte, la QuinteSSence et l'Initiation (317 mots, 2 603 emplois
relevés le 26/10). Un mot qui porte deux formulations garde la plus
fréquente, et la liste des 19 cas est à cÎté pour qu'elle tranche.
UNE SEULE BULLE pour toute la page : c'est la solution qu'elle a
validée le 26/10 à 02h27, aprÚs avoir vu deux bulles s'ouvrir en
mĂȘme temps sur un mot (« ça se mange »).
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */
(function(){'use strict';
var DICO = __DICO__;
var D=document;
var R = D.querySelector('div[id^="ami-"]') || D.body;
if(!DICO || !Object.keys(DICO).length) return;
/* les plus longs d'abord : « 8 Plans de ConScience » avant « ConScience » */
var MOTS = Object.keys(DICO).sort(function(a,b){return b.length-a.length;});
/* on ne touche pas : ce qui est déjà défini, les liens, le code, les
scripts, les champs de saisie, ni un mot déjà posé dans la section */
function interdit(n){
for(var e=n.parentElement;e;e=e.parentElement){
var t=e.tagName;
if(t==='A'||t==='CODE'||t==='PRE'||t==='SCRIPT'||t==='STYLE'||t==='TEXTAREA'||t==='INPUT'||t==='BUTTON'||t==='SVG'||t==='svg') return true;
if(e.classList&&(e.classList.contains('df')||e.classList.contains('passe-df'))) return true;
if(e.hasAttribute&&e.hasAttribute('data-d')) return true;
}
return false;
}
function section(n){
for(var e=n.parentElement;e;e=e.parentElement){
if(e===R) break;
var t=e.tagName;
if(t==='SECTION'||t==='ARTICLE') return e;
if(e.classList&&(e.classList.contains('tab-content')||e.classList.contains('panel'))) return e;
if(e.hasAttribute&&e.hasAttribute('data-panel')) return e;
}
return R;
}
var faits=0, poses=new WeakMap();
function dejaVu(sec,mot){
var m=poses.get(sec); if(!m){ m={}; poses.set(sec,m); }
if(m[mot]) return true; m[mot]=1; return false;
}
function echappe(s){return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}
/* une frontiÚre de mot qui compte les lettres accentuées comme des lettres */
function motif(m){
return new RegExp('(^|[^A-Za-zĂ-ĂĂ-öÞ-Ăż0-9\'â-])('+echappe(m)+')(?=[^A-Za-zĂ-ĂĂ-öÞ-Ăż0-9\'â-]|$)');
}
function parcours(){
var it=D.createTreeWalker(R, NodeFilter.SHOW_TEXT, null);
var lots=[], n;
while((n=it.nextNode())){
if(!n.nodeValue || n.nodeValue.length<3) continue;
if(interdit(n)) continue;
lots.push(n);
}
lots.forEach(function(nd){
for(var i=0;i<MOTS.length;i++){
var mot=MOTS[i];
var v=nd.nodeValue; if(!v || v.indexOf(mot)<0) continue;
var sec=section(nd);
if(dejaVu(sec,mot)) continue;
var re=motif(mot), m=re.exec(v);
if(!m){ var p=poses.get(sec); if(p) delete p[mot]; continue; }
var a=m.index+m[1].length, b=a+mot.length;
var apres=nd.splitText(a); apres.splitText(mot.length);
var sp=D.createElement('span');
sp.className='passe-df'; sp.setAttribute('tabindex','0');
sp.setAttribute('data-d', DICO[mot]);
sp.textContent=mot;
apres.parentNode.replaceChild(sp, apres);
faits++;
nd = sp.nextSibling || nd; /* on continue aprÚs le mot posé */
if(!nd || nd.nodeType!==3) break;
}
});
}
/* ââ la bulle, une seule pour toute la page âââââââââââââââââââââââ */
var bulle=null, ancre=null;
function montre(e){
if(!bulle){
bulle=D.createElement('div'); bulle.className='passe-bulle';
bulle.setAttribute('role','tooltip'); D.body.appendChild(bulle);
}
bulle.textContent=e.getAttribute('data-d');
bulle.style.visibility='hidden'; bulle.style.opacity='0'; bulle.style.display='block';
var r=e.getBoundingClientRect();
var larg=Math.min(380, Math.max(240, window.innerWidth-32));
bulle.style.width=larg+'px';
var x=Math.min(Math.max(8, r.left), window.innerWidth-larg-8);
var h=bulle.getBoundingClientRect().height;
var y=r.bottom+8; if(y+h>window.innerHeight-8) y=Math.max(8, r.top-h-8);
bulle.style.left=x+'px'; bulle.style.top=y+'px';
bulle.style.visibility='visible'; bulle.style.opacity='1';
ancre=e;
}
function cache(){ if(bulle){ bulle.style.opacity='0'; bulle.style.visibility='hidden'; } ancre=null; }
D.addEventListener('mouseover',function(e){var t=e.target.closest&&e.target.closest('.passe-df'); if(t) montre(t);});
D.addEventListener('mouseout', function(e){var t=e.target.closest&&e.target.closest('.passe-df'); if(t&&t===ancre) cache();});
D.addEventListener('focusin', function(e){var t=e.target.closest&&e.target.closest('.passe-df'); if(t) montre(t);});
D.addEventListener('focusout', function(e){var t=e.target.closest&&e.target.closest('.passe-df'); if(t&&t===ancre) cache();});
D.addEventListener('keydown', function(e){ if(e.key==='Escape') cache(); });
window.addEventListener('scroll',function(){ if(ancre) montre(ancre); },{passive:true});
function pose(){ try{ parcours(); }catch(e){} window.__passeDefs=faits; }
if(D.readyState==='loading') D.addEventListener('DOMContentLoaded',pose); else pose();
})();
bloc_defs.css.tpl (gabarit)14 lignes
Le pointillé et la bulle, relevés au rendu de la Charte V38.
â§ Copier le script
/* ââ les dĂ©finitions : grammaire relevĂ©e AU RENDU de la Charte V38 ââ
soulignement pointillĂ© 1 px rgb(176,176,189), mĂȘme teinte pour le
mot, curseur « help », graisse 300 ; la bulle : fond #181826,
texte #b0b0bd, 13,12 px / 20,99 px, coins 5 px, ombre
0 12px 34px rgba(0,0,0,.55). Une seule bulle pour toute la page. */
.passe-df{border-bottom:1px dotted rgb(176,176,189);cursor:help;font-weight:300}
.passe-df:hover,.passe-df:focus{color:#ffe08a;border-bottom-color:#ffc545;outline:none}
.passe-bulle{position:fixed;z-index:200;display:none;
background:#181826;color:#b0b0bd;border-radius:5px;padding:12px 16px;
font:300 13.12px/20.992px 'Jost',Arial,sans-serif;
box-shadow:0 12px 34px rgba(0,0,0,.55);
opacity:0;transition:opacity .14s;pointer-events:none;white-space:normal}
@media print{.passe-bulle{display:none!important}}
collant.py python3 collant.py source.html sortie.html27 lignes
Pose LE COLLANT SOUS L'HABILLAGE : les deux scripts de la Charte (le pont du collant et le dĂ©calage sous l'hĂŽte), rendus gĂ©nĂ©riques. Sans eux, sur Webnode, la navbar et les onglets ne collent pas et la barre de progression passe sous l'en-tĂȘte du site.
â§ Copier le script
# -*- coding: utf-8 -*-
"""collant.py â pose LE COLLANT SOUS L'HABILLAGE (les deux scripts de la Charte)."""
import io, os, sys
MARQUE = 'passe-collant-2026-10-26'
ICI = os.path.dirname(os.path.abspath(__file__))
from appliquer import borne_srcdoc, dans_attribut
def pose(src, dst):
s = io.open(src, encoding='utf-8').read()
if MARQUE in s: return None, 'déjà posé'
js = io.open(os.path.join(ICI, 'bloc_collant.js.tpl'), encoding='utf-8').read()
B = '\n<script id="%s">\n%s\n</script>\n' % (MARQUE, js)
bornes = borne_srcdoc(s)
if bornes:
a, b = bornes; inner = s[a:b]
i = inner.rfind('</body>'); esc = dans_attribut
if i < 0:
i = inner.rfind('</body>')
if i >= 0: esc = lambda t: (t.replace('&','&').replace('<','<').replace('>','>').replace('"','"'))
if i < 0: return None, 'cadre hĂŽte sans fin de corps'
s = s[:a] + inner[:i] + esc(B) + inner[i:] + s[b:]; forme='cadre hĂŽte'
else:
i = s.rfind('</body>'); s = (s[:i] + B + s[i:]) if i >= 0 else (s + B); forme='page ordinaire'
io.open(dst, 'w', encoding='utf-8').write(s)
return forme, None
if __name__ == '__main__':
f, e = pose(sys.argv[1], sys.argv[2]); print(e or ('posé (%s)' % f))
bloc_collant.js.tpl (gabarit)141 lignes
Les deux scripts, repris mot pour mot de la Charte (26/09/2026), la racine rendue générique et la portée élargie à la boßte du bloc.
â§ Copier le script
/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
LE COLLANT SOUS L'HABILLAGE â repris MOT POUR MOT de la Charte
(scripts `ami-charte-pont-du-collant` et
`ami-charte-decale-ce-qui-colle`, 26/09/2026), seule la racine est
rendue générique : le bloc du volume, quel que soit son nom.
Mesuré le 26/10 sur agdistys.fr/ami-leveil-de-lia, à 1 500 px de
dĂ©filement : la navbar du volume Ă©tait Ă â998 et la rangĂ©e d'onglets
Ă 397 au lieu de 48 â deux ancĂȘtres Webnode (DIV.b-c et
DIV.wnd-page) portent overflow:hidden, ce qui fait d'eux la boĂźte de
référence du collant. Et la barre de progression, elle, fonctionnait
(fixed, 427 px de large sur 1 200) mais l'en-tĂȘte fixe de Webnode
(80 px) était peint par-dessus.
La Charte réglait les deux depuis septembre. Diane : « la charte est
ok, t'as juste à faire pareil. »
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */
/* ââ LE PONT DU COLLANT (26/09/2026) ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
« position:sticky » cesse d'agir dĂšs qu'un ANCĂTRE porte overflow hidden/auto/scroll : l'Ă©lĂ©ment se
colle alors à une boßte qui, elle, ne défile pas. Les habillages de site (Webnode entre autres) posent
couramment overflow-x:hidden pour mater les dĂ©bordements. On remplace, sur les seuls ancĂȘtres du bloc,
« hidden » par « clip » : ça coupe pareil, et ça ne crĂ©e pas de conteneur de dĂ©filement. ââ */
(function(){'use strict';
var R=document.querySelector('div[id^="ami-"]')||document.getElementById('ami-charte');
if(!R) return;
function libere(){
var e=R.parentElement, n=0;
while(e && e!==document.documentElement){
var c=getComputedStyle(e);
if(c.overflowX==='hidden'){ e.style.overflowX='clip'; n++; }
if(c.overflowY==='hidden'){ e.style.overflowY='visible'; n++; }
e=e.parentElement;
}
return n;
}
libere();
/* l'habillage peut arriver aprÚs nous : on repasse une fois la page posée */
window.addEventListener('load', libere);
setTimeout(libere, 600); setTimeout(libere, 2000);
})();
/* ââ CE QUI COLLE SE POSE SOUS CE QUE L'HĂTE OCCUPE (V11, 26/09/2026) ââââââââââ
Diane : « j'ai ma navbarre Webnode sticky, et la navbarre de la page passe en
dessous. » La V10 visait une liste de noms et a raté nav.ami-barre. Ici on ne
nomme plus rien : on prend tout ce qui colle en haut DANS le bloc, on retient
la hauteur qu'il demandait, et on y ajoute celle de l'hĂŽte. ĂcartĂ©s : les
en-tĂȘtes de tableaux et tout ce qui colle Ă l'intĂ©rieur d'un rouleau â ceux-lĂ
ne collent pas Ă la fenĂȘtre, les dĂ©caler les ferait sauter. */
(function(){'use strict';
var R = document.querySelector('div[id^="ami-"]') || document.getElementById('ami-charte');
if (!R) return;
var dernier = -1;
function hautDeLHote(){
var bas = 0, x = Math.round(window.innerWidth / 2);
[1, 3].forEach(function(y){
var l = document.elementsFromPoint(x, y) || [];
[].slice.call(l).forEach(function(e){
if (!e || e === document.documentElement || e === document.body) return;
if (R.contains(e) || e.contains(R)) return; /* pas nous, ni notre parent */
var c = getComputedStyle(e);
if (c.position !== 'fixed' && c.position !== 'sticky') return;
var r = e.getBoundingClientRect();
if (r.height <= 0 || r.height > 260) return; /* une barre, pas un décor */
if (r.width < window.innerWidth * 0.6) return; /* large, donc elle barre l'écran */
if (r.top > 12) return; /* posée tout en haut */
if (r.bottom > bas) bas = Math.round(r.bottom);
});
});
return bas;
}
function dansUnRouleau(e){
var p = e.parentElement;
while (p && p !== R && p !== document.body) {
var c = getComputedStyle(p);
if (/auto|scroll/.test(c.overflowY) || /auto|scroll/.test(c.overflowX)) return true;
p = p.parentElement;
}
return false;
}
function collants(){
var out = [];
/* 26/10 : dans la Charte, la barre d'architecture vit DANS #ami-charte ;
dans l'Ăveil elle est posĂ©e JUSTE AVANT #ami-leveil-de-lia, et la barre
de progression est un <div id="prog"> frĂšre lui aussi. Scanner la seule
racine les laissait derriĂšre â mesurĂ© : les onglets passaient bien Ă
128 px, la navbar et la barre de progression restaient Ă 0, sous
l'en-tĂȘte de l'hĂŽte.
On scanne donc la BOĂTE DU BLOC â le parent de la racine. Chez Webnode
c'est .b-c, qui ne contient que notre bloc ; l'en-tĂȘte du site vit
ailleurs dans le document et n'est donc jamais touché. Sur une page
autonome ce parent est <body>, et il n'y a pas d'hĂŽte Ă heurter. */
var PORTEE = (R.parentElement && R.parentElement !== document.documentElement)
? R.parentElement : R;
var tous = [PORTEE].concat([].slice.call(PORTEE.querySelectorAll('*')));
tous.forEach(function(e){
var c = getComputedStyle(e);
if (c.position !== 'sticky' && c.position !== 'fixed') return;
if (/^(TH|THEAD|TR|TD|CAPTION)$/.test(e.tagName)) return; /* en-tĂȘte de tableau */
if (dansUnRouleau(e)) return; /* colle dans son rouleau */
out.push(e);
});
return out;
}
/* On ne retient AUCUNE hauteur : celle que la feuille de style demande peut
changer d'un instant Ă l'autre (min(var(--nav-h),9rem) suit la barre qui se
replie). à chaque passe on lui rend la parole, on écoute, puis on ajoute. */
function pose(){
var hote = hautDeLHote();
document.documentElement.style.setProperty('--top-site', hote + 'px');
var barres = collants();
barres.forEach(function(e){ e.style.removeProperty('top'); });
var dits = barres.map(function(e){ return parseFloat(getComputedStyle(e).top); });
barres.forEach(function(e, i){
var t = dits[i];
if (!isFinite(t) || t > 260) return; /* top:auto, ou posé bas : pas notre affaire */
e.style.setProperty('top', Math.round(t + hote) + 'px', 'important');
});
R.style.setProperty('scroll-padding-top', (hote + 96) + 'px');
dernier = hote;
}
pose();
window.addEventListener('load', pose);
window.addEventListener('resize', pose);
var t = null;
/* la barre se replie EN DĂFILANT : il faut repasser pendant le dĂ©filement,
sinon les onglets restent posés à la hauteur de la barre dépliée. */
window.addEventListener('scroll', function(){
if (t) return;
t = setTimeout(function(){ t = null; pose(); }, 120);
}, {passive:true});
/* le bloc se déplie encore un moment aprÚs l'arrivée : on repasse */
[400, 1200, 2500, 5000].forEach(function(ms){ setTimeout(pose, ms); });
})();
essai_webnode.py python3 essai_webnode.py volume.html essai.html28 lignes
Reproduit l'habillage de Webnode autour d'un volume â en-tĂȘte fixe de 80 px et deux boĂźtes en overflow:hidden â pour vĂ©rifier le collant SANS publier.
â§ Copier le script
# -*- coding: utf-8 -*-
"""essai_webnode.py â reproduit l'habillage de Webnode autour d'un volume, tel
qu'il a Ă©tĂ© MESURĂ le 26/10 sur agdistys.fr : un en-tĂȘte fixe de 80 px, et
deux boĂźtes en overflow:hidden entre la page et la racine du volume
(DIV.b-c et DIV.wnd-page). Sert à vérifier le collant sans publier."""
import io, re, sys
src, dst = sys.argv[1], sys.argv[2]
s = io.open(src, encoding='utf-8').read()
HOTE_CSS = """
<style id="essai-hote-webnode">
.h-w{position:fixed;top:0;left:0;right:0;height:80px;background:#2b1a0b;color:#fff;
z-index:900;display:flex;align-items:center;justify-content:center;font:14px Arial}
.b-c{overflow:hidden}
.wnd-page{overflow:hidden}
body{padding-top:0}
</style>
"""
i = s.find('<body')
j = s.find('>', i) + 1
k = s.rfind('</body>')
corps = s[j:k]
s = (s[:j] + HOTE_CSS
+ '<div class="h-w">en-tĂȘte de l\'hĂŽte â 80 px, fixe</div>'
+ '<div class="wnd-page"><div class="b-c">' + corps + '</div></div>'
+ s[k:])
io.open(dst, 'w', encoding='utf-8').write(s)
print('écrit', dst, len(s))
collant_test.js node collant_test.js sans.html avec.html24 lignes
Mesure la position des barres à 0, 1 500 et 4 000 px de défilement, avant et aprÚs le collant.
â§ Copier le script
const {chromium}=require('playwright');
(async()=>{const b=await chromium.launch();
for(const f of [process.argv[2],process.argv[3]]){
const p=await b.newPage({viewport:{width:1280,height:900}});
await p.route('**/*',r=>/^file:/.test(r.request().url())?r.continue():r.abort());
await p.goto('file://'+f,{waitUntil:'load',timeout:120000}); await p.waitForTimeout(2800);
const lignes=[];
for(const y of [0,1500,4000]){
await p.evaluate(Y=>window.scrollTo(0,Y),y); await p.waitForTimeout(450);
lignes.push(await p.evaluate(()=>{
const g=s=>{const e=document.querySelector(s);if(!e)return null;const r=e.getBoundingClientRect();const c=getComputedStyle(e);
return {top:Math.round(r.top),pos:c.position,top_css:c.top};};
return {y:Math.round(window.pageYOffset), barre:g('.ami-barre'), tabs:g('#tabbar'),
ov:(()=>{const R=document.querySelector('div[id^="ami-"]');const o=[];
for(let e=R.parentElement;e&&e!==document.documentElement;e=e.parentElement){const c=getComputedStyle(e);
if(!/visible/.test(c.overflowX)||!/visible/.test(c.overflowY)) o.push((e.className||e.tagName)+':'+c.overflowX+'/'+c.overflowY);}
return o;})()};
}));
}
console.log(f.split('/').pop()); lignes.forEach(l=>console.log(' ',JSON.stringify(l)));
await p.close();
}
await b.close();})();
verif.js node verif.js avant.html apres.html49 lignes
LES TĂMOINS. Compare un volume avant/aprĂšs : titres perdus, compteurs de structure, erreurs JS, dĂ©bordements, et surtout LE TEXTE LU TOUT DĂPLIĂ â le seul qui dise si quelque chose a disparu.
â§ Copier le script
/* verif.js â compare un volume AVANT et APRĂS le bloc de la passe. */
const {chromium}=require('playwright');
(async()=>{
const b=await chromium.launch();
const out={};
for(const [cle,f] of [['avant',process.argv[2]],['apres',process.argv[3]]]){
const p=await b.newPage({viewport:{width:1280,height:900}});
const errs=[]; p.on('pageerror',e=>errs.push(String(e).slice(0,140)));
await p.route('**/*',r=>/^file:/.test(r.request().url())?r.continue():r.abort());
await p.goto('file://'+f,{waitUntil:'load',timeout:120000});
await p.waitForTimeout(2500);
let cible=p.mainFrame(),mieux=-1;
for(const fr of p.frames()){let k=0;try{k=await fr.evaluate(()=>document.querySelectorAll('*').length)}catch(e){k=-1}if(k>mieux){mieux=k;cible=fr}}
out[cle]=await cible.evaluate(()=>{
const n=s=>document.querySelectorAll(s).length;
const R=document.querySelector('div[id^="ami-"]')||document.body;
const vw=document.documentElement.clientWidth; let over=0;
R.querySelectorAll('*').forEach(e=>{const r=e.getBoundingClientRect();if(r.width>0&&r.right>vw+2)over++;});
const T=[...document.querySelectorAll('h1,h2,h3')].map(e=>(e.textContent||'').replace(/\s+/g,' ').trim());
/* LE TĂMOIN QUI COMPTE : on ouvre TOUT avant de lire le texte. Un repli
cache du texte sans rien perdre ; seul le texte tout déplié dit si
quelque chose a disparu. */
document.querySelectorAll('details').forEach(d=>d.open=true);
const texte=(R.innerText||'').replace(/\s+/g,' ').trim();
return {h1:n('h1'),h2:n('h2'),h3:n('h3'),details:n('details'),ouverts:n('details[open]'),
a:n('a'),img:n('img'),svg:n('svg'),tables:n('table'),li:n('li'),p:n('p'),
plier:n('.passe-plier'),acces:n('.passe-acces button'),emp:n('details.passe-emp'),pli:n('details.passe-pli'),
over, titres:T, nTexte:texte.length, bilan:window.__passe||null,
hauteur:Math.round(document.documentElement.scrollHeight)};
});
out[cle].erreursJS=errs.length; out[cle].ex=errs.slice(0,2);
await p.close();
}
const A=out.avant,B=out.apres;
const diff={};
for(const k of ['h1','h2','h3','a','img','svg','tables','li','p']) if(A[k]!==B[k]) diff[k]=A[k]+' â '+B[k];
// les titres doivent ĂȘtre les mĂȘmes, mot pour mot
const perdus=A.titres.filter(t=>!B.titres.includes(t));
const neufs=B.titres.filter(t=>!A.titres.includes(t));
console.log(JSON.stringify({
avant:{details:A.details,over:A.over,err:A.erreursJS,h:A.hauteur,texte:A.nTexte},
apres:{details:B.details,ouverts:B.ouverts,over:B.over,err:B.erreursJS,h:B.hauteur,texte:B.nTexte,
plier:B.plier,acces:B.acces,emp:B.emp,pli:B.pli,bilan:B.bilan,ex:B.ex},
compteurs_qui_bougent:diff,
titres_perdus:perdus.slice(0,8), titres_neufs:neufs.slice(0,8),
texte_delta:B.nTexte-A.nTexte
},null,1));
await b.close();})();
photo.js node photo.js volume.html photo.png [y]11 lignes
Photographie un volume Ă une hauteur donnĂ©e, pour comparer avant/aprĂšs Ă l'Ćil.
â§ Copier le script
const {chromium}=require('playwright');
(async()=>{const b=await chromium.launch();
const p=await b.newPage({viewport:{width:1280,height:1100},deviceScaleFactor:1});
await p.route('**/*',r=>/^file:/.test(r.request().url())?r.continue():r.abort());
await p.goto('file://'+process.argv[2],{waitUntil:'load',timeout:120000});
await p.waitForTimeout(2600);
await p.evaluate(y=>window.scrollTo(0,y), +(process.argv[4]||0));
await p.waitForTimeout(500);
await p.screenshot({path:process.argv[3]});
await b.close();})();
tourner.py python3 tourner.py49 lignes
Pose le bloc de la passe sur tous les volumes, puis vérifie chacun. Ne garde que ceux dont les témoins tiennent.
â§ Copier le script
# -*- coding: utf-8 -*-
"""tourner.py â pose le bloc sur chaque volume de la passe, puis VĂRIFIE.
Un volume n'est gardé que si ses témoins tiennent : aucun titre perdu,
aucun compteur de structure qui bouge, le texte tout déplié inchangé
(à la marge des boutons posés), et zéro erreur JS de plus qu'avant."""
import json, os, subprocess, sys
U = '/mnt/user-data/uploads/Protocole de Conscience/0.Protocole'
S = '[référence locale non publiée]'
os.makedirs(S, exist_ok=True)
VOLUMES = ['0.2_PSYCHATRICE_aMi_V25_WEBNODE.html', '0_Index_aMi.html',
'1.1_EGO_Evidence-Engine_aMi_V31_WEBNODE.html',
'1.3_ANDROGYNIE_travail_passe8_bande_et_sans_yin.html',
'2.1_ECO_Bonus-Plaisir_aMi_V5_WEBNODE.html', '2.3_ENCYCLOPEDIE_Culture_aMi_(V17).html',
'3.1_Rencontre_Tu-Sais-Apprivoiser_aMi_V13_WEBNODE.html',
'3.2_JUSTICE_Code-Peinal_aMi_V12_WEBNODE.html', '3.3_COSMOLOGIE_aMi_V14_WEBNODE.html',
'4.1_DECRYPTAGE_Tuto-Ouroboros_aMi_V8_WEBNODE.html', '4.3_COSMOGONIE_Connexion_V37.html',
'5.1_SOUVENIR_Memorire_aMi_V3.html', '5.2_LEGS_Corpus_aMi_V2_WEBNODE.html',
'5.3_ASTRO_Espace-Temps_aMi_V21_WEBNODE.html',
'6.0_KARMA_travail_passe10_avant_de_chiffrer.html']
res = []
for v in VOLUMES:
src, dst = os.path.join(U, v), os.path.join(S, v)
p = subprocess.run(['python3', 'appliquer.py', src, dst], capture_output=True, text=True)
pose = p.stdout.strip() or p.stderr.strip()[-200:]
if not os.path.exists(dst):
res.append({'nom': v, 'pose': pose, 'verdict': 'NON POSĂ'})
print('â', v, pose, file=sys.stderr); continue
q = subprocess.run(['node', 'verif.js', src, dst], capture_output=True, text=True, timeout=900)
try:
d = json.loads(q.stdout)
except Exception:
res.append({'nom': v, 'pose': pose, 'verdict': 'VĂRIF ILLISIBLE', 'err': q.stderr[-200:]})
print('â', v, 'vĂ©rif illisible', file=sys.stderr); continue
ok = (not d['titres_perdus']) and (not d['compteurs_qui_bougent']) \
and abs(d['texte_delta']) <= 40 \
and d['apres']['err'] <= d['avant']['err'] \
and d['apres']['over'] <= d['avant']['over'] + 2
d.update({'nom': v, 'pose': pose, 'verdict': 'OK' if ok else 'Ă REVOIR'})
res.append(d)
b = d['apres'].get('bilan') or {}
print(('â' if ok else 'â ') + ' ' + v.split('_')[0].ljust(5)
+ ' chap %-4s onglets %-3s emporter %-2s accĂšs %-2s texte %+d err %dâ%d'
% (b.get('chapitres'), b.get('onglets'), b.get('emporter'), b.get('acces'),
d['texte_delta'], d['avant']['err'], d['apres']['err']), file=sys.stderr)
json.dump(res, open('tour.json', 'w'), ensure_ascii=False, indent=1)
tourner2.py python3 tourner2.py31 lignes
Pose les définitions sur tous les volumes, puis vérifie chacun contre son état d'avant.
â§ Copier le script
# -*- coding: utf-8 -*-
"""tourner2.py â pose LE BLOC DES DĂFINITIONS par-dessus le bloc de la passe,
puis vérifie chaque volume contre son état d'avant."""
import json, os, subprocess, sys
S, T = '[référence locale non publiée]', '[référence locale non publiée]'
os.makedirs(T, exist_ok=True)
res = []
for v in sorted(os.listdir(S)):
if not v.endswith('.html'): continue
src, dst = os.path.join(S, v), os.path.join(T, v)
p = subprocess.run(['python3', 'defs.py', src, dst], capture_output=True, text=True)
pose = p.stdout.strip() or p.stderr.strip()[-200:]
if not os.path.exists(dst):
res.append({'nom': v, 'pose': pose, 'verdict': 'NON POSĂ'})
print('â', v.split('_')[0], pose, file=sys.stderr); continue
q = subprocess.run(['node', 'verif.js', src, dst], capture_output=True, text=True, timeout=900)
try: d = json.loads(q.stdout)
except Exception:
res.append({'nom': v, 'pose': pose, 'verdict': 'VĂRIF ILLISIBLE', 'err': q.stderr[-300:]})
print('â', v.split('_')[0], 'vĂ©rif illisible', q.stderr[-160:], file=sys.stderr); continue
ok = (not d['titres_perdus']) and (not d['compteurs_qui_bougent']) \
and abs(d['texte_delta']) <= 5 and d['apres']['err'] <= d['avant']['err'] \
and d['apres']['over'] <= d['avant']['over'] + 2
d.update({'nom': v, 'pose': pose, 'verdict': 'OK' if ok else 'Ă REVOIR'})
res.append(d)
print(('â' if ok else 'â ') + ' ' + v.split('_')[0].ljust(5)
+ ('%-28s posĂ©es %-5s texte %+d err %dâ%d dĂ©bord %dâ%d'
% (pose, d['apres']['defs'], d['texte_delta'],
d['avant']['err'], d['apres']['err'], d['avant']['over'], d['apres']['over'])), file=sys.stderr)
json.dump(res, open('tour2.json', 'w'), ensure_ascii=False, indent=1)
tourner3.py python3 tourner3.py18 lignes
Pareil, pour le collant.
â§ Copier le script
# -*- coding: utf-8 -*-
"""tourner3.py â vĂ©rifie chaque volume aprĂšs la pose du collant."""
import json, os, subprocess, sys
S, T = '[référence locale non publiée]', '[référence locale non publiée]'
res=[]
for v in sorted(os.listdir(T)):
if not v.endswith('.html'): continue
q = subprocess.run(['node','verif.js',os.path.join(S,v),os.path.join(T,v)],capture_output=True,text=True,timeout=900)
try: d=json.loads(q.stdout)
except Exception:
print('â',v.split('_')[0],'vĂ©rif illisible',q.stderr[-160:],file=sys.stderr); res.append({'nom':v,'verdict':'ILLISIBLE'}); continue
ok=(not d['titres_perdus']) and (not d['compteurs_qui_bougent']) and abs(d['texte_delta'])<=5 \
and d['apres']['err']<=d['avant']['err'] and d['apres']['over']<=d['avant']['over']+2
d.update({'nom':v,'verdict':'OK' if ok else 'Ă REVOIR'}); res.append(d)
print(('â' if ok else 'â ')+' '+v.split('_')[0].ljust(5)+'texte %+d err %dâ%d dĂ©bord %dâ%d replis %dâ%d'
%(d['texte_delta'],d['avant']['err'],d['apres']['err'],d['avant']['over'],d['apres']['over'],d['avant']['details'],d['apres']['details']),file=sys.stderr)
json.dump(res,open('tour3.json','w'),ensure_ascii=False,indent=1)
đȘ¶ La Charte, de la V39 Ă la V46 â 26 octobre 2026
Huit versions dans la soirĂ©e, toutes reconstruites depuis la V38 par un seul outil : les versions intermĂ©diaires ne sont que des Ă©tapes, et l'outil ci-dessous porte les dix-neuf corrections Ă la fois. Deux retouches seulement entrent dans la source (le lien des fontes, le <small> retirĂ©, le titre des Non-Excuses, le đ± du Legs, les deux portes de la navbar, l'Ă©cran de chargement) ; tout le reste vit dans un bloc marquĂ© qu'on retire d'un coup pour revenir en arriĂšre. Les archives d'avant-passe sont dans CONTROLE QUALITE/ARCHIVES/, le dĂ©tail mesurĂ© de chaque version dans 00_LISTE_PASSE_CHARTE_V28.md.
V39 â la une sans capitales · le gras de l'onglet (la fonte 600 n'Ă©tait pas chargĂ©e) · les onglets qui s'enroulent au dĂ©filement · la devise du Manuel en ambre, son catalogue retirĂ© · la marge des onze familles (Ă 393 px, « 10. » et « 11. » sortaient du cadre).
V40 â les sous-onglets collĂ©s, sur toute la largeur, visibles quand on dĂ©plie, et repliĂ©s DANS la ligne de l'onglet primaire en sans-serif · « ⟠Tout dĂ©plier » sous le titre de la partie · le « enâĄïžre » dorĂ©.
V41 â les deux blocs du pied resserrĂ©s Ă la largeur de l'encre du texte du dessous · le Legs en moldavite · « La rĂšgle qui tient toute la page » en bandeau pleine largeur, posĂ© avant le bandeau d'esprit critique qui se colle aux onglets · « đ§ Tuto d'Esprit Critique » en gras.
V42 et V43 â le pied sur UNE SEULE LIGNE : filet de fraction · puce · « enâĄïžre » · puce · filet, Ă l'Ă©cart 0,00 · plus de blanc sous le pied quand le fichier s'ouvre seul.
V44 â le « & » abaissĂ© de 2,60 px pour que le haut de l'Ă©clair touche sa boucle · les puces au milieu du creux · le dorĂ© d'entrĂ©e Ă©clairci · « Pour » sur sa ligne · l'Ă©cran de chargement noir « Chargement · Protocole aMi » · Les SchĂ©mas et Les Fables dans la navbar.
V45 â les onglets sans article de tĂȘte (22 libellĂ©s, 71 caractĂšres, nom entier gardĂ© en infobulle) · le sous-titre en sans-serif · le compteur maigre et gris, et retirĂ© de la ligne repliĂ©e.
V46 â « Pour Le DĂ©sir de » recalĂ© sur l'axe de la paire et rapprochĂ© d'une ligne · les bords sombres du mĂ©tal Ă©claircis · « đïž Les SchĂ©mas Illustratifs » et « đŁïž Les Fables ContĂ©es ».
TĂ©moin de la V46 contre la V38 , tout dĂ©pliĂ©, Ă 1 280 et Ă 393 px : â517 caractĂšres â le <small> du Manuel (â449), le đ± (+3) et les articles des onglets (â71), tous comptĂ©s un Ă un â +2 liens (les deux portes), et 258 replis · 8 onglets · 24 sous-onglets · 763 titres · 269 tableaux identiques. DĂ©bordements inchangĂ©s Ă 1 280 (16) et en baisse Ă 393 (986 â 921). 0 erreur JS.
charte_v46.py python3 charte_v46.py V38.html V46.html731 lignes
Reconstruit la Charte V46 depuis la V38 : dix-neuf corrections, deux retouches de source et un bloc marquĂ©. Chaque remplacement est comptĂ© et refusĂ© s'il ne tombe pas sur le nombre attendu â un motif qui ne mord plus ARRĂTE la construction au lieu de produire un fichier Ă moitiĂ© juste. Les rĂ©glages qui dĂ©pendent de la fonte (largeur d'encre, croisement du « & », alignement des puces) sont MESURĂS au rendu par le bloc, jamais Ă©crits en dur.
â§ Copier le script
# -*- coding: utf-8 -*-
"""
charte_v46.py â la passe du 26 octobre 2026 sur la Charte de Communication.
(V46 = V45 + ses retours de 20h08 : « Pour Le Désir de » recalé
et rapproché, les bords du doré éclaircis, et les deux portes
de la navbar renommées.)
(V45 = V44 + ses retours de 19h52 : les onglets sans article,
le sous-titre en sans-serif, et le repli qui porte le nom du
sous-onglet â pas son compteur.)
(V44 = V43 + ses retours de 19h28 : le « & » abaissé pour que
l'éclair touche sa boucle, les puces au milieu des creux, le
noir jusqu'aux bords, le doré du pied éclairci, « Pour » sur
sa ligne, et un écran de chargement noir.)
Six retours de Diane, un par bloc :
1 la une : plus de capitales, on lit « La Charte de Communication »
2 l'onglet : son gras manquait parce que la fonte 600 n'était pas chargée
3 les onglets s'enroulent au défilement, et les sous-rangées ont leur poignée
4 le pied : « enâĄïžre » revient sur la ligne des deux puces
5 le pied du Manuel : la devise plus grande et en ambre, le catalogue retiré
6 les onze familles : une marge qui tient les numéros à deux chiffres
7 les sous-onglets : collés, sur toute la largeur, et repliés DANS la ligne
de l'onglet primaire, en sans-serif
8 « đ Les Non-Excuses » retrouve son titre de chapitre
9 « ⟠Tout dĂ©plier » passe sous le titre de la partie, et le « enâĄïžre » dore
10 le pied : les pictos reviennent DANS les boucles du « & », les deux
puces descendent à la hauteur de l'éclair, les deux blocs se
resserrent Ă la largeur du texte du dessous, et le Legs passe en
moldavite
11 le haut : la rĂšgle devient un bandeau pleine largeur et passe AVANT
le bandeau d'esprit critique, qui se colle aux onglets â et son nom
passe en gras
12 le « enâĄïžre » va au CROISEMENT des boucles du « & » â relevĂ© dans le
dessin du glyphe servi, pas devinĂ© â pendant que les deux puces
restent sur la ligne des deux formules ; et le Legs prend son đ±
et son gras
13 tout se range sur UNE SEULE LIGNE : le filet de la fraction de
gauche, la puce, « enâĄïžre », la puce, le filet de droite â et cette
ligne est le milieu de l'encre du « & »
14 plus de blanc sous le pied quand le fichier s'ouvre tout seul
15 le « & » descend juste assez pour que le haut de l'éclair touche sa
boucle · les puces se posent au milieu du creux entre la formule et
le sceau · le doré de « Pour / Le Désir de / Se Découvrir / Se
réUnir » s'éclaircit · « Pour » passe sur sa propre ligne
16 un écran de chargement NOIR, « Chargement · Protocole aMi »
18 les onglets perdent leur article (Le · La · Les · L') pour gagner de
la place · le sous-titre passe en sans-serif · le compteur reste
maigre et gris · et la rangée repliée porte le NOM du sous-onglet,
jamais son compteur
Deux retouches seulement dans la source (le lien des fontes, le <small> retiré) ;
tout le reste vit dans UN bloc marqué, qu'on retire pour revenir en arriÚre.
"""
import re, sys, hashlib
SRC = sys.argv[1]
DST = sys.argv[2]
s = open(SRC, encoding='utf-8').read()
avant = len(s)
journal = []
def une_fois(vieux, neuf, nom, attendu=1):
global s
n = s.count(vieux)
if n != attendu:
raise SystemExit(f"ARRĂT · {nom} : {n} occurrence(s), {attendu} attendue(s)")
s = s.replace(vieux, neuf)
journal.append(f"{nom} : {n} remplacement(s)")
# ââ 2 · LA FONTE QUI MANQUAIT ââââââââââââââââââââââââââââââââââââââââââââ
# .ong demande font-weight:600!important (sa consigne du 25/10, « les onglets
# doivent ĂȘtre bold en serif sinon on les lit pas sur mobile ») â mais le lien
# ne chargeait Cormorant Garamond qu'en 300 et 400. Un poids qu'on ne charge
# pas n'existe pas : le navigateur rendait le 400, ou un gras synthétique.
une_fois(
'family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400',
'family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400',
"fonte · Cormorant 500/600/700 chargés", attendu=2)
# ââ 5 · LE PIED DU MANUEL ââââââââââââââââââââââââââââââââââââââââââââââââ
vieux_pied = re.search(
r'<div class="foot">\s*\n?\s*đĄïž Une dĂ©fense qui se sait dĂ©fense a dĂ©jĂ cessĂ© dâĂȘtre une prison\.\s*\n?\s*<small>.*?</small>\s*\n?\s*</div>',
s, re.S)
if not vieux_pied:
raise SystemExit("ARRĂT · le pied du Manuel est introuvable")
une_fois(vieux_pied.group(0),
'<div class="foot" id="mn-devise">\n đĄïž Une dĂ©fense qui se sait dĂ©fense a dĂ©jĂ cessĂ© dâĂȘtre une prison.\n</div>',
"pied du Manuel · catalogue retiré, devise nommée")
# ââ 8 · LE TITRE QUE LE PLIEUR EMPORTAIT ââââââââââââââââââââââââââââââââ
# `plie()` dĂ©place hors de la tĂȘte tout enfant de .sechead qui n'est ni .num,
# ni .sec-header, ni .tag, ni un span â un <h2> posĂ© NU dans une .sechead part
# donc dans le corps, et le chapitre perd son titre. Une seule tĂȘte sur 49
# était dans ce cas. On l'enveloppe comme les 48 autres.
une_fois(
'''<span class="num">1a</span><h2>đ Les non-excuses</h2>''',
'''<span class="num">1a</span><div class="sec-header"><h2 id="man-les-non-excuses-corps">đ Les Non-Excuses</h2></div>''',
"Les Non-Excuses · le titre rendu au chapitre")
# ââ 12a · LE LEGS PREND SON PICTO âââââââââââââââââââââââââââââââââââââââ
une_fois(
'''class="f-legs" title="Legs = \u03a6 \u00d7 t \u2014 la Puissance du lien, dans la dur\u00e9e">Legs = \u03a6 \u00d7 t</a>''',
'''class="f-legs" title="Legs = \u03a6 \u00d7 t \u2014 la Puissance du lien, dans la dur\u00e9e"><span class="f-legs-e">\U0001f331</span> Legs = \u03a6 \u00d7 t</a>''',
"le Legs · son đ±")
# ââ 17 · LA NAVBAR · LES SCHĂMAS ET LES FABLES DANS L'ARCHITECTURE âââââ
# Le tiroir « temple » porte le sous-titre « L'Architecture : Les Ancres du
# Réel ». Les deux pages du site s'y ajoutent sur une deuxiÚme ligne, sans
# opérateur : ce ne sont pas les termes d'une formule, ce sont deux portes.
# Noms et émojis donnés par Diane le 26/10 à 20h08.
une_fois(
'''<span class="m-e e">\U0001f5bc\ufe0f</span>La Charte d\u2019Expression</a>
</div>''',
'''<span class="m-e e">\U0001f5bc\ufe0f</span>La Charte d\u2019Expression</a>
</div>
<div class="ln">
<a class="m" href="https://agdistys.fr/schemas"
title="\U0001f441\ufe0f Les Sch\u00e9mas Illustratifs \u2014 ce que la forme montre d\u2019un seul coup d\u2019\u0153il"><span class="m-e e">\U0001f441\ufe0f</span>Les Sch\u00e9mas Illustratifs</a>
<a class="m" href="https://agdistys.fr/fables"
title="\U0001f5e3\ufe0f Les Fables Cont\u00e9es \u2014 ce que le r\u00e9cit fait comprendre sans le dire"><span class="m-e e">\U0001f5e3\ufe0f</span>Les Fables Cont\u00e9es</a>
</div>''',
"navbar · Les Schémas et Les Fables")
# ââ 16 · L'ĂCRAN DE CHARGEMENT ââââââââââââââââââââââââââââââââââââââââââ
# Le blanc de la page se voit AVANT que la moindre feuille du bloc soit
# lue : il faut donc que ça parte de la tĂȘte. Et l'Ă©cran d'attente se
# pose juste aprĂšs <body>, hors du bloc â ainsi il ne part pas dans
# Webnode avec le bloc, et il ne couvre jamais le site.
une_fois(
'<meta name="viewport" content="width=device-width,initial-scale=1">',
'<meta name="viewport" content="width=device-width,initial-scale=1">\n'
'<meta name="color-scheme" content="dark">\n'
'<style id="ami-charte-ecran-noir">html{background:#09090e;color-scheme:dark}\n'
'body{background:#09090e;margin:0}\n'
'#ami-attente{position:fixed;inset:0;z-index:2147483000;background:#09090e;\n'
' display:flex;flex-direction:column;align-items:center;justify-content:center;\n'
' gap:.55rem;font-family:Jost,-apple-system,BlinkMacSystemFont,\'Segoe UI\',sans-serif;\n'
' transition:opacity .45s ease}\n'
'#ami-attente.parti{opacity:0;pointer-events:none}\n'
'#ami-attente .at-t{font-size:.74rem;letter-spacing:.34em;text-transform:uppercase;\n'
' color:#9595a2}\n'
'#ami-attente .at-n{font-family:\'Cormorant Garamond\',Georgia,serif;font-size:1.5rem;\n'
' letter-spacing:.02em;color:#ffd06a}\n'
'#ami-attente .at-f{width:88px;height:1px;background:linear-gradient(90deg,\n'
' rgba(200,168,74,0),#c8a84a,rgba(200,168,74,0));margin-top:.3rem}\n'
'@media(prefers-reduced-motion:reduce){#ami-attente{transition:none}}</style>',
"Ă©cran de chargement · la tĂȘte")
une_fois(
'<body><div id="ami-charte">',
'<body>\n'
'<div id="ami-attente" role="status" aria-live="polite">\n'
' <span class="at-t">Chargement</span>\n'
' <span class="at-n">Protocole aMi</span>\n'
' <span class="at-f"></span>\n'
'</div>\n'
'<script>(function(){var e=document.getElementById("ami-attente");if(!e)return;\n'
' function pars(){e.classList.add("parti");setTimeout(function(){if(e.parentNode)e.parentNode.removeChild(e);},600);}\n'
' if(document.readyState==="complete")setTimeout(pars,120);\n'
' else window.addEventListener("load",function(){setTimeout(pars,120);});\n'
' setTimeout(pars,6000);})();</script>\n'
'<div id="ami-charte">',
"écran de chargement · le voile")
# ââ LE BLOC ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
BLOC = r'''
<style id="charte-v46-2026-10-26">
/* ââ LA PASSE DU 26 OCTOBRE 2026 · neuf retours de Diane ââââââââââââââââ
Tout ce bloc se retire d'un coup pour revenir Ă la V38. */
/* 1 · LA UNE â elle se lit, elle ne crie pas.
Le dégradé doré de r57 reste ; seules les capitales s'en vont. */
#ami-charte .c-nom.r57-or{text-transform:none;letter-spacing:.015em}
/* 6 · LES ONZE FAMILLES â la marge tient « 10. » et « 11. »
à 393 px la marge tombait à 8 px : les numéros à deux chiffres
sortaient du cadre et s'affichaient « 0. » et « 1. ». */
#ami-charte .man #ami-defenses .toc{padding-left:1.1rem;padding-right:1.1rem}
#ami-charte .man #ami-defenses .toc ol{padding-left:2.1rem}
@media(max-width:700px){
#ami-charte .man #ami-defenses .toc{padding-left:.9rem!important;padding-right:.9rem!important}
#ami-charte .man #ami-defenses .toc ol{padding-left:1.9rem!important}
}
/* 5 · LA DEVISE DU MANUEL â plus grande, en ambre. */
#ami-charte .man #ami-defenses .foot#mn-devise{
font-size:1.34rem;line-height:1.5;color:var(--ambre,#ffc545)}
@media(max-width:600px){
#ami-charte .man #ami-defenses .foot#mn-devise{font-size:1.12rem}}
/* 18 · LES ONGLETS
a) le sous-titre en sans-serif â il Ă©tait en Cormorant 600, 8,96 px ;
b) le compteur du sous-onglet (« 210 ») reste maigre et gris : ce
n'est pas une information de premier rang. */
/* La derniĂšre feuille du fichier impose `font-family:inherit!important`
Ă TOUT ce qui vit dans un onglet : sans !important ici, le sous-titre
restait en Cormorant. */
#ami-charte .bar > button.ong .os,
#ami-charte .bar .ong .os{font-family:'Jost',-apple-system,sans-serif!important;
font-weight:400!important;letter-spacing:.04em}
#ami-charte .sbar .tab-btn .n,
#ami-charte .sbar .sous-btn .n{font-weight:400!important;color:var(--gris,#9595a2)!important}
/* 15 · LE PIED â le dorĂ© d'entrĂ©e, un cran plus clair, et « Pour » sur
sa propre ligne. Mesuré avant : #d1ae6b pour « Le Désir », #9f7044
pour « Pour » et « de » â deux bruns qui s'Ă©teignent sur le noir. */
#ami-charte .ami-pied .f-haut .f-or,
#ami-charte .ami-pied .f-col .f-act{color:#e2c68a}
/* 19b · LES BORDS DU DORĂ. Le dĂ©gradĂ© mĂ©tal part et finit sur #653815 â
un brun qui s'éteint sur le noir. On ne touche qu'aux deux extrémités
et au deuxiĂšme arrĂȘt ; le cĆur du mĂ©tal reste celui de la Charte. */
/* PIĂGE PAYĂ : `background` est un RACCOURCI â il remet background-clip
à sa valeur initiale et efface le `background-clip:text` du métal : les
deux mots devenaient deux rectangles dorés pleins. On n'écrit donc que
`background-image`. */
body #ami-charte .ami-pied .f-col .f-act b{
background-image:linear-gradient(100deg,#8a5526 0%,#a5753f 8%,#c19d5e 25%,#d1ae6b 28%,
#d3b16f 33%,#d9ba7d 38%,#e3c994 43%,#f2deb3 48%,#fcecc8 51%,#f1dcb1 58%,
#d6b677 71%,#d1ae6b 74%,#8a5526 100%)}
#ami-charte .ami-pied .f-haut .f-or b,
#ami-charte .ami-pied .f-col .f-act b{color:#fbeec9}
#ami-charte .ami-pied .f-haut .f-min{color:#bd9060}
#ami-charte .ami-pied .f-haut .f-min:first-child{display:block;margin-bottom:.08rem}
/* 9 · LE SCEAU DU PIED â « enâĄïžre » dorĂ©. */
#ami-charte .ami-pied .f-sceau .amp-g .f-entre{color:var(--or,#ffc545)}
/* 10 · LE PIED
a) les deux blocs se resserrent sur la largeur de la ligne du dessous
â la mesure est prise au rendu et posĂ©e dans --f-largeur ;
b) le Legs en moldavite, comme dans l'Initiation : une rÚgle dorée
posée plus bas le repeignait en #ffd06a. */
#ami-charte .ami-pied .f-grille{max-width:min(calc(880px * var(--sc,.86)),
var(--f-largeur, 880px))}
#ami-charte .ami-pied .ap-fin .f-legs,
body #ami-charte .ami-pied .ap-fin .f-legs{color:var(--moldavite,#62a802);font-weight:600}
#ami-charte .ami-pied .ap-fin .f-legs .f-legs-e{font-style:normal;font-weight:400;
margin-right:.12em}
/* 11 · LE HAUT DE PAGE
La rÚgle devient un bandeau pleine largeur, posée AVANT le bandeau
d'esprit critique â qui vient, lui, se coller aux onglets. */
#ami-charte .regle.v45-bandeau{max-width:none;margin:1.1rem 0 0;border-radius:0;
padding:1.25rem clamp(1rem,4vw,3rem)}
#ami-charte .ami-bande-pli.v45-collee{margin-bottom:0!important}
#ami-charte .ami-bande-pli.v45-collee + .bar,
#ami-charte .ami-bande-pli.v45-collee ~ .bar{margin-top:0!important}
#ami-charte .ami-bande-pli .br-pli-n{font-weight:600}
/* 7 · LES SOUS-ONGLETS
PIĂGE PAYĂ LE 26/10 : poser `position:relative` sur .sbar TUE son
`position:sticky` â mĂȘme poids de sĂ©lecteur, la rĂšgle la plus basse
gagne â et la rangĂ©e part Ă la dĂ©rive au milieu de la page. On n'y
touche pas : un élément collant est déjà un repÚre pour ses enfants
en position absolue.
a) ils prennent toute la largeur, au lieu d'un petit groupe centré ;
b) repliés, ils n'ouvrent PAS une deuxiÚme ligne : le sous-onglet
ouvert vient se poser dans la ligne de l'onglet primaire, en
sans-serif, comme la Charte le dit d'elle-mĂȘme. */
#ami-charte .bar:has(~ .sbar:not([hidden])){margin-bottom:0!important}
#ami-charte .sbar{margin-top:-1px}
#ami-charte .sbar .sog,
#ami-charte .sbar .tab-btn,
#ami-charte .sbar .sous-btn{flex:1 1 auto;max-width:50%}
#ami-charte .sbar.sous-dans-la-ligne{display:none!important}
#ami-charte .bar .v40-sous{display:none}
#ami-charte .bar.enroulee .v40-sous{display:inline-flex;align-items:baseline;
gap:.34rem;margin-left:.2rem;min-width:0;
font-family:'Jost',-apple-system,sans-serif;font-weight:600;
font-size:.8rem;letter-spacing:.03em;color:var(--tcl,var(--tc,#c8a84a));
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#ami-charte .bar.enroulee .v40-sous .v40-pt{opacity:.55;font-weight:400}
@media print{#ami-charte .plier{display:none!important}
#ami-charte .bar.enroulee .ong{display:flex!important}
#ami-charte .sbar{display:flex!important}
#ami-charte .bar .v40-sous{display:none!important}}
</style>
<script id="charte-v46-js-2026-10-26">
(function(){
'use strict';
var R = document.getElementById('ami-charte') || document.body;
if(!R) return;
/* ââ 4 · LE SCEAU DU PIED · « enâĄïžre » sur la ligne des deux puces âââ
On ne devine pas l'écart : on le MESURE. Deux émojis au-dessus, un
seul en dessous â le mot tombait 7 px sous l'axe des puces. MesurĂ©
au rendu, le décalage vaut sur n'importe quelle fonte d'émoji. */
/* ââ 4 et 10 · LE SCEAU DU PIED ââââââââââââââââââââââââââââââââââââââ
LEĂON PAYĂE : j'avais dĂ©placĂ© la COLONNE DE PICTOS pour amener le mot
sur la ligne des deux puces. C'était l'inverse qu'il fallait faire :
la colonne tombe DANS les boucles du « & » â l'Ă©clair sur le
croisement â et ce sont les PUCES qui reviennent Ă sa hauteur, comme
la Charte l'Ă©crit elle-mĂȘme. On ne bouge donc plus les pictos, on
mesure l'éclair et on y descend les points. */
/* ââ 13 · UNE SEULE LIGNE AU PIED âââââââââââââââââââââââââââââââââââ
Sa rĂšgle du 26/10 19h05, et elle est simple une fois dite : le filet
de la fraction de gauche, la puce, « enâĄïžre », la puce et le filet de
droite sont sur LA MĂME ligne â et cette ligne est le milieu de
l'encre du « & » (le trait bleu du relevé, juste sous le rouge).
Deux essais pour l'entendre : j'avais d'abord descendu les puces vers
le mot (V41), puis monté le mot sur le croisement des boucles (V42).
Ce n'était ni l'un ni l'autre : tout se range sur une ligne.
PIĂGE PAYĂ : Ă©crire `transform` en ligne EFFACE le translateX que la
feuille pose (--gly-dx : les 9,7 % qui font tomber les glyphes dans
les boucles et non sur la panse). On le reporte toujours. */
function sceau(){
var g = R.querySelector('.ami-pied .f-sceau .amp-g');
var sc = R.querySelector('.ami-pied .f-sceau');
var te = R.querySelector('.ami-pied .f-sceau .f-entre');
var pts = R.querySelectorAll('.ami-pied .f-amp .f-pt');
var barre = R.querySelector('.ami-pied .f-eq .frac span + span');
if(!g || !sc || !te) return;
/* on repart de la place naturelle, en gardant le décalage horizontal */
var dx = (getComputedStyle(g).getPropertyValue('--gly-dx') || '0px').trim() || '0px';
g.style.transform = 'translateX(' + dx + ')';
[].forEach.call(pts, function(pt){ pt.style.transform = ''; });
var rs = sc.getBoundingClientRect();
if(!rs.height) return;
/* LA LIGNE. Le milieu de l'encre du « & » et le filet de la fraction
sont à un pixel l'un de l'autre (mesuré). On prend le FILET comme
rĂ©fĂ©rence, puisque c'est lui qu'on voit des deux cĂŽtĂ©s â et on ne
déplace jamais le sceau pour l'y amener : il porte la ligne, il ne
la suit pas. */
var LIGNE = rs.top + rs.height / 2;
if(barre){
var rb0 = barre.getBoundingClientRect();
if(rb0.height && Math.abs(rb0.top - LIGNE) < 12) LIGNE = rb0.top;
}
/* le mot y monte */
var rt = te.getBoundingClientRect();
if(rt.height){
var dy = LIGNE - (rt.top + rt.height/2);
if(Math.abs(dy) >= 0.4)
g.style.transform = 'translate(' + dx + ',' + dy.toFixed(2) + 'px)';
}
/* les puces aussi â et les filets de fraction sont dĂ©jĂ lĂ : on le
VĂRIFIE au lieu de le supposer, et on les y amĂšne sinon. */
/* ââ 15a · LE « & » DESCEND JUSTE ASSEZ âââââââââââââââââââââââââââââ
Sa rĂšgle du 19h28 : « le haut de âĄïž doit toucher la boucle du & ».
On ne baisse donc pas « un chouilla » au jugé : on relÚve dans le
DESSIN du glyphe la bande de lignes SANS trou qui sépare les deux
contre-formes â c'est lĂ que les boucles se croisent â et on pose
l'esperluette pour que le bas de sa boucle haute tombe sur le haut
de l'éclair. Mesuré avant : 2,6 px de trop-haut. */
var fin = R.querySelector('.ami-pied .ap-fin');
var ecl = R.querySelector('.ami-pied .f-entre .e');
if (fin && ecl){
if (BOUCLE === null) BOUCLE = basDeLaBoucleHaute();
if (BOUCLE !== null){
var re2 = ecl.getBoundingClientRect(), rs2 = sc.getBoundingClientRect();
if (re2.height && rs2.height){
/* PIĂGE : la boĂźte du sceau ne bouge pas avec --amp-dy, c'est
l'ENCRE qui bouge. Une correction cumulĂ©e diverge donc Ă
chaque passage (mesurĂ© : -1 â 1,6 â 4,2 â 6,8 px). On pose
une valeur ABSOLUE, pas un ajout. */
var vise = re2.top - (rs2.top + BOUCLE * rs2.height);
if (Math.abs(vise) < 24)
fin.style.setProperty('--amp-dy', vise.toFixed(2) + 'px');
}
}
}
/* ââ 15b · LES PUCES AU MILIEU DU CREUX âââââââââââââââââââââââââââââ
Entre le bord du bloc de formules et le bord du sceau. On mesure le
creux au lieu de s'en remettre à l'écart du flex, qui colle la puce
au « & » dÚs que la colonne est plus large que son texte. */
var cols = R.querySelectorAll('.ami-pied .f-col');
var bordG = null, bordD = null;
if (cols[0]) [].forEach.call(cols[0].children, function(e){
var r = e.getBoundingClientRect(); if(!r.width) return;
if (bordG === null || r.right > bordG) bordG = r.right; });
if (cols[1]) [].forEach.call(cols[1].children, function(e){
var r = e.getBoundingClientRect(); if(!r.width) return;
if (bordD === null || r.left < bordD) bordD = r.left; });
var rsc = sc.getBoundingClientRect();
var creux = [ (bordG !== null ? (bordG + rsc.left)/2 : null),
(bordD !== null ? (rsc.right + bordD)/2 : null) ];
[].forEach.call(pts, function(pt, i){
var rp = pt.getBoundingClientRect();
if(!rp.height) return;
var dy2 = LIGNE - (rp.top + rp.height/2);
var dx2 = 0;
if (creux[i] !== null && isFinite(creux[i])){
var cc = creux[i] - (rp.left + rp.width/2);
if (Math.abs(cc) < 140) dx2 = cc;
}
if (Math.abs(dy2) < 0.4 && Math.abs(dx2) < 0.4) return;
pt.style.transform = 'translate(' + dx2.toFixed(2) + 'px,' + dy2.toFixed(2) + 'px)';
});
}
/* Le bas de la boucle haute du « & », relevé dans le dessin du glyphe
servi. Rien n'est écrit en dur : une autre fonte, une autre valeur. */
var BOUCLE = null;
function basDeLaBoucleHaute(){
var w = R.querySelector('.ami-pied .amp-w');
if(!w) return null;
try{
var cs2 = getComputedStyle(w), T = 240;
var cv = document.createElement('canvas'); cv.width = T; cv.height = T;
var x = cv.getContext('2d', {willReadFrequently:true});
if(!x) return null;
x.fillStyle = '#000'; x.fillRect(0,0,T,T);
x.fillStyle = '#fff'; x.textAlign = 'center'; x.textBaseline = 'alphabetic';
x.font = (cs2.fontWeight || '700') + ' ' + Math.round(T*0.72) + 'px ' + cs2.fontFamily;
x.fillText('&', T/2, T*0.82);
var d = x.getImageData(0,0,T,T).data, li = [];
for (var y=0; y<T; y++){
var runs = 0, dans = false, vu = false;
for (var i=0; i<T; i++){
var v = d[(y*T+i)*4] > 90;
if (v && !dans){ dans = true; runs++; vu = true; }
if (!v && dans) dans = false;
}
if (vu) li.push({y:y, trous:runs-1});
}
if (li.length < 12) return null;
var h0 = li[0].y, b0 = li[li.length-1].y, H = b0-h0+1, grd = H*0.08;
var grp = [], cur = [];
for (var k=1; k<li.length-1; k++){
var L = li[k];
if (L.trous === 0 && L.y > h0+grd && L.y < b0-grd){
if (cur.length && L.y === cur[cur.length-1]+1) cur.push(L.y);
else { if(cur.length) grp.push(cur); cur = [L.y]; }
}
}
if (cur.length) grp.push(cur);
if (!grp.length) return null;
grp.sort(function(a,b){ return b.length - a.length; });
var pct = (grp[0][0] - h0) / H;
return (pct > 0.12 && pct < 0.88) ? pct : null;
}catch(e){ return null; }
}
/* ââ 19 · « POUR LE DĂSIR DE » âââââââââââââââââââââââââââââââââââââââ
Deux réglages, tous deux mesurés au rendu :
a) L'AXE. La ligne est bien centrée sur la page (640 px), mais la
PAIRE « Se Découvrir · Se réUnir » ne l'est pas : la colonne de
gauche est alignée à droite, celle de droite à gauche, et les deux
mots n'ont pas la mĂȘme longueur â leur milieu optique tombe Ă
629,7, soit 10,3 px Ă gauche. L'Ćil lit l'Ă©cart. On recale donc la
ligne sur la paire, pas sur la page.
b) LA HAUTEUR. « Pour » et « Le Désir de » sont séparés de 2,3 px,
mais « Le Désir de » et « Se Découvrir » de 15,7 : ça se lit comme
un double saut de ligne. On ramÚne l'écart à 6 px. */
function pourLeDesir(){
var haut = R.querySelector('.ami-pied .f-haut');
var acts = R.querySelectorAll('.ami-pied .f-col .f-act');
if(!haut || acts.length < 2 || !document.createRange) return;
var rg = document.createRange();
var enc = function(e){ rg.selectNodeContents(e); return rg.getBoundingClientRect(); };
var a = enc(acts[0]), z = enc(acts[1]);
if(!a.width || !z.width) return;
haut.style.transform = '';
var axe = (a.left + z.right) / 2;
var b = haut.querySelector('b');
if (b){
var rb = enc(b).left, rd = rb;
var mins = haut.querySelectorAll('.f-min');
var der = mins.length > 1 ? enc(mins[mins.length-1]).right : enc(b).right;
var centre = (enc(b).left + der) / 2;
var dx = axe - centre;
if (Math.abs(dx) >= 0.4 && Math.abs(dx) < 80)
haut.style.transform = 'translateX(' + dx.toFixed(2) + 'px)';
}
haut.style.marginBottom = '';
var basTexte = (b ? enc(b).bottom : haut.getBoundingClientRect().bottom);
var hautAct = Math.min(a.top, z.top);
var vide = hautAct - basTexte;
var mb = parseFloat(getComputedStyle(haut).marginBottom) || 0;
if (vide > 6.5 && vide < 60)
haut.style.marginBottom = (mb - (vide - 6)).toFixed(2) + 'px';
}
/* ââ 14 · LE BLANC SOUS LE PIED ââââââââââââââââââââââââââââââââââââââ
Ouvert tout seul, le fichier laisse voir le blanc de la page sous le
bloc (html et body sont transparents, et body garde sa marge de
8 px). Dans Webnode le bloc est enfoui sous les enveloppes du site :
il n'est JAMAIS enfant direct de body. C'est donc ce test-lĂ , et pas
une supposition, qui dĂ©cide â et la couleur se lit sur le bloc. */
function fondDeLaPage(){
if(!R || R.parentElement !== document.body) return;
var fond = getComputedStyle(R).backgroundColor;
if(!fond || fond === 'rgba(0, 0, 0, 0)' || fond === 'transparent') return;
document.documentElement.style.background = fond;
document.body.style.background = fond;
}
/* ââ 10c · LES DEUX BLOCS Ă LA LARGEUR DU TEXTE DU DESSOUS âââââââââââ
On mesure l'ENCRE de « Tu es un Beau Cadeau⊠», pas sa boßte, qui
fait toute la page. Une fonte plus large donnera une grille plus
large : c'est voulu, les deux bougent ensemble. */
function largeurDuPied(){
var bas = R.querySelector('.ami-pied .f-bas');
var fin = R.querySelector('.ami-pied .ap-fin');
var amp = R.querySelector('.ami-pied .f-amp');
if(!bas || !fin || !amp || !document.createRange) return;
var rg = document.createRange();
rg.selectNodeContents(bas);
var vise = rg.getBoundingClientRect().width; /* la largeur visée */
if(!(vise > 40)) return;
/* Un plancher qui ne se devine pas : la plus large ligne des deux
colonnes, doublée, plus le sceau. En deçà , « Plaisir d'Offrir »
viendrait toucher le « & ». */
var large = 0;
[].forEach.call(R.querySelectorAll('.ami-pied .f-col > *'), function(e){
var w = e.getBoundingClientRect().width;
if(w > large) large = w;
});
var plancher = 2 * large + amp.getBoundingClientRect().width + 48;
fin.style.setProperty('--f-largeur', Math.round(Math.max(vise, plancher)) + 'px');
}
/* ââ 11 · LE HAUT DE PAGE ââââââââââââââââââââââââââââââââââââââââââââ
La rĂšgle sort de son <main> â qui la rĂ©trĂ©cissait Ă 1 080 px â et
vient se poser juste aprĂšs la une, en bandeau pleine largeur ; le
bandeau d'esprit critique passe derriÚre elle, collé aux onglets. */
function hautDePage(){
var regle = R.querySelector('.regle');
var bande = R.querySelector('.ami-bande-pli');
var barre = R.querySelector('.bar');
if(!regle || !bande || !barre) return;
if(!bande.classList.contains('v45-collee')) bande.classList.add('v45-collee');
if(!regle.classList.contains('v45-bandeau')) regle.classList.add('v45-bandeau');
/* l'ordre voulu : la une · la rÚgle · le bandeau · les onglets */
if(regle.nextElementSibling !== bande){
bande.parentNode.insertBefore(regle, bande);
}
if(bande.nextElementSibling !== barre){
barre.parentNode.insertBefore(bande, barre);
}
var vide = R.querySelector('main:empty');
if(vide) vide.style.display = 'none';
}
/* ââ 9 · « ⟠TOUT DĂPLIER » SOUS LE TITRE DE LA PARTIE âââââââââââââââ
La Charte le dit d'elle-mĂȘme â « en pastille dorĂ©e, sous le titre de
la partie » â mais son script le posait en tĂȘte du volet, donc
AU-DESSUS du portail. On le repose juste aprĂšs. */
function boutonSousLePortail(){
[].forEach.call(R.querySelectorAll('section.pan'), function(pan){
var b = pan.querySelector(':scope > .ch-tout');
var portail = pan.querySelector(':scope > .portail');
if(!b || !portail) return;
if(b.previousElementSibling === portail) return;
portail.parentNode.insertBefore(b, portail.nextSibling);
});
}
/* ââ 7 · LE SOUS-ONGLET DANS LA LIGNE ââââââââââââââââââââââââââââââââ
Le nom se LIT sur l'onglet, il n'est jamais recopié en dur. */
/* ââ 18a · LES ONGLETS SANS ARTICLE ââââââââââââââââââââââââââââââââââ
Sa rÚgle du 19h52 : « retire les articles (la le les), comme ça on a
un peu plus de place pour le texte, car c'est petit ». On ne retire
que l'article de TĂTE, jamais un « du » ou un « de » Ă l'intĂ©rieur ;
et le nom entier est gardé sur l'élément (data-plein + title), pour
que rien ne disparaisse. */
var ARTICLE = /^(?:Les|Le|La)\s+|^L[\u2019']\s*/;
function sansArticle(el){
if(!el || el.dataset.v45 === '1') return;
var plein = (el.textContent || '').trim();
var court = plein.replace(ARTICLE, '');
el.dataset.v45 = '1';
if(court === plein || !court) return;
el.dataset.plein = plein;
el.textContent = court;
var bt = el.closest('button, a');
if(bt && !bt.getAttribute('title')) bt.setAttribute('title', plein);
}
function onglelsSansArticle(){
[].forEach.call(R.querySelectorAll('.bar .ong .ot, .sbar .tb-lbl, .sbar .sous-btn .t'),
sansArticle);
}
/* ââ 18b · LE NOM DU SOUS-ONGLET, PAS SON COMPTEUR âââââââââââââââââââ
Le bouton « đ€ DĂ©finitions<span class="n">210</span> » n'a pas de
libellé à lui : mon repli prenait donc son PREMIER enfant, c'est-à -
dire le compteur, et affichait « 210 » en gras dans la ligne. On lit
maintenant le texte du bouton en ayant retiré compteurs et
sous-titres, et on enlĂšve l'Ă©moji de tĂȘte : l'onglet primaire porte
déjà le sien. */
function nomDu(bouton){
if(!bouton) return '';
var n = bouton.querySelector('.tb-lbl, .t, .ot');
var txt;
if(n){ txt = n.textContent; }
else {
var c = bouton.cloneNode(true);
[].forEach.call(c.querySelectorAll('.n, .tb-pol, .d, .os, .cpt'),
function(e){ e.remove(); });
txt = c.textContent;
}
txt = (txt || '').trim();
try{ txt = txt.replace(/^[\p{Extended_Pictographic}\uFE0F\u200D\s]+/u, ''); }
catch(e){ txt = txt.replace(/^[^\p{L}\p{N}]+/u, ''); }
return txt.replace(ARTICLE, '').trim();
}
function ligne(){
var bar = R.querySelector('.bar');
if(!bar) return;
var enroule = bar.classList.contains('enroulee');
var sb = [].filter.call(R.querySelectorAll('.sbar'), function(s){
return !s.hidden && s.getBoundingClientRect().height > 0 || (enroule && !s.hidden);
})[0];
var actif = bar.querySelector('.ong.on');
var vieux = bar.querySelector('.v40-sous');
[].forEach.call(R.querySelectorAll('.sbar'), function(s){
s.classList.toggle('sous-dans-la-ligne', enroule);
});
if(!enroule || !sb || !actif){ if(vieux) vieux.remove(); return; }
var nom = nomDu(sb.querySelector('[aria-selected="true"], .on'));
if(!nom){ if(vieux) vieux.remove(); return; }
var sp = vieux;
if(!sp){
sp = document.createElement('span');
sp.className = 'v40-sous';
sp.setAttribute('aria-hidden','true');
sp.innerHTML = '<span class="v40-pt">·</span><span class="v40-n"></span>';
}
sp.querySelector('.v40-n').textContent = nom;
if(actif.nextSibling !== sp) actif.parentNode.insertBefore(sp, actif.nextSibling);
}
/* ââ 3 · LE REPLI AU DĂFILEMENT ââââââââââââââââââââââââââââââââââââââ
La poignée ⎠existait, mais il fallait aller la chercher : la rangée
ne s'enroulait jamais toute seule. Elle est présentée à l'arrivée,
s'enroule dÚs qu'elle se colle, et se redéploie quand on remonte. */
var bar = R.querySelector('.bar');
var mainPosee = 0; /* un choix Ă la main vit quatre secondes */
/* UNE RANGĂE COLLĂE MENT SUR SA PLACE : son offsetTop suit le collage.
On pose donc un fantĂŽme de hauteur nulle juste avant elle. */
var fantome = null;
if(bar && bar.parentNode){
fantome = document.createElement('div');
fantome.setAttribute('aria-hidden','true');
fantome.style.cssText = 'height:0;margin:0;padding:0;border:0';
bar.parentNode.insertBefore(fantome, bar);
}
/* La poignĂ©e est fabriquĂ©e par la Charte APRĂS ce script : on ne peut pas
s'y accrocher directement. On délÚgue, et surtout on SURVEILLE la classe
de la rangĂ©e â ainsi la ligne suit, qui que ce soit qui la replie. */
if(bar){
bar.addEventListener('click', function(e){
if(e.target.closest && e.target.closest('.plier')) mainPosee = Date.now();
}, true);
if(window.MutationObserver){
new MutationObserver(function(){ ligne(); })
.observe(bar, {attributes:true, attributeFilter:['class']});
}
}
/* UN DĂFILEMENT QU'ON PROVOQUE SOI-MĂME N'EST PAS UN DĂFILEMENT.
Un clic d'onglet fait descendre la page jusqu'à la rangée : sans cette
garde, la rangĂ©e s'enroulait Ă l'instant mĂȘme oĂč on vient de l'ouvrir,
et on ne voyait jamais ses sous-onglets. On la PRĂSENTE, et elle ne
s'enroule qu'au premier vrai mouvement â 120 px suffisent. */
var presentation = null;
function presente(){
presentation = window.scrollY;
if(bar) bar.classList.remove('enroulee');
ligne();
}
function lire(){
if(Date.now() - mainPosee < 4000) return;
if(!bar || !fantome) return;
if(presentation !== null){
if(Math.abs(window.scrollY - presentation) < 120) return;
presentation = null;
}
var collee = (parseFloat(getComputedStyle(bar).top) || 0) + 2;
var enroule = fantome.getBoundingClientRect().top <= collee;
if(bar.classList.contains('enroulee') !== enroule){
bar.classList.toggle('enroulee', enroule);
}
ligne();
}
/* UNE TRANSITION SE JUGE QUAND LE DĂFILEMENT EST REPOSĂ. */
var tempo = null;
function auDefilement(){ clearTimeout(tempo); tempo = setTimeout(lire, 180); }
window.addEventListener('scroll', auDefilement, {passive:true});
window.addEventListener('resize', function(){ sceau(); largeurDuPied(); pourLeDesir(); boutonSousLePortail(); auDefilement(); });
/* Un clic d'onglet change la sous-rangée : on la regarde à nouveau,
aprÚs le silence que la Charte s'accorde déjà . */
R.addEventListener('click', function(e){
if(!e.target.closest) return;
if(e.target.closest('.ong, .sog, .sous-btn, .tab-btn')){
setTimeout(function(){ onglelsSansArticle(); presente(); sceau(); }, 720);
setTimeout(ligne, 1400);
}
}, true);
function depart(){ fondDeLaPage(); hautDePage(); largeurDuPied(); sceau(); pourLeDesir(); boutonSousLePortail(); onglelsSansArticle(); ligne(); }
if(document.readyState === 'complete') setTimeout(depart, 260);
else window.addEventListener('load', function(){ setTimeout(depart, 260); });
setTimeout(depart, 1200); setTimeout(boutonSousLePortail, 2000);
if(document.fonts && document.fonts.ready) document.fonts.ready.then(function(){ setTimeout(function(){ largeurDuPied(); sceau(); pourLeDesir(); }, 140); });
})();
</script>
'''
i = s.rfind('</body>')
if i < 0:
raise SystemExit("ARRĂT · pas de </body>")
s = s[:i] + BLOC + s[i:]
journal.append("bloc charte-v46-2026-10-26 posé avant </body>")
open(DST, 'w', encoding='utf-8').write(s)
print("\n".join(" · " + l for l in journal))
print(f"\n{avant} â {len(s)} caractĂšres ({len(s)-avant:+d})")
print("md5 :", hashlib.md5(open(DST,'rb').read()).hexdigest()[:12])
đ Apokalypse · le sceau portĂ© de la Charte, les titres rendus Ă leurs onglets â 27 octobre 2026
Sa rĂšgle du matin : « tu veux pas plutĂŽt copier coller celui de la charte directement ? plus simple. » Elle a raison, et pas seulement pour la simplicitĂ© : on ne reproduit pas un rĂ©glage, on transporte le bloc qui marche â ce qui transforme un accordage Ă l'Ćil en une greffe vĂ©rifiable. RelevĂ© des quatre Ă©carts entre les deux pieds, avant de toucher Ă quoi que ce soit : la colonne de glyphes portait « âĄïžÂ » nu lĂ oĂč la Charte y met « enâĄïžre » ; « Se rĂ©Unir » traĂźnait un « entre » mort ; la formule de droite gardait le « à t » qui part avec le Legs ; et la ligne « đ± Legs = Ί Ă t » manquait.
Le sceau, mesurĂ© aprĂšs â le filet de la fraction de gauche, la puce, « enâĄïžre », la puce et le filet de droite sont sur la mĂȘme ligne, Ă l'Ă©cart 0,00 px (ligne y = 4008,7). Le Legs rend sa moldavite rgb(98, 168, 2), le mĂ©tal garde son background-clip:text, le « entre » mort a disparu de la ligne des mots et la formule de droite s'Ă©crit bien Ί = (U Ă I)/(R Ă P).
Les huit titres de parties â sa rĂšgle : « l'onglet = le titre et le sous onglet le sous titre ». Avant : on cliquait « đïž DĂ©ni & Emprise » et on atterrissait sur « Lever le Brouillard » â l'onglet cliquĂ© et le titre lu ne se rĂ©pondaient pas. AprĂšs : les huit titres sont leur onglet, emoji compris, et les huit sous-titres sont son sous-titre. Rien n'a disparu : le nom propre de la partie et sa ligne descendent d'un cran dans un troisiĂšme fil. Le bloc est centrĂ© (centre 632 = centre du parent), sans filet (border-left: 0px) ni dĂ©gradĂ©.
L'appariement se lit, il ne se compte pas â chaque prĂ©sentation est reliĂ©e Ă son onglet par aria-labelledby, pas par son rang : c'est le document qui dit la correspondance. Un outil qui apparierait par l'ordre marcherait aujourd'hui et mentirait dĂšs qu'un onglet se dĂ©place.
TĂ©moin, avant contre aprĂšs , tout dĂ©pliĂ© Ă 1 280 px : 41 722 â 41 778 caractĂšres (+56 : la ligne du Legs et les huit fils), et 251 replis · 8 onglets · 76 liens · 268 titres · 304 dĂ©finitions identiques . 0 erreur JS avant comme aprĂšs. La rangĂ©e d'onglets est static dans les deux fichiers â elle l'Ă©tait dĂ©jĂ , la passe n'y a pas touchĂ©.
L'Ă©cran blanc de l'aperçu, Ă©lucidĂ© le mĂȘme matin : Apokalypse est un cadre hĂŽte Ă cinq niveaux, six documents imbriquĂ©s, chacun construit par le script du prĂ©cĂ©dent. MesurĂ© â ouverture normale : 6 cadres , le volume trouvĂ©, 4 116 px de haut, 0 erreur ; ouverture dans un bac Ă sable qui refuse les scripts : 2 cadres , rien. Un aperçu sans scripts ne peut construire aucun niveau : il affiche la page hĂŽte â donc l'Ă©cran de chargement, qui est du CSS pur â puis le vide. L'Ă©cran de chargement n'a rien cassĂ© : il a rendu visible un vide qui existait dĂ©jĂ . Ă vĂ©rifier dans un vrai navigateur avant de conclure Ă une rĂ©gression.
apo_passe3.py python3 apo_passe3.py avant.html apres.html402 lignes
Porte le sceau du pied de la Charte V46 dans Apokalypse et rend aux huit prĂ©sentations de parties le titre de leur onglet. Quatre Ă©changes de balisage comptĂ©s un Ă un â un motif qui ne mord plus ARRĂTE la construction â plus un bloc marquĂ© apo-passe3-2026-10-27 qui se retire d'un coup. Les rĂ©glages qui dĂ©pendent de la fonte (croisement du « & », largeur d'encre, alignement des puces) sont mesurĂ©s au rendu, jamais Ă©crits en dur. Les deux piĂšges payĂ©s sont notĂ©s dedans , Ă l'endroit oĂč ils mordent.
â§ Copier le script
# -*- coding: utf-8 -*-
"""
apo_passe3.py â la troisiĂšme passe sur 0.1 APOKALYPSE · 27 octobre 2026.
Deux points de la liste de Diane, chacun mesuré avant et aprÚs :
A · LE SCEAU DU PIED â copiĂ© de la Charte V46, pas reconstruit.
C · LES TITRES DE PARTIES â « l'onglet = le titre, le sous-onglet = le
sous-titre », sans rien faire disparaßtre.
LA CONVENTION DU CADRE HĂTE, TRANCHĂE PAR LA MESURE (27/10 06h20) :
à cette profondeur, les guillemets ET les esperluettes sont échappés.
Preuve : le bandeau porte « &#9662; » et le rendu affiche « ⟠».
Si on n'échappait que les guillemets, le chevron s'afficherait en clair.
D'oĂč E() ci-dessous â et l'ordre compte : l'esperluette d'abord.
"""
import io, re, sys, hashlib
SRC, DST = sys.argv[1], sys.argv[2]
s = io.open(SRC, encoding='utf-8').read()
avant = len(s)
J = []
def E(t):
return t.replace('&', '&').replace('"', '"')
def une_fois(vieux, neuf, nom, attendu=1):
global s
n = s.count(vieux)
if n != attendu:
raise SystemExit("ARRĂT · %s : %d occurrence(s), %d attendue(s)" % (nom, n, attendu))
s = s.replace(vieux, neuf)
J.append(nom)
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# A · LE SCEAU DU PIED â copiĂ© de la Charte V46
#
# Sa rÚgle du 27/10 : « tu veux pas plutÎt copier coller celui de la charte
# directement ? plus simple. » Elle a raison, et pas seulement pour la
# simplicité : on ne reproduit pas un réglage, on transporte le bloc qui
# marche. Relevé des quatre écarts entre les deux pieds :
# 1. la colonne de glyphes porte « âĄïž » nu â la Charte y met « enâĄïžre »
# 2. « Se rĂ©Unir » traĂźne un « entre » mort â le mot vit dans le sceau
# 3. la formule de droite porte « Ă t » â le t part avec le Legs
# 4. la ligne « đ± Legs = Ί Ă t » manque â on la pose
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
une_fois(E('<span class="amp-g"><span class="e">đ</span><span class="e">đ</span>'
'<span class="e">âĄïž</span><span class="e">đ©ž</span></span>'),
E('<span class="amp-g"><span class="e">đ</span><span class="e">đ</span>'
'<span class="f-entre">en<span class="e">âĄïž</span>re</span>'
'<span class="e">đ©ž</span></span>'),
"A1 · le sceau · « enâĄïžre » entre les glyphes, comme la Charte")
une_fois(E('<b>Se réUnir</b> <span class="f-min">entre</span>'),
E('<b>Se réUnir</b>'),
"A2 · « entre » retiré de la ligne des mots (il est dans le sceau)")
une_fois(E('class="f-eq turq" title="Ί = (U Ă I) / (R Ă P) Ă t â la Puissance du lien dans la durĂ©e">Ί =')
+ '\n ' +
E('<span class="frac"><span>(U Ă I)</span><span>(R Ă P)</span></span> Ă t</a>'),
E('class="f-eq turq" title="Ί = (U Ă I) / (R Ă P) â la Puissance du lien ; '
'dans la durée, Legs = Ί à t">Ί = '
'<span class="frac"><span>(U Ă I)</span><span>(R Ă P)</span></span></a>'),
"A3 · « à t » quitte la formule et part avec le Legs")
une_fois(E('>Tu es un Beau Cadeau fait Ă Moi-MĂȘme & Je-u Tâaime</a>'),
E('>Tu es un Beau Cadeau fait Ă Moi-MĂȘme & Je-u Tâaime</a>\n '
'<a href="https://agdistys.fr/union-du-tout" class="f-legs" '
'title="Legs = Ί Ă t â la Puissance du lien, dans la durĂ©e">'
'<span class="f-legs-e">đ±</span> Legs = Ί Ă t</a>'),
"A4 · la ligne « đ± Legs = Ί Ă t » posĂ©e sous le Cadeau")
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# C · LES TITRES DE PARTIES
#
# Sa rÚgle du 27/10 : « moi c'est plus l'onglet = le titre et le sous onglet
# le sous titre ». RelevĂ© avant : on clique « đïž DĂ©ni & Emprise » et on
# atterrit sur un titre qui dit « Lever le Brouillard » â l'onglet cliquĂ© et
# le titre lu ne se répondent pas. Et le bloc portait un filet à gauche et
# un dégradé : ses « lignes » à retirer.
#
# ON NE PERD RIEN : le nom propre de la partie et sa ligne descendent d'un
# cran, dans un troisiĂšme fil. L'appariement se fait par aria-labelledby,
# pas par l'ordre â c'est le document qui le dit, pas moi qui le suppose.
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Q = '"'
onglets = {}
for m in re.finditer(r'<button[^>]*?id=' + Q + r'(ami-volume-tab-\d+)' + Q + r'[^>]*>(.*?)</button>',
s, re.S):
onglets[m.group(1)] = m.group(2)
if len(onglets) != 8:
raise SystemExit("ARRĂT · %d onglets de partie relevĂ©s, 8 attendus" % len(onglets))
def part(bloc, cls):
m = re.search(r'<span[^>]*class=' + Q + r'[^' + Q + r']*' + cls + r'[^' + Q + r']*' + Q
+ r'[^>]*>(.*?)</span>', bloc, re.S)
return m.group(1).strip() if m else None
n_titres = 0
def refais(m):
global n_titres
tete, corps = m.group(0), m.group(2)
idt = m.group(1)
bloc = onglets.get(idt)
if bloc is None:
return tete
nom = part(bloc, 'ami-tab-text')
pol = part(bloc, 'ami-tab-pol')
emo = part(bloc, 'ami-tab-icon') # .ami-emoji n'existe QU'AU RENDU
if not nom or not pol:
return tete
anc_t = re.search(r'<div class=' + Q + r'ami-volume-panel-title' + Q + r'>(.*?)</div>', corps, re.S)
anc_s = re.search(r'<div class=' + Q + r'ami-volume-panel-subtitle' + Q + r'>(.*?)</div>', corps, re.S)
sour = re.search(r'<div class=' + Q + r'ami-volume-panel-eyebrow' + Q + r'>(.*?)</div>', corps, re.S)
if not (anc_t and anc_s and sour):
return tete
fil = anc_t.group(1).strip() + ' · ' + anc_s.group(1).strip()
neuf = ('<header class=' + Q + 'ami-volume-panel-intro p3-centre' + Q + '>'
'<div class=' + Q + 'ami-volume-panel-eyebrow' + Q + '>' + sour.group(1).strip() + '</div>'
'<div class=' + Q + 'ami-volume-panel-title' + Q + '>'
+ (('<span class=' + Q + 'e' + Q + '>' + emo + '</span> ') if emo else '')
+ nom + '</div>'
'<div class=' + Q + 'ami-volume-panel-subtitle' + Q + '>' + pol + '</div>'
'<div class=' + Q + 'ami-volume-panel-fil' + Q + '>' + fil + '</div>'
'</header>')
n_titres += 1
return neuf
s = re.sub(r'<section[^>]*aria-labelledby=' + Q + r'(ami-volume-tab-\d+)' + Q
+ r'[^>]*>\s*(<header class=' + Q + r'ami-volume-panel-intro' + Q + r'>.*?</header>)',
lambda m: m.group(0)[:m.group(0).index(m.group(2))] + refais(m), s, flags=re.S)
if n_titres != 8:
raise SystemExit("ARRĂT · %d prĂ©sentations de partie réécrites, 8 attendues" % n_titres)
J.append("C · les huit présentations · l'onglet devient le titre, son sous-titre le sous-titre")
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# LE BLOC â il se retire d'un coup pour revenir en arriĂšre
# ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
BLOC = r'''
<style id="apo-passe3-2026-10-27">
/* ââ LA TROISIĂME PASSE · 27 octobre 2026 ââââââââââââââââââââââââââââââ */
/* A · LE SCEAU DU PIED â les rĂšgles de la Charte, telles quelles.
Base (sa V32) : le mot se pose en bloc dans la colonne de glyphes. */
body .ami-pied .f-sceau .amp-g .f-entre{display:block;
font-family:'Cormorant Garamond',Georgia,serif;font-style:normal;
font-size:.74rem;font-weight:600;letter-spacing:.08em;color:var(--or,#ffc545);
line-height:1;margin:.08rem 0;white-space:nowrap}
body .ami-pied .f-sceau .amp-g .f-entre .e{font-size:.82em;margin:0 .02em}
/* La ligne du Legs, en moldavite comme dans l'Initiation. */
body .ami-pied .ap-fin .f-legs,
body .ami-pied .ap-fin .f-legs{display:block;text-align:center;
font-family:'Cormorant Garamond',Georgia,serif;
font-size:calc(.98rem * var(--sc,.86));color:var(--moldavite,#62a802);
margin-top:.22rem;letter-spacing:.02em;text-decoration:none;font-weight:600}
body .ami-pied .ap-fin .f-legs:hover{filter:brightness(1.3)}
body .ami-pied .ap-fin .f-legs .f-legs-e{font-style:normal;
font-weight:400;margin-right:.12em}
/* Les deux blocs se resserrent sur la largeur de la ligne du dessous ;
la mesure est prise au rendu et posée dans --f-largeur. */
body .ami-pied .f-grille{max-width:min(calc(880px * var(--sc,.86)),
var(--f-largeur, 880px))}
/* PIĂGE PAYĂ SUR LA CHARTE, ĂVITĂ ICI : « background » est un RACCOURCI â
il remet background-clip Ă sa valeur initiale et efface le
background-clip:text du métal : les mots deviennent des rectangles
dorés pleins. On n'écrit donc que background-image. */
body .ami-pied .ap-fin .f-or b{color:var(--or-svg,#d1ae6b);
background-image:var(--or-metal);-webkit-background-clip:text;
background-clip:text;-webkit-text-fill-color:transparent}
/* C · LES TITRES DE PARTIES â centrĂ©s, sans filet ni dĂ©gradĂ©. */
#ami-apokalypse .ami-volume-panel-intro.p3-centre{text-align:center;
border-left:0;background:none;padding:.9rem 1rem .2rem;margin:0 0 1.15rem}
#ami-apokalypse .ami-volume-panel-intro.p3-centre .ami-volume-panel-title{
margin:.22rem 0 .1rem}
#ami-apokalypse .ami-volume-panel-intro.p3-centre .ami-volume-panel-title .e{
font-style:normal;font-weight:400;margin-right:.18em}
#ami-apokalypse .ami-volume-panel-intro.p3-centre .ami-volume-panel-subtitle{
font-style:italic}
#ami-apokalypse .ami-volume-panel-fil{margin:.42rem 0 0;color:var(--gris,#9595a2);
font:400 .64rem/1.5 Jost,sans-serif;letter-spacing:.06em}
</style>
<script id="apo-passe3-js-2026-10-27">
(function(){'use strict';
/* PIĂGE PAYĂ (27/10 06h40) : le pied est enfant DIRECT de body, frĂšre de
#ami-apokalypse â pas son enfant comme dans la Charte. MesurĂ© :
pied_dans_apo = false. On part donc du PIED, et le volume ne sert
qu'aux titres de parties. */
var P = document.querySelector('.ami-pied');
var R = { querySelector: function(x){ return P ? P.querySelector(x.replace(/^\.ami-pied\s*/,'')) : null; },
querySelectorAll: function(x){ return P ? P.querySelectorAll(x.replace(/^\.ami-pied\s*/,'')) : []; } };
if(!P) return;
/* ââ A · UNE SEULE LIGNE AU PIED ââââââââââââââââââââââââââââââââââââââââ
Le bloc de la Charte, porté tel quel. Sa rÚgle : le filet de la
fraction de gauche, la puce, « enâĄïžre », la puce et le filet de droite
sont sur LA MĂME ligne â et cette ligne est le milieu de l'encre du
« & ». Le sceau PORTE la ligne, il ne la suit pas.
PIĂGE PAYĂ SUR LA CHARTE : Ă©crire `transform` en ligne EFFACE le
translateX que la feuille pose (--gly-dx : les 9,7 % qui font tomber
les glyphes dans les boucles et non sur la panse). On le reporte
toujours. */
var BOUCLE = null;
function sceau(){
var g = R.querySelector('.ami-pied .f-sceau .amp-g');
var sc = R.querySelector('.ami-pied .f-sceau');
var te = R.querySelector('.ami-pied .f-sceau .f-entre');
var pts = R.querySelectorAll('.ami-pied .f-amp .f-pt');
var barre = R.querySelector('.ami-pied .f-eq .frac span + span');
if(!g || !sc || !te) return;
var dx = (getComputedStyle(g).getPropertyValue('--gly-dx') || '0px').trim() || '0px';
g.style.transform = 'translateX(' + dx + ')';
[].forEach.call(pts, function(pt){ pt.style.transform = ''; });
var rs = sc.getBoundingClientRect();
if(!rs.height) return;
var LIGNE = rs.top + rs.height / 2;
if(barre){
var rb0 = barre.getBoundingClientRect();
if(rb0.height && Math.abs(rb0.top - LIGNE) < 12) LIGNE = rb0.top;
}
var rt = te.getBoundingClientRect();
if(rt.height){
var dy = LIGNE - (rt.top + rt.height/2);
if(Math.abs(dy) >= 0.4)
g.style.transform = 'translate(' + dx + ',' + dy.toFixed(2) + 'px)';
}
/* Le « & » descend juste assez : le haut de âĄïž touche sa boucle. On
relĂšve la bande SANS trou dans le DESSIN du glyphe servi â rien
n'est écrit en dur, une autre fonte donnera une autre valeur.
PIĂGE : la boĂźte du sceau ne bouge pas avec --amp-dy, c'est
l'ENCRE qui bouge ; une correction cumulée diverge (mesuré sur la
Charte : -1 â 1,6 â 4,2 â 6,8 px). On pose une valeur ABSOLUE. */
var fin = R.querySelector('.ami-pied .ap-fin');
var ecl = R.querySelector('.ami-pied .f-entre .e');
if (fin && ecl){
if (BOUCLE === null) BOUCLE = basDeLaBoucleHaute();
if (BOUCLE !== null){
var re2 = ecl.getBoundingClientRect(), rs2 = sc.getBoundingClientRect();
if (re2.height && rs2.height){
var vise = re2.top - (rs2.top + BOUCLE * rs2.height);
if (Math.abs(vise) < 24) fin.style.setProperty('--amp-dy', vise.toFixed(2) + 'px');
}
}
}
/* Les puces au milieu du creux, entre le bord du bloc de formules et
le bord du sceau. On mesure le creux au lieu de s'en remettre Ă
l'écart du flex, qui colle la puce au « & » dÚs que la colonne est
plus large que son texte. */
var cols = R.querySelectorAll('.ami-pied .f-col');
var bordG = null, bordD = null;
if (cols[0]) [].forEach.call(cols[0].children, function(e){
var r = e.getBoundingClientRect(); if(!r.width) return;
if (bordG === null || r.right > bordG) bordG = r.right; });
if (cols[1]) [].forEach.call(cols[1].children, function(e){
var r = e.getBoundingClientRect(); if(!r.width) return;
if (bordD === null || r.left < bordD) bordD = r.left; });
var rsc = sc.getBoundingClientRect();
var creux = [ (bordG !== null ? (bordG + rsc.left)/2 : null),
(bordD !== null ? (rsc.right + bordD)/2 : null) ];
[].forEach.call(pts, function(pt, i){
var rp = pt.getBoundingClientRect();
if(!rp.height) return;
var dy2 = LIGNE - (rp.top + rp.height/2);
var dx2 = 0;
if (creux[i] !== null && isFinite(creux[i])){
var cc = creux[i] - (rp.left + rp.width/2);
if (Math.abs(cc) < 140) dx2 = cc;
}
if (Math.abs(dy2) < 0.4 && Math.abs(dx2) < 0.4) return;
pt.style.transform = 'translate(' + dx2.toFixed(2) + 'px,' + dy2.toFixed(2) + 'px)';
});
}
function basDeLaBoucleHaute(){
var w = R.querySelector('.ami-pied .amp-w');
if(!w) return null;
try{
var cs2 = getComputedStyle(w), T = 240;
var cv = document.createElement('canvas'); cv.width = T; cv.height = T;
var x = cv.getContext('2d', {willReadFrequently:true});
if(!x) return null;
x.fillStyle = '#000'; x.fillRect(0,0,T,T);
x.fillStyle = '#fff'; x.textAlign = 'center'; x.textBaseline = 'alphabetic';
x.font = (cs2.fontWeight || '700') + ' ' + Math.round(T*0.72) + 'px ' + cs2.fontFamily;
x.fillText('&', T/2, T*0.82);
var d = x.getImageData(0,0,T,T).data, li = [];
for (var y=0; y<T; y++){
var runs = 0, dans = false, vu = false;
for (var i=0; i<T; i++){
var v = d[(y*T+i)*4] > 90;
if (v && !dans){ dans = true; runs++; vu = true; }
if (!v && dans) dans = false;
}
if (vu) li.push({y:y, trous:runs-1});
}
if (li.length < 12) return null;
var h0 = li[0].y, b0 = li[li.length-1].y, H = b0-h0+1, grd = H*0.08;
var grp = [], cur = [];
for (var k=1; k<li.length-1; k++){
var L = li[k];
if (L.trous === 0 && L.y > h0+grd && L.y < b0-grd){
if (cur.length && L.y === cur[cur.length-1]+1) cur.push(L.y);
else { if(cur.length) grp.push(cur); cur = [L.y]; }
}
}
if (cur.length) grp.push(cur);
if (!grp.length) return null;
grp.sort(function(a,b){ return b.length - a.length; });
var pct = (grp[0][0] - h0) / H;
return (pct > 0.12 && pct < 0.88) ? pct : null;
}catch(e){ return null; }
}
/* Les deux blocs à la largeur de l'ENCRE de « Tu es un Beau Cadeau⊠»,
pas de sa boĂźte, qui fait toute la page. Un plancher qui ne se devine
pas : la plus large ligne des deux colonnes, doublĂ©e, plus le sceau â
en deçà , « Plaisir d'Offrir » viendrait toucher le « & ». */
function largeurDuPied(){
var bas = R.querySelector('.ami-pied .f-bas');
var fin = R.querySelector('.ami-pied .ap-fin');
var amp = R.querySelector('.ami-pied .f-amp');
if(!bas || !fin || !amp || !document.createRange) return;
var rg = document.createRange();
rg.selectNodeContents(bas);
var vise = rg.getBoundingClientRect().width;
if(!(vise > 40)) return;
var large = 0;
[].forEach.call(R.querySelectorAll('.ami-pied .f-col > *'), function(e){
var w = e.getBoundingClientRect().width;
if(w > large) large = w;
});
var plancher = 2 * large + amp.getBoundingClientRect().width + 48;
fin.style.setProperty('--f-largeur', Math.round(Math.max(vise, plancher)) + 'px');
}
/* « Pour le Désir de » : recalé sur la PAIRE « Se Découvrir · Se réUnir »
et non sur la page â la colonne de gauche est alignĂ©e Ă droite, celle
de droite Ă gauche, et les deux mots n'ont pas la mĂȘme longueur. */
function pourLeDesir(){
var haut = R.querySelector('.ami-pied .f-haut');
var acts = R.querySelectorAll('.ami-pied .f-col .f-act');
if(!haut || acts.length < 2 || !document.createRange) return;
var rg = document.createRange();
var enc = function(e){ rg.selectNodeContents(e); return rg.getBoundingClientRect(); };
var a = enc(acts[0]), z = enc(acts[1]);
if(!a.width || !z.width) return;
haut.style.transform = '';
var axe = (a.left + z.right) / 2;
var b = haut.querySelector('b');
if (b){
var mins = haut.querySelectorAll('.f-min');
var der = mins.length > 1 ? enc(mins[mins.length-1]).right : enc(b).right;
var centre = (enc(b).left + der) / 2;
var dx = axe - centre;
if (Math.abs(dx) >= 0.4 && Math.abs(dx) < 80)
haut.style.transform = 'translateX(' + dx.toFixed(2) + 'px)';
}
haut.style.marginBottom = '';
var basTexte = (b ? enc(b).bottom : haut.getBoundingClientRect().bottom);
var hautAct = Math.min(a.top, z.top);
var vide = hautAct - basTexte;
var mb = parseFloat(getComputedStyle(haut).marginBottom) || 0;
if (vide > 6.5 && vide < 60)
haut.style.marginBottom = (mb - (vide - 6)).toFixed(2) + 'px';
}
function depart(){ largeurDuPied(); sceau(); pourLeDesir(); }
window.addEventListener('resize', function(){ largeurDuPied(); sceau(); pourLeDesir(); });
if(document.fonts && document.fonts.ready)
document.fonts.ready.then(function(){ setTimeout(depart, 140); });
setTimeout(depart, 420); setTimeout(depart, 1200);
depart();
})();
</script>
'''
# PIĂGE PAYĂ (26/10) : il y a DEUX « </body> » littĂ©raux, et le PREMIER vit
# dans un COMMENTAIRE JavaScript. Mesuré alors : +5 214 caractÚres de texte
# visible et une erreur JS. La vraie fin est la DERNIĂRE, celle que suit
# </html>"></iframe>.
i = s.rfind('</body>')
if '</html>' not in s[i:i+400]:
raise SystemExit("ARRĂT · la fin de corps trouvĂ©e n'est pas suivie de </html>")
s = s[:i] + E(BLOC) + s[i:]
J.append("le bloc de la passe 3 posé à la fin du corps")
io.open(DST, 'w', encoding='utf-8').write(s)
print("apo_passe3 · %d â %d caractĂšres (%+d)" % (avant, len(s), len(s) - avant))
print("md5 : " + hashlib.md5(io.open(DST,'rb').read()).hexdigest()[:12])
for k, x in enumerate(J, 1):
print(" %2d · %s" % (k, x))