0xffd6563c…95fesent to0x72842d09…a7a6·#25,495,195·view on Etherscan
62; // THIN Voronoi boundary -> clean fracture line
const edge = y < top + GRID*1.5; // top facet catches light
const crev = Math.abs(x-cx) < GRID*1.5 && y < top+52; // central crevice the tuft emerges from
let col, sym='.';
if(crev){ col=P.crack; }
else if(seam){ col=P.crack; // crack runs perpendicular to the seed-pair axis
sym = dirSym(-(b1.y-b0.y), b1.x-b0.x); }
else { col = b0.t<0.30?P.rockDk : b0.t<0.68?P.rock : P.rockLt;
if(edge) col=mix(col,P.rockLt,0.5); }
C.push(x,y, edge?12:(seam?11:8), sym, shade(col,rs), 'season');
if(edge && !seam && snowF>0.2 && rs()<snowF*0.75)
C.push(x,y-GRID, 14, '.', shade(rs()<0.6?P.snow:P.snowDk,rs), 'season'); // snow caps the lit edges
}
}
// 3. a few long straight MASTER CRACKS across the faces (fractured-granite feel)
for(let kc=0; kc<3+Math.floor(rs()*3); kc++){
let px=FRAME+24+rs()*(W-2*FRAME-48), py=topAt(px)+12+rs()*120;
let dir=DOWN+(rs()*2-1)*1.15; const len=44+rs()*120, stp=Math.ceil(len/GRID);
for(let s=0;s<stp;s++){ px+=Math.cos(dir)*GRID; py+=Math.sin(dir)*GRID;
if(py>baseY+36 || px<FRAME+6 || px>W-FRAME-6) break;
C.push(px,py, 11, dirSym(Math.cos(dir),Math.sin(dir)), shade(P.crack,rs), 'season'); }
}
}
rockField();
// panicles bloom GREEN; they turn straw only as they SET SEED — late maturity, autumn, or senescence
const seedSet=clamp(smooth(0.80,1.0,m)*0.8 + sen*0.9 + (season==='Autumn'?0.45:0), 0, 1);
const bgc=BACKDROPS[backdropFor(st.seed)]; // backdrop colour, for fading distant tufts back
const fmix=(c,f)=> f>0 ? mix(c,bgc,f) : c;
// ---------- one TUFT (cushion of arching blades + airy panicles); sc=size, fade=distance, tag=identity ----------
function tuft(ox, oy, sc, fade, tag){
const zo = tag==='hero' ? 0 : -13; // companions sit BEHIND the hero
const nF=clamp(16+density*74,16,90)*(0.55+0.45*sc), n=Math.ceil(nF); // blade COUNT (cushion lushness) ← entries (density); floor 16 = the survivor always tufts
for(let k=0;k<n;k++){
const rs=rng('blade',st.seed,tag,k);
const emerge=k===n-1?clamp(nF-(n-1),0.12,1):1;
const a0=(rs()*2-1); // stable fan position
const ang=UP + a0*1.18;
const len=(84+205*m)*sc*(0.5+0.6*rs())*emerge*(1-0.22*Math.abs(a0));
const arch=0.5+0.85*rs(), outer=Math.abs(a0); // outer = older blade
const bladeBrown=clamp(smooth(0.62,1.0,outer)*0.9 + browny*0.6, 0, 1); // ONLY outer/older blades straw off
let dir=ang, pos=[ox+a0*11*sc, oy]; const steps=Math.max(2,Math.ceil(len/GRID));
for(let i=1;i<=steps;i++){ const u=i/steps;
dir += angTo(dir,DOWN)*arch*0.05*(0.3+0.7*u); // arch outward then over the rocks
pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID];
const tipBrown=clamp(bladeBrown*(0.6+0.4*u) + browny*u*0.35, 0, 1);
const green=mix(P.blade, rs()<0.5?P.bladeDk:P.bladeLt, rs());
const col=fmix(mix(green, mix(P.straw,P.strawDk,rs()), tipBrown), fade);
C.push(pos[0],pos[1], 40+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(col,rs), 'leaves');
}
}
function spikelet(x,y,dir,rs){
const col=fmix(mix(P.spikeGreen, P.spikeStraw, clamp(seedSet*(0.55+0.45*rs()),0,1)), fade);
const awnCol=fmix(mix(P.spikeGreen, P.awn, clamp(0.35+seedSet,0,1)), fade);
let sp=[x,y]; const nn=3+Math.floor(rs()*2); // a small oval lemma cluster
for(let i=0;i<nn;i++){ sp=[sp[0]+Math.cos(dir)*GRID*0.9, sp[1]+Math.sin(dir)*GRID*0.9];
C.push(sp[0],sp[1], 92+zo, i%2?'O':'.', shade(col,rs), 'flowers'); }
let ap=[sp[0],sp[1]]; // a fine awn extending past the tip
for(let i=0;i<3+Math.floor(rs()*3);i++){ ap=[ap[0]+Math.cos(dir)*GRID, ap[1]+Math.sin(dir)*GRID];
C.push(ap[0],ap[1], 90+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(awnCol,rs), 'flowers'); }
}
if(flowering>0.08){
const nPan=Math.max(1, Math.round((1+m*4)*flowering*(0.45+0.55*rewards)*(tag==='hero'?1:0.55))); // seed-heads ← rewards (floor keeps the survivor seeding)
for(let p=0;p<nPan;p++){
const rs=rng('pan',st.seed,tag,p);
const grown=clamp((m-(0.4+p*0.09))/0.12,0,1); // panicles emerge progressively
if(grown<=0) continue;
const sx=ox+(rs()*2-1)*(20+m*44)*sc;
const sh=(190+260*m)*sc*grown*(0.7+0.4*rs());
let pos=[sx,oy-4], dir=UP+(rs()*2-1)*0.2; const steps=Math.ceil(sh/GRID);
for(let s=1;s<=steps;s++){ const u=s/steps;
dir += Math.sin(u*3+rs()*6)*0.02 + (rs()*2-1)*0.004; // wiry wobble
pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID];
C.push(pos[0],pos[1], 70+zo, dirSym(Math.cos(dir),Math.sin(dir)), shade(fmix(P.stalk,fade),rs), 'stem');
if(u>0.5 && s%3===0){ // open panicle branches in the upper half
const nb=1+Math.floor(rs()*2);
for(let b=0;b<nb;b++){ const side=rs()<0.5?1:-1;
const bl=(16+30*m)*sc*(1-u)*(0.6+0.6*rs());
let bp=[pos[0],pos[1]], bd=dir+side*(0.6+0.5*rs()); const bst=Math.max(2,Math.ceil(bl/GRID));
for(let bs=1;bs<=bst;bs++){ bd += angTo(bd, UP+side*0.3)*0.1; // branch arcs up
bp=[bp[0]+Math.cos(bd)*GRID, bp[1]+Math.sin(bd)*GRID];
C.push(bp[0],bp[1], 71+zo, dirSym(Math.cos(bd),Math.sin(bd)), shade(fmix(P.stalk,fade),rs), 'stem'); }
spikelet(bp[0],bp[1],bd,rs);
}
}
}
spikelet(pos[0],pos[1],dir,rs); // a terminal spikelet
}
}
}
// ---------- a small COLONY: companions scatter in the crevices behind the hero (varies by seed; not always solo) ----------
const cr=rng('colony',st.seed);
const nComp=[0,1,1,2,2,3][Math.floor(cr()*6)];
for(let i=0;i<nComp;i++){
const side=cr()<0.5?-1:1, depth=cr(); // depth 0 = near ... 1 = far
const bx=clamp(cx + side*(78+cr()*210), FRAME+72, W-FRAME-72);
const by=(baseY-150) + cr()*46; // rooted lower on the rock surface than the hero
tuft(bx, by, lerp(0.62,0.38,depth), lerp(0.40,0.64,depth), 'bg'+i);
}
tuft(cx, cy, 1, 0, 'hero'); // the hero tuft, full size and in front
// ---------- SNOW: scattered flecks (more in winter), settled + drifting ----------
const sr=rng('snow',st.seed); const nSnow=Math.round(70*snowF + 14);
for(let i=0;i<nSnow;i++){
const x=FRAME+8 + sr()*(W-2*FRAME-16);
const y=lerp(baseY+10, 140, sr()*sr()); // biased low (settled on the ground/rocks)
C.push(x,y, 96, '.', shade(sr()<0.5?P.snow:P.snowDk, sr), 'flowers');
}
},
traits(st,env){
const season=seasonOf(st.now);
const snow = season==='Winter'?'Snowfall' : (season==='Summer'?'Clear':'Dusting');
return [['Habitat','Rock crevice'], ['Frost', st.diedAt>0?'Frozen':snow], ['Hardiness','Extreme']]; }
});
})();
;
/* species/lavender.js */
/* lavender — species index 3. GENERATED from prototypes/lavender.html by extract.js.
Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's
mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */
(function () {
const mount = (spec) => SeedlingRenderer.registerSpecies(3, spec);
/* Lavender — Lavandula. A radiating fountain of fine woody stems (a rounded clump), narrow
opposite grey-green leaves, and vertical violet FLOWER-SPIKES of whorled florets held above
the foliage. Hardy Mediterranean shrub: slow to wilt, summer bloom. Stems emerge progressively
— old stems splay outward, the newest keep rising from the centre. Death = grey dried wands.
Grows in a patch (Setting: Solo / Pair / Cluster / Field). Ported to the shared core. */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='lavender');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Lavender', sub:'lavandula v2',
senFrom:0.80,
schemeExempt:[], // spikes follow the per-seed scheme (woven, like the rest)
LW:{season:1.0, stem:1.05, leaves:1.1, flowers:1.9},
LS:{season:1.0, stem:1.0, leaves:0.9, flowers:0.8},
note:'Hardy Mediterranean clump · radiating woody stems · narrow opposite leaves · violet flower-spikes above the foliage · summer bloom · death = grey dried wands.',
PAL:{ soil:'#6f5128', grey:'#9a9180',
flower:'#7A4FC0', flowerLt:'#AE8FE2', flowerDk:'#553399', bud:'#7E8C56',
leafDark:'#566E50', leaf:'#88A07E', leafLt:'#B4C3A4', stem:'#6E7A54' },
stageName(st,env){ if(st.diedAt>0)return'Sealed'; const m=env.m;
if(env.ageF>0.86)return'Spent';
if(m<0.12)return'Sprout'; if(m<0.42)return'Young bush'; if(m<0.6)return'Bush';
return (env.season==='Summer'||env.season==='Spring')?'Flowering':'Bush'; },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,baseX,baseY}=env, P=this.PAL;
const bgC = BACKDROPS[backdropFor(st.seed)];
const seasonOpen = season==='Summer'?1 : season==='Spring'?0.8 : season==='Autumn'?0.55 : 0.4;
// ===================== one lavender bush (repeated to form a patch) =====================
function paintBush(I){
const {cx, by, S, fade, m, v, sen, browny, density, rewards, zb, pseed} = I;
const FC = c => fade>0 ? mix(c, bgC, fade) : c;
const flowering = smooth(0.40,0.62,m) * rewards * (1-sen*0.8);
const cFlower=FC(mix(P.flower,P.grey,browny)), cFlowerLt=FC(mix(P.flowerLt,P.grey,browny)), cFlowerDk=FC(mix(P.flowerDk,P.grey,browny));
const cBud=FC(mix(P.bud,P.grey,browny*0.9));
const leafD=FC(mix(P.leafDark,P.grey,browny)), leafM=FC(mix(P.leaf,P.grey,browny)), leafL=FC(mix(P.leafLt,P.grey,browny*0.9));
const stemC=FC(mix(P.stem,P.grey,Math.max((1-v)*0.5,sen)));
const greenOf=rs=>shade(mix(leafM, rs()<0.5?leafD:leafL, rs()), rs);
// per-seed personality so two bushes differ structurally (not just noise)
const hero=pseed==='hero';
const dd= hero?rng('dna',st.seed):rng('dna',st.seed,pseed);
const D={ spread:0.50+0.55*dd(), lean:(dd()*2-1)*0.16, fanBase:16+30*dd(), vigor:0.85+0.30*dd(),
bushiness:dd(), openBias:0.85+0.30*dd(), spikeF:0.80+0.40*dd() };
const MAXS=30, growWin=0.12, splayOpen=smooth(0.0,0.34,m);
const lenBase=(180+440*m)*(0.72+0.28*v)*D.vigor*S;
for(let k=0;k<MAXS;k++){
const rs= hero?rng('stem',st.seed,k):rng('stem',st.seed,pseed,k);
const birthM=(k/MAXS)*0.70;
const grown=clamp((density-birthM)/growWin, k===0?0.2:0, 1); // stem COUNT ← density; first always present
if(grown<=0) continue;
const outerness=lerp(1.0,0.22,k/MAXS); // OLD outer · NEW central → fresh growth from the middle
const a0=(rs()*2-1)*outerness*splayOpen;
const ang0=UP + a0*D.spread + D.lean;
const baseX0=cx + a0*D.fanBase*S;
const maxLen=lenBase*(1-0.30*Math.abs(a0))*(0.8+0.4*rs());
const len=maxLen*grown;
const zf=rs()<0.5, stemZ=(zf?42:30)+zb, leafZ=(zf?44:28)+zb, flZ=(zf?94:84)+zb;
const stemFlower = flowering>0.05 && grown>0.4 && rs() < (0.3+0.66*flowering);
const leafTop = stemFlower?0.58:0.86;
let dir=ang0, pos=[baseX0, by-3]; const steps=Math.max(3,Math.ceil(len/GRID));
let tip=[pos[0],pos[1]], tdir=dir;
for(let i=1;i<=steps;i++){ const u=i/steps;
dir += angTo(dir,ang0)*0.05 + Math.sin(i*0.5+rs()*6)*0.009 + a0*0.004*u;
const dx=Math.cos(dir),dy=Math.sin(dir); pos=[pos[0]+dx*GRID, pos[1]+dy*GRID]; tip=[pos[0],pos[1]]; tdir=dir;
C.push(pos[0],pos[1], stemZ, dirSym(dx,dy), shade(stemC,rs), 'stem');
if(u<leafTop && i%2===0 && rs() < (u<0.28?0.45:0.85)){ // OPPOSITE linear leaves, airier near the base
for(const sd of [1,-1]){ const la=dir + sd*(0.5+0.18*rs()) - 0.05;
const ll=2+Math.floor((2+m*3)*S*(0.6+0.5*rs())); let lp=[pos[0],pos[1]];
for(let kk=1;kk<=ll;kk++){ const ax=Math.cos(la),ay=Math.sin(la); lp=[lp[0]+ax*GRID,lp[1]+ay*GRID]; const lt=kk>=ll-1;
C.push(lp[0],lp[1], leafZ+(lt?2:0), dirSym(ax,ay), lt?shade(leafD,rs):greenOf(rs), 'leaves'); } }
}
}
if(stemFlower){ // tip FLOWER-SPIKE — develops with the stem's own age
const stemOpen=smooth(0.4,1.0,grown);
const open=clamp(smooth(0.18,0.82,flowering)*D.openBias*seasonOpen*stemOpen,0,1);
const sLen=(30+66*m)*smooth(0.05,0.7,flowering)*D.spikeF*stemOpen*(0.72+0.5*rs())*S;
const sW=GRID*(1.5+0.8*rs())*Math.sqrt(S), sst=Math.max(3,Math.ceil(sLen/GRID));
let sp=[tip[0],tip[1]], d=tdir;
for(let i=1;i<=sst;i++){ const u=i/sst;
d += angTo(d,UP)*0.16;
sp=[sp[0]+Math.cos(d)*GRID, sp[1]+Math.sin(d)*GRID];
const taper=Math.min(1,u*4)*(1-Math.pow(u,1.7)), hw=sW*taper, px=-Math.sin(d), py=Math.cos(d);
for(let w=-hw; w<=hw; w+=GRID){ if(rs()<0.12) continue;
const fx=sp[0]+px*w, fy=sp[1]+py*w, localOpen=clamp(open*(1.25-u),0,1), isOpen=rs()<localOpen*0.85;
const col = isOpen ? shade(mix(cFlower, cFlowerLt, rs()*0.7), rs) : shade(mix(cBud, cFlowerDk, localOpen), rs);
const sym = isOpen ? (rs()<0.5?'+':'X') : (rs()<0.6?'.':'O');
C.push(fx, fy, flZ, sym, col, 'flowers');
}
}
}
}
}
// ===================== SETTING: a patch — Solo / Pair / Cluster / Field =====================
const sr=rng('setting',st.seed), roll=sr();
const nC = roll<0.18?0 : roll<0.50?1 : roll<0.82?2 : 3+Math.floor(rng('fn',st.seed)()*2);
const comps=[];
for(let i=0;i<nC;i++){ const rc=rng('comp',st.seed,i);
const side=(i%2===0?1:-1), f=i/Math.max(1,nC);
const scale=lerp(0.72,0.46,f)*(0.9+0.2*rc()), off=side*(80+i*46+rc()*46), raise=(34+i*24+rc()*30);
comps.push({cx:baseX+off, by:baseY-raise, S:scale, fade:lerp(0.5,0.74,f),
m, v, sen, browny, density:clamp(density+(rc()*2-1)*0.12,0.05,1), rewards, zb:-10, pseed:'c'+i, depth:f});
}
comps.sort((a,b)=>b.depth-a.depth).forEach(paintBush); // far (faint) → near
paintBush({cx:baseX, by:baseY, S:1.0, fade:0, m, v, sen, browny, density, rewards, zb:0, pseed:'hero'});
},
traits(st,env){
const sr=rng('setting',st.seed), roll=sr();
const setting = roll<0.18?'Solo' : roll<0.50?'Pair' : roll<0.82?'Cluster' : 'Field';
return [
['Setting', setting],
['Stems', clamp(Math.round(1+env.density*14),1,15)],
['Stature', Math.round((110+230*env.m)*(0.7+0.3*env.v))+' px'],
];
}
}));
})();
;
/* species/lotus.js */
/* lotus — species index 10. GENERATED from prototypes/sacred-lotus.html by extract.js.
Do not edit here — edit the prototype and re-run `npm run extract`. The prototype's
mount() call is redirected to SeedlingRenderer.registerSpecies via the local shim. */
(function () {
const mount = (spec) => SeedlingRenderer.registerSpecies(10, spec);
/* Sacred Lotus — Nelumbo nucifera.
An aquatic plant: large peltate pads with a WAVY / undulate margin and radial veins,
held on SHORT petioles low over the water (heavy leaves don't reach far up; the newest
leaf is a small cupped emerging blade), and a solitary red-pink BLOOM raised above the
foliage on its own taller peduncle — broad layered petals around a golden receptacle
with a fringe of stamens. Lifecycle on the peduncle: nodding BUD → open BLOOM → green
POD → dry "showerhead" seed pod. Lotuses often grow in a stand: a seed-driven COLONY
renders 1 → many plants (solo to a pond bunch). Summer bloom, winter pad-dieback. */
const SP = window.SEEDLING_SPECIES.find(x=>x.key==='lotus');
mount(Object.assign(window.SEEDLING_toSpec(SP), {
title:'Sacred Lotus', sub:'nelumbo v4',
senFrom:0.82,
note:'Aquatic · big wavy pads low on the water, side-view cup blooms (pointed petals + golden stamen crown) on stems of varied height → showerhead seed pod · flower varieties Pink / White / Rose · coherent WIND bends the stalks downwind · 1–5 flowers per root, spread both sides · solo to a full pond bunch scattered across the water.',
PAL:{ soil:'#6a7a5a',
stalk:'#6E9A6B', stalkDk:'#4C7754', speck:'#365E3C',
pad:'#6E9A5E', padDk:'#3E6B43', padLt:'#9CC07E', padVein:'#33572F', padEdge:'#274A29', padYellow:'#C7A24E',
petal:'#E0796E', petalLt:'#F7D6CE', petalDk:'#C53A30', petalVein:'#962017',
center:'#E8B23A', centerDk:'#B07A1E', stamen:'#EFC85C',
pod:'#7E9A55', podRim:'#5A7A3C', podDry:'#8A714A', podDryDk:'#5A4730', pit:'#3E2E1A' },
stageName(st,env){ if(st.diedAt>0)return'Dry pod'; const m=env.m;
if(m<0.15)return'Sprout'; if(m<0.50)return'Floating pads';
if(m<0.72)return'Budding'; return'Bloom'; },
draw(st, C, env){
const {m,v,sen,season,browny,density,rewards,reproOK,dormancy,dead,baseX,baseY}=env, P=this.PAL;
const winter=dormancy||0;
const bgC = BACKDROPS[backdropFor(st.seed)];
const waterY = baseY - 8; // plants root at the bottom (like the other species), grow UP from here
const seasonBloom = season==='Summer'?1 : season==='Spring'?0.7 : season==='Autumn'?0.45 : 0.30; // monsoon bias, floored
// per-seed POND environment, shared by every plant in the colony
const windDir = rng('windD',st.seed)()<0.5?-1:1;
const windStr = Math.pow(rng('windS',st.seed)(),2.6); // 0..1, skewed LOW → mostly calm, rarely a storm
function varietyPal(key){ const x=rng('variety',st.seed,key)(); // per-PLANT flower colour (so a pond can mix pink + white)
if(x<0.42) return {name:'Pink', lt:'#F7D6CE', pt:'#E0796E', dk:'#C53A30', vn:'#962017'};
if(x<0.80) return {name:'White', lt:'#FFFFFF', pt:'#F6F1EA', dk:'#EBA48C', vn:'#C56A50'}; // white petals, soft rose-flushed tips + rim (reads white & pops on cream)
return {name:'Rose', lt:'#F3C4D4', pt:'#D85A7A', dk:'#A82A52', vn:'#7C1B3C'};
}
// ===================== one lotus plant (repeated to form a colony) =====================
function paintLotus(I){
const {cx, by, S, fade, m, v, sen, density, rewards, reproOK, browny, dead, pseed} = I;
const FC = c => fade>0 ? mix(c, bgC, fade) : c;
const PV = varietyPal(pseed); // this plant's petal palette (Pink / White / Rose)
const collapse = clamp(1-0.45*sen-0.42*winter, 0.18, 1); // pads sink & yellow as they die back
const standC = clamp(1-0.12*sen-0.42*winter, 0.55, 1); // woody flower/pod stalks stay STANDING
const bloomDrive = reproOK ? clamp(rewards*(0.55+0.45*seasonBloom)*(1-sen),0,1) : 0;
const podDrive = reproOK ? clamp(sen*1.2 + (season==='Autumn'?0.35:0) + smooth(0.82,1.0,m)*0.5, 0,1) : 0;
// -- petiole / peduncle: erect at the base, GENTLY arcing toward `lean` near the top, with a
// faint sway — so stems read natural (not ramrod-straight parallel poles). --
function rise(x0,y0,h,lean,z,col,wideF,rs){
const steps=Math.max(2,Math.ceil(h/GRID)); let pos=[x0,y0], dir=UP;
const sway=(rs()*2-1), swF=0.7+rs()*0.8;
for(let s=1;s<=steps;s++){ const u=s/steps;
const target=UP + lean*smooth(0,1,u); // straight low → curving over near the flower
dir += angTo(dir, target)*0.08 + Math.sin(u*Math.PI*swF)*0.006*sway;
pos=[pos[0]+Math.cos(dir)*GRID, pos[1]+Math.sin(dir)*GRID];
const px=-Math.sin(dir), py=Math.cos(dir);
C.push(pos[0],pos[1],z,'|',shade(FC(col),rs),'stem');
if(wideF) C.push(pos[0]+px*GRID,pos[1]+py*GRID,z,'|',shade(FC(mix(col,P.stalkDk,0.35)),rs),'stem');
if(s%4===0) C.push(pos[0]+px*(rs()<0.5?GRID:-GRID), pos[1]+py*(rs()<0.5?GRID:-GRID), z,'.', shade(FC(P.speck),rs),'stem'); }
return pos;
}
// -- WAVY peltate pad: undulate margin (not a clean circle) + radial veins + sinus notch --
function drawPad(px,py,R,z,yellowF,ragged,cupped,rs){
const sq = cupped? 0.60 : 0.84;
const seam = rng('pr',Math.round(px),Math.round(py),pseed)()*TAU;
const Rf = a => R*(1 + 0.14*Math.sin(7*a+seam) + 0.06*Math.sin(3*a-seam*1.3)); // undulate edge
const notch = seam + DOWN; // sinus toward the petiole
const tone = c => shade(FC(mix(c,P.padYellow,yellowF)),rs);
for(let gx=-R*1.25;gx<=R*1.25;gx+=GRID)for(let gy=-R*sq*1.25;gy<=R*sq*1.25;gy+=GRID){
const a=Math.atan2(gy/sq,gx), rr=Math.hypot(gx,gy/sq), re=Rf(a); if(rr>re)continue;
const rad=rr/re, cr=rng('pf',Math.round(px),Math.round(gx),Math.round(gy));
if(Math.abs(angTo(a,notch))<0.22 && rad>0.5) continue; // sinus cut
if(cr()>0.78) continue; // airy stipple (ground shows through)
if(rad>0.6 && ragged>0 && cr()<ragged) continue; // collapsing margin
const sheen=Math.sin(a*3+seam)*0.5*(0.6-rad);
const col=mix(mix(P.pad, sheen<0?P.padDk:P.padLt, Math.abs(sheen)), P.padDk, smooth(0.7,1.0,rad)*0.3 + (cupped&&gy>0?0.22:0));
C.push(px+gx,py+gy, z, dirSym(gx,gy), tone(col),'leaves');
}
const nVein=cupped?9:13;
for(let k=0;k<nVein;k++){ const a=seam + k/nVein*TAU; if(Math.abs(angTo(a,notch))<0.22) continue;
const ca=Math.cos(a), sa=Math.sin(a)*sq, re=Rf(a);
for(let t=R*0.16;t<=re*0.92;t+=GRID){ const gx=ca*t, gy=sa*t;
if(ragged>0 && t>re*0.6 && rng('vg',Math.round(px),Math.round(gx),k)()<ragged) continue;
C.push(px+gx, py+gy, z+0.6, dirSym(ca,sa), tone(P.padVein),'leaves'); } }
for(let a=0;a<TAU;a+=0.045){ if(Math.abs(angTo(a,notch))<0.20) continue;
const re=Rf(a), ca=Math.cos(a), sa=Math.sin(a)*sq;
if(ragged>0 && rng('eg',Math.round(px),Math.round(a*30))()<ragged*0.8) continue;
for(const rf of [0.99,0.92]) C.push(px+ca*re*rf, py+sa*re*rf, z+0.7, dirSym(ca,sa), tone(P.padEdge),'leaves'); }
if(cupped) for(let a=Math.PI*1.04;a<=Math.PI*1.96;a+=0.05){ const re=Rf(a), ca=Math.cos(a), sa=Math.sin(a)*sq; // curled upper rim
C.push(px+ca*re*0.85, py+sa*re*0.85, z+0.8, dirSym(ca,sa), tone(mix(P.padEdge,'#000000',0.18)),'leaves'); }
C.push(px,py,z+0.9,'O',tone(P.padEdge),'leaves'); // peltate hub
C.push(px,py,z+1.0,'.',tone(P.padEdge),'leaves');
}
// -- one DISTINCT pointed lotus petal: almond shape, dark outline (so petals separate),
// central vein, pale base → red tip. `ang` is an absolute direction; the petal grows
//