diff --git a/.gitignore b/.gitignore index e8454de..b058bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ # build output dist/ dist-*.tar.gz +dist-*.tar +release-backups/ +tmp/ +images/facebook-060426/ # generated types .astro/ diff --git a/package.json b/package.json index a9756d8..fc841e4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "node": ">=22.12.0" }, "scripts": { + "predev": "node scripts/sync-family-lab.mjs", "dev": "astro dev", + "prebuild": "node scripts/sync-family-lab.mjs", "build": "astro build", "preview": "astro preview", "astro": "astro" diff --git a/public/images/family-lab/facebook-060426/0kevin.jpg b/public/images/family-lab/facebook-060426/0kevin.jpg new file mode 100644 index 0000000..238e7e5 Binary files /dev/null and b/public/images/family-lab/facebook-060426/0kevin.jpg differ diff --git a/public/images/family-lab/facebook-060426/0meanddave1.jpg b/public/images/family-lab/facebook-060426/0meanddave1.jpg new file mode 100644 index 0000000..56dccac Binary files /dev/null and b/public/images/family-lab/facebook-060426/0meanddave1.jpg differ diff --git a/public/images/family-lab/facebook-060426/0meandjr.jpg b/public/images/family-lab/facebook-060426/0meandjr.jpg new file mode 100644 index 0000000..7e18a12 Binary files /dev/null and b/public/images/family-lab/facebook-060426/0meandjr.jpg differ diff --git a/public/images/family-lab/facebook-060426/0meandjr2.jpg b/public/images/family-lab/facebook-060426/0meandjr2.jpg new file mode 100644 index 0000000..e612bd5 Binary files /dev/null and b/public/images/family-lab/facebook-060426/0meandjr2.jpg differ diff --git a/public/images/family-lab/facebook-060426/0noreen.jpg b/public/images/family-lab/facebook-060426/0noreen.jpg new file mode 100644 index 0000000..0daddcf Binary files /dev/null and b/public/images/family-lab/facebook-060426/0noreen.jpg differ diff --git a/public/images/family-lab/facebook-060426/120123513_10160098332913356_8152493316232887398_n.jpg b/public/images/family-lab/facebook-060426/120123513_10160098332913356_8152493316232887398_n.jpg new file mode 100644 index 0000000..5a72e66 Binary files /dev/null and b/public/images/family-lab/facebook-060426/120123513_10160098332913356_8152493316232887398_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/492885364_10164927662418356_3456606569142775457_n.jpg b/public/images/family-lab/facebook-060426/492885364_10164927662418356_3456606569142775457_n.jpg new file mode 100644 index 0000000..682eef0 Binary files /dev/null and b/public/images/family-lab/facebook-060426/492885364_10164927662418356_3456606569142775457_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/493087455_10164927900423356_1783228569477447040_n.jpg b/public/images/family-lab/facebook-060426/493087455_10164927900423356_1783228569477447040_n.jpg new file mode 100644 index 0000000..63397e9 Binary files /dev/null and b/public/images/family-lab/facebook-060426/493087455_10164927900423356_1783228569477447040_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/493128663_10164927900128356_6172483040320236425_n.jpg b/public/images/family-lab/facebook-060426/493128663_10164927900128356_6172483040320236425_n.jpg new file mode 100644 index 0000000..b7d6693 Binary files /dev/null and b/public/images/family-lab/facebook-060426/493128663_10164927900128356_6172483040320236425_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/493487173_10164927517273356_2877364168908790490_n.jpg b/public/images/family-lab/facebook-060426/493487173_10164927517273356_2877364168908790490_n.jpg new file mode 100644 index 0000000..2aa8d10 Binary files /dev/null and b/public/images/family-lab/facebook-060426/493487173_10164927517273356_2877364168908790490_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/493521840_10164927899783356_8451095767608071352_n.jpg b/public/images/family-lab/facebook-060426/493521840_10164927899783356_8451095767608071352_n.jpg new file mode 100644 index 0000000..fbb4936 Binary files /dev/null and b/public/images/family-lab/facebook-060426/493521840_10164927899783356_8451095767608071352_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/493568147_10164927507723356_8989083916261478039_n.jpg b/public/images/family-lab/facebook-060426/493568147_10164927507723356_8989083916261478039_n.jpg new file mode 100644 index 0000000..48f7d7c Binary files /dev/null and b/public/images/family-lab/facebook-060426/493568147_10164927507723356_8989083916261478039_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494051695_10164923235368356_3123961340022374259_n.jpg b/public/images/family-lab/facebook-060426/494051695_10164923235368356_3123961340022374259_n.jpg new file mode 100644 index 0000000..41de02b Binary files /dev/null and b/public/images/family-lab/facebook-060426/494051695_10164923235368356_3123961340022374259_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494221802_10164917313928356_6201487686145085585_n.jpg b/public/images/family-lab/facebook-060426/494221802_10164917313928356_6201487686145085585_n.jpg new file mode 100644 index 0000000..4f20a14 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494221802_10164917313928356_6201487686145085585_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494354193_10164927899778356_4712104588235306906_n.jpg b/public/images/family-lab/facebook-060426/494354193_10164927899778356_4712104588235306906_n.jpg new file mode 100644 index 0000000..89881b7 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494354193_10164927899778356_4712104588235306906_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494399787_10164927568323356_3441460625876252528_n.jpg b/public/images/family-lab/facebook-060426/494399787_10164927568323356_3441460625876252528_n.jpg new file mode 100644 index 0000000..46fdfd3 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494399787_10164927568323356_3441460625876252528_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494518551_10164927568298356_160274271465471474_n.jpg b/public/images/family-lab/facebook-060426/494518551_10164927568298356_160274271465471474_n.jpg new file mode 100644 index 0000000..ec89b42 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494518551_10164927568298356_160274271465471474_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494523316_10164927900478356_7004155854187873875_n.jpg b/public/images/family-lab/facebook-060426/494523316_10164927900478356_7004155854187873875_n.jpg new file mode 100644 index 0000000..e38ef86 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494523316_10164927900478356_7004155854187873875_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494547170_10164927900203356_4601746082640736573_n.jpg b/public/images/family-lab/facebook-060426/494547170_10164927900203356_4601746082640736573_n.jpg new file mode 100644 index 0000000..8691e11 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494547170_10164927900203356_4601746082640736573_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/494730998_10164927063383356_3349522253005043734_n.jpg b/public/images/family-lab/facebook-060426/494730998_10164927063383356_3349522253005043734_n.jpg new file mode 100644 index 0000000..fcdb836 Binary files /dev/null and b/public/images/family-lab/facebook-060426/494730998_10164927063383356_3349522253005043734_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/495001724_10164927900168356_72306345637966179_n.jpg b/public/images/family-lab/facebook-060426/495001724_10164927900168356_72306345637966179_n.jpg new file mode 100644 index 0000000..8a8c69d Binary files /dev/null and b/public/images/family-lab/facebook-060426/495001724_10164927900168356_72306345637966179_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/495135525_10164927330908356_5836717729774348692_n.jpg b/public/images/family-lab/facebook-060426/495135525_10164927330908356_5836717729774348692_n.jpg new file mode 100644 index 0000000..0ee69da Binary files /dev/null and b/public/images/family-lab/facebook-060426/495135525_10164927330908356_5836717729774348692_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/495166206_10164927899843356_2646229680737695736_n.jpg b/public/images/family-lab/facebook-060426/495166206_10164927899843356_2646229680737695736_n.jpg new file mode 100644 index 0000000..599c58f Binary files /dev/null and b/public/images/family-lab/facebook-060426/495166206_10164927899843356_2646229680737695736_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/504433479_10165460847233356_5037537808079063229_n.jpg b/public/images/family-lab/facebook-060426/504433479_10165460847233356_5037537808079063229_n.jpg new file mode 100644 index 0000000..1257215 Binary files /dev/null and b/public/images/family-lab/facebook-060426/504433479_10165460847233356_5037537808079063229_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/506594002_10165460847798356_6801445086539726348_n.jpg b/public/images/family-lab/facebook-060426/506594002_10165460847798356_6801445086539726348_n.jpg new file mode 100644 index 0000000..5657b49 Binary files /dev/null and b/public/images/family-lab/facebook-060426/506594002_10165460847798356_6801445086539726348_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/515207021_10165460796413356_7035465656300099072_n.jpg b/public/images/family-lab/facebook-060426/515207021_10165460796413356_7035465656300099072_n.jpg new file mode 100644 index 0000000..98c23fc Binary files /dev/null and b/public/images/family-lab/facebook-060426/515207021_10165460796413356_7035465656300099072_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/515406890_10165460847593356_2250561871011237348_n.jpg b/public/images/family-lab/facebook-060426/515406890_10165460847593356_2250561871011237348_n.jpg new file mode 100644 index 0000000..82a1216 Binary files /dev/null and b/public/images/family-lab/facebook-060426/515406890_10165460847593356_2250561871011237348_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/516434687_10165460846843356_7492668183879035239_n.jpg b/public/images/family-lab/facebook-060426/516434687_10165460846843356_7492668183879035239_n.jpg new file mode 100644 index 0000000..c507cba Binary files /dev/null and b/public/images/family-lab/facebook-060426/516434687_10165460846843356_7492668183879035239_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/516746900_10165460847883356_5536692516259284515_n.jpg b/public/images/family-lab/facebook-060426/516746900_10165460847883356_5536692516259284515_n.jpg new file mode 100644 index 0000000..5ea9600 Binary files /dev/null and b/public/images/family-lab/facebook-060426/516746900_10165460847883356_5536692516259284515_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/517669004_10165460849048356_9107647133061018734_n.jpg b/public/images/family-lab/facebook-060426/517669004_10165460849048356_9107647133061018734_n.jpg new file mode 100644 index 0000000..56d0d73 Binary files /dev/null and b/public/images/family-lab/facebook-060426/517669004_10165460849048356_9107647133061018734_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/520491700_10165459654093356_8841314862368753486_n.jpg b/public/images/family-lab/facebook-060426/520491700_10165459654093356_8841314862368753486_n.jpg new file mode 100644 index 0000000..fd7a9a5 Binary files /dev/null and b/public/images/family-lab/facebook-060426/520491700_10165459654093356_8841314862368753486_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/521327225_10165460849143356_7607127912863739734_n.jpg b/public/images/family-lab/facebook-060426/521327225_10165460849143356_7607127912863739734_n.jpg new file mode 100644 index 0000000..e194e75 Binary files /dev/null and b/public/images/family-lab/facebook-060426/521327225_10165460849143356_7607127912863739734_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/521400941_10165460846418356_2864901854967710765_n.jpg b/public/images/family-lab/facebook-060426/521400941_10165460846418356_2864901854967710765_n.jpg new file mode 100644 index 0000000..e00a0c8 Binary files /dev/null and b/public/images/family-lab/facebook-060426/521400941_10165460846418356_2864901854967710765_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/521427032_10165460847753356_5860891320429451752_n.jpg b/public/images/family-lab/facebook-060426/521427032_10165460847753356_5860891320429451752_n.jpg new file mode 100644 index 0000000..96b6b2b Binary files /dev/null and b/public/images/family-lab/facebook-060426/521427032_10165460847753356_5860891320429451752_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522004434_10165460846548356_1607923897930236407_n.jpg b/public/images/family-lab/facebook-060426/522004434_10165460846548356_1607923897930236407_n.jpg new file mode 100644 index 0000000..b1140e6 Binary files /dev/null and b/public/images/family-lab/facebook-060426/522004434_10165460846548356_1607923897930236407_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522182143_10165459471338356_1606784056438710351_n.jpg b/public/images/family-lab/facebook-060426/522182143_10165459471338356_1606784056438710351_n.jpg new file mode 100644 index 0000000..bdb59a2 Binary files /dev/null and b/public/images/family-lab/facebook-060426/522182143_10165459471338356_1606784056438710351_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522453807_10165460846603356_6259684892242969873_n.jpg b/public/images/family-lab/facebook-060426/522453807_10165460846603356_6259684892242969873_n.jpg new file mode 100644 index 0000000..91f62ec Binary files /dev/null and b/public/images/family-lab/facebook-060426/522453807_10165460846603356_6259684892242969873_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522633712_10165460846373356_5230371714367054002_n.jpg b/public/images/family-lab/facebook-060426/522633712_10165460846373356_5230371714367054002_n.jpg new file mode 100644 index 0000000..6ac0c39 Binary files /dev/null and b/public/images/family-lab/facebook-060426/522633712_10165460846373356_5230371714367054002_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522636279_10165460847413356_4318144176740033047_n.jpg b/public/images/family-lab/facebook-060426/522636279_10165460847413356_4318144176740033047_n.jpg new file mode 100644 index 0000000..168de83 Binary files /dev/null and b/public/images/family-lab/facebook-060426/522636279_10165460847413356_4318144176740033047_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522668117_10165460847213356_1893563108470380328_n.jpg b/public/images/family-lab/facebook-060426/522668117_10165460847213356_1893563108470380328_n.jpg new file mode 100644 index 0000000..d470657 Binary files /dev/null and b/public/images/family-lab/facebook-060426/522668117_10165460847213356_1893563108470380328_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/522935853_10165460847343356_185224315164191281_n.jpg b/public/images/family-lab/facebook-060426/522935853_10165460847343356_185224315164191281_n.jpg new file mode 100644 index 0000000..db4d8ae Binary files /dev/null and b/public/images/family-lab/facebook-060426/522935853_10165460847343356_185224315164191281_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/523221514_10165460846378356_1111240215091017428_n (1).jpg b/public/images/family-lab/facebook-060426/523221514_10165460846378356_1111240215091017428_n (1).jpg new file mode 100644 index 0000000..acb6435 Binary files /dev/null and b/public/images/family-lab/facebook-060426/523221514_10165460846378356_1111240215091017428_n (1).jpg differ diff --git a/public/images/family-lab/facebook-060426/523865744_10165460847048356_5182246231523944155_n.jpg b/public/images/family-lab/facebook-060426/523865744_10165460847048356_5182246231523944155_n.jpg new file mode 100644 index 0000000..cca3b25 Binary files /dev/null and b/public/images/family-lab/facebook-060426/523865744_10165460847048356_5182246231523944155_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524010471_10165460846848356_695497702775547970_n.jpg b/public/images/family-lab/facebook-060426/524010471_10165460846848356_695497702775547970_n.jpg new file mode 100644 index 0000000..02a0131 Binary files /dev/null and b/public/images/family-lab/facebook-060426/524010471_10165460846848356_695497702775547970_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524107103_10165460846738356_699955291386022689_n.jpg b/public/images/family-lab/facebook-060426/524107103_10165460846738356_699955291386022689_n.jpg new file mode 100644 index 0000000..1b8c6bc Binary files /dev/null and b/public/images/family-lab/facebook-060426/524107103_10165460846738356_699955291386022689_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524165568_10165460846943356_1339740361325743590_n.jpg b/public/images/family-lab/facebook-060426/524165568_10165460846943356_1339740361325743590_n.jpg new file mode 100644 index 0000000..d4975da Binary files /dev/null and b/public/images/family-lab/facebook-060426/524165568_10165460846943356_1339740361325743590_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524407015_10165460847228356_2983740593836906185_n.jpg b/public/images/family-lab/facebook-060426/524407015_10165460847228356_2983740593836906185_n.jpg new file mode 100644 index 0000000..ce9343c Binary files /dev/null and b/public/images/family-lab/facebook-060426/524407015_10165460847228356_2983740593836906185_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524472920_10165460846978356_5806742561007283782_n.jpg b/public/images/family-lab/facebook-060426/524472920_10165460846978356_5806742561007283782_n.jpg new file mode 100644 index 0000000..eab7b55 Binary files /dev/null and b/public/images/family-lab/facebook-060426/524472920_10165460846978356_5806742561007283782_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/524494622_10165460848878356_8168258628112986117_n.jpg b/public/images/family-lab/facebook-060426/524494622_10165460848878356_8168258628112986117_n.jpg new file mode 100644 index 0000000..4686bbf Binary files /dev/null and b/public/images/family-lab/facebook-060426/524494622_10165460848878356_8168258628112986117_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/525689069_10165489321193356_2966819798581987122_n.jpg b/public/images/family-lab/facebook-060426/525689069_10165489321193356_2966819798581987122_n.jpg new file mode 100644 index 0000000..fcebaa3 Binary files /dev/null and b/public/images/family-lab/facebook-060426/525689069_10165489321193356_2966819798581987122_n.jpg differ diff --git a/public/images/family-lab/facebook-060426/525797305_10165489462568356_6520445644919774104_n (2).jpg b/public/images/family-lab/facebook-060426/525797305_10165489462568356_6520445644919774104_n (2).jpg new file mode 100644 index 0000000..2573759 Binary files /dev/null and b/public/images/family-lab/facebook-060426/525797305_10165489462568356_6520445644919774104_n (2).jpg differ diff --git a/public/images/family-lab/facebook-060426/baby1.jpg b/public/images/family-lab/facebook-060426/baby1.jpg new file mode 100644 index 0000000..2d96de7 Binary files /dev/null and b/public/images/family-lab/facebook-060426/baby1.jpg differ diff --git a/public/images/family-lab/facebook-060426/balls.jpg b/public/images/family-lab/facebook-060426/balls.jpg new file mode 100644 index 0000000..b08e761 Binary files /dev/null and b/public/images/family-lab/facebook-060426/balls.jpg differ diff --git a/public/images/family-lab/facebook-060426/beret.jpg b/public/images/family-lab/facebook-060426/beret.jpg new file mode 100644 index 0000000..50b830c Binary files /dev/null and b/public/images/family-lab/facebook-060426/beret.jpg differ diff --git a/public/images/family-lab/facebook-060426/citizen.jpg b/public/images/family-lab/facebook-060426/citizen.jpg new file mode 100644 index 0000000..5a5ebc9 Binary files /dev/null and b/public/images/family-lab/facebook-060426/citizen.jpg differ diff --git a/public/images/family-lab/facebook-060426/dressUp.jpg b/public/images/family-lab/facebook-060426/dressUp.jpg new file mode 100644 index 0000000..4c87403 Binary files /dev/null and b/public/images/family-lab/facebook-060426/dressUp.jpg differ diff --git a/public/images/family-lab/facebook-060426/eating.jpg b/public/images/family-lab/facebook-060426/eating.jpg new file mode 100644 index 0000000..fe4156e Binary files /dev/null and b/public/images/family-lab/facebook-060426/eating.jpg differ diff --git a/public/images/family-lab/facebook-060426/fireman.jpg b/public/images/family-lab/facebook-060426/fireman.jpg new file mode 100644 index 0000000..61df41b Binary files /dev/null and b/public/images/family-lab/facebook-060426/fireman.jpg differ diff --git a/public/images/family-lab/facebook-060426/fireman2.jpg b/public/images/family-lab/facebook-060426/fireman2.jpg new file mode 100644 index 0000000..09613ff Binary files /dev/null and b/public/images/family-lab/facebook-060426/fireman2.jpg differ diff --git a/public/images/family-lab/facebook-060426/fireman3.jpg b/public/images/family-lab/facebook-060426/fireman3.jpg new file mode 100644 index 0000000..49e58ac Binary files /dev/null and b/public/images/family-lab/facebook-060426/fireman3.jpg differ diff --git a/public/images/family-lab/facebook-060426/fireman4.jpg b/public/images/family-lab/facebook-060426/fireman4.jpg new file mode 100644 index 0000000..a9a031a Binary files /dev/null and b/public/images/family-lab/facebook-060426/fireman4.jpg differ diff --git a/public/images/family-lab/facebook-060426/gift1.jpg b/public/images/family-lab/facebook-060426/gift1.jpg new file mode 100644 index 0000000..b09ed47 Binary files /dev/null and b/public/images/family-lab/facebook-060426/gift1.jpg differ diff --git a/public/images/family-lab/facebook-060426/giftagain.jpg b/public/images/family-lab/facebook-060426/giftagain.jpg new file mode 100644 index 0000000..6fb82dd Binary files /dev/null and b/public/images/family-lab/facebook-060426/giftagain.jpg differ diff --git a/public/images/family-lab/facebook-060426/glasses.jpg b/public/images/family-lab/facebook-060426/glasses.jpg new file mode 100644 index 0000000..0262f9a Binary files /dev/null and b/public/images/family-lab/facebook-060426/glasses.jpg differ diff --git a/public/images/family-lab/facebook-060426/haircut.jpg b/public/images/family-lab/facebook-060426/haircut.jpg new file mode 100644 index 0000000..7d7a5bd Binary files /dev/null and b/public/images/family-lab/facebook-060426/haircut.jpg differ diff --git a/public/images/family-lab/facebook-060426/icu.jpg b/public/images/family-lab/facebook-060426/icu.jpg new file mode 100644 index 0000000..a8dd80f Binary files /dev/null and b/public/images/family-lab/facebook-060426/icu.jpg differ diff --git a/public/images/family-lab/facebook-060426/jr_Grandma.jpg b/public/images/family-lab/facebook-060426/jr_Grandma.jpg new file mode 100644 index 0000000..0026e60 Binary files /dev/null and b/public/images/family-lab/facebook-060426/jr_Grandma.jpg differ diff --git a/public/images/family-lab/facebook-060426/manifest.json b/public/images/family-lab/facebook-060426/manifest.json new file mode 100644 index 0000000..e879d67 --- /dev/null +++ b/public/images/family-lab/facebook-060426/manifest.json @@ -0,0 +1,1337 @@ +[ + { + "filename": "0kevin.jpg", + "relative_url": "/uploads/facebook-060426/0kevin.jpg", + "title": "Kevin", + "description": "Family portrait moment.", + "tags": [ + "family" + ], + "sha1": "84ec1327b4915dda9a02433889848cdb946fe09b", + "file_size": 147835, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/0kevin.jpg" + }, + { + "filename": "0meanddave1.jpg", + "relative_url": "/uploads/facebook-060426/0meanddave1.jpg", + "title": "Dave And Dave Jr", + "description": "Dave with Dave Jr.", + "tags": [ + "child", + "dave", + "dave-jr" + ], + "sha1": "bfabdb34f67b105da843fa89a9aaa32a1eeec65b", + "file_size": 60946, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/0meanddave1.jpg" + }, + { + "filename": "0meandjr.jpg", + "relative_url": "/uploads/facebook-060426/0meandjr.jpg", + "title": "Dave And Dave Jr", + "description": "Dave with Dave Jr.", + "tags": [ + "child", + "dave", + "dave-jr" + ], + "sha1": "de24308c85de0adc0c2163fbf2b712722bdbc810", + "file_size": 93676, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/0meandjr.jpg" + }, + { + "filename": "0meandjr2.jpg", + "relative_url": "/uploads/facebook-060426/0meandjr2.jpg", + "title": "Dave And Dave Jr", + "description": "Dave with Dave Jr.", + "tags": [ + "child", + "dave", + "dave-jr" + ], + "sha1": "a96f4a863078acaf6696f1ce8693856b9c56355e", + "file_size": 23002, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/0meandjr2.jpg" + }, + { + "filename": "0noreen.jpg", + "relative_url": "/uploads/facebook-060426/0noreen.jpg", + "title": "Noreen", + "description": "Family portrait moment.", + "tags": [ + "family" + ], + "sha1": "4c4764c204c6b6cb0deacadc8e18bb3f00611ce0", + "file_size": 129877, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/0noreen.jpg" + }, + { + "filename": "120123513_10160098332913356_8152493316232887398_n.jpg", + "relative_url": "/uploads/facebook-060426/120123513_10160098332913356_8152493316232887398_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "0fb3d5dce4b75d920513f6a75d51d8b1dcda1c43", + "file_size": 117291, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/120123513_10160098332913356_8152493316232887398_n.jpg" + }, + { + "filename": "492885364_10164927662418356_3456606569142775457_n.jpg", + "relative_url": "/uploads/facebook-060426/492885364_10164927662418356_3456606569142775457_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "bbb38df8e3b8dbe7dab337b4de759b4676f8b2e2", + "file_size": 543277, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/492885364_10164927662418356_3456606569142775457_n.jpg" + }, + { + "filename": "493087455_10164927900423356_1783228569477447040_n.jpg", + "relative_url": "/uploads/facebook-060426/493087455_10164927900423356_1783228569477447040_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "805fda0a294c08293f48d3fc0b993c1193f12994", + "file_size": 811526, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/493087455_10164927900423356_1783228569477447040_n.jpg" + }, + { + "filename": "493128663_10164927900128356_6172483040320236425_n.jpg", + "relative_url": "/uploads/facebook-060426/493128663_10164927900128356_6172483040320236425_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "4261698b731d7ba3d1f526706e273a7539c2a28d", + "file_size": 549609, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/493128663_10164927900128356_6172483040320236425_n.jpg" + }, + { + "filename": "493487173_10164927517273356_2877364168908790490_n.jpg", + "relative_url": "/uploads/facebook-060426/493487173_10164927517273356_2877364168908790490_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "f45b879c5338da91fc6619052caa976e1ce2378f", + "file_size": 127631, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/493487173_10164927517273356_2877364168908790490_n.jpg" + }, + { + "filename": "493521840_10164927899783356_8451095767608071352_n.jpg", + "relative_url": "/uploads/facebook-060426/493521840_10164927899783356_8451095767608071352_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "68200882ea790ef73bfdaf19fde6327570b765aa", + "file_size": 558550, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/493521840_10164927899783356_8451095767608071352_n.jpg" + }, + { + "filename": "493568147_10164927507723356_8989083916261478039_n.jpg", + "relative_url": "/uploads/facebook-060426/493568147_10164927507723356_8989083916261478039_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "72d7c6c26e67a269a7ca165e2399bab77565f905", + "file_size": 139940, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/493568147_10164927507723356_8989083916261478039_n.jpg" + }, + { + "filename": "494051695_10164923235368356_3123961340022374259_n.jpg", + "relative_url": "/uploads/facebook-060426/494051695_10164923235368356_3123961340022374259_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "3f49a168499aebd1443a20e118d7a899ffe81275", + "file_size": 27012, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494051695_10164923235368356_3123961340022374259_n.jpg" + }, + { + "filename": "494221802_10164917313928356_6201487686145085585_n.jpg", + "relative_url": "/uploads/facebook-060426/494221802_10164917313928356_6201487686145085585_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "b11ea9f3e3a633bb8bf9447ff3b07c04840d28a9", + "file_size": 238105, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494221802_10164917313928356_6201487686145085585_n.jpg" + }, + { + "filename": "494354193_10164927899778356_4712104588235306906_n.jpg", + "relative_url": "/uploads/facebook-060426/494354193_10164927899778356_4712104588235306906_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "df23eac774591cc77bcb5526a660690a9e5199fa", + "file_size": 399115, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494354193_10164927899778356_4712104588235306906_n.jpg" + }, + { + "filename": "494399787_10164927568323356_3441460625876252528_n.jpg", + "relative_url": "/uploads/facebook-060426/494399787_10164927568323356_3441460625876252528_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "341a4225418311a9fa74f6ad2f5b156bb5ef78ad", + "file_size": 386204, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494399787_10164927568323356_3441460625876252528_n.jpg" + }, + { + "filename": "494518551_10164927568298356_160274271465471474_n.jpg", + "relative_url": "/uploads/facebook-060426/494518551_10164927568298356_160274271465471474_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "c6233fa34cc4d1be001ffe5dca39bd412ae54055", + "file_size": 170142, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494518551_10164927568298356_160274271465471474_n.jpg" + }, + { + "filename": "494523316_10164927900478356_7004155854187873875_n.jpg", + "relative_url": "/uploads/facebook-060426/494523316_10164927900478356_7004155854187873875_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "db4eab9be0d75ae108854280bb62fa3191e2c366", + "file_size": 487266, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494523316_10164927900478356_7004155854187873875_n.jpg" + }, + { + "filename": "494547170_10164927900203356_4601746082640736573_n.jpg", + "relative_url": "/uploads/facebook-060426/494547170_10164927900203356_4601746082640736573_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "30f7f17bcdb739e3d0a1fdf2fcc9ad4386134f4f", + "file_size": 980857, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494547170_10164927900203356_4601746082640736573_n.jpg" + }, + { + "filename": "494730998_10164927063383356_3349522253005043734_n.jpg", + "relative_url": "/uploads/facebook-060426/494730998_10164927063383356_3349522253005043734_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "b9dd65c4d95c0e87681fc8fe96bcc925c0148e07", + "file_size": 754242, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/494730998_10164927063383356_3349522253005043734_n.jpg" + }, + { + "filename": "495001724_10164927900168356_72306345637966179_n.jpg", + "relative_url": "/uploads/facebook-060426/495001724_10164927900168356_72306345637966179_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "27d2c423a58834f70c4e580bc689a4fc32cb49f7", + "file_size": 465947, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/495001724_10164927900168356_72306345637966179_n.jpg" + }, + { + "filename": "495135525_10164927330908356_5836717729774348692_n.jpg", + "relative_url": "/uploads/facebook-060426/495135525_10164927330908356_5836717729774348692_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "c4b6aaeb8e9786b537c184fd2bfb7686573139d5", + "file_size": 299916, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/495135525_10164927330908356_5836717729774348692_n.jpg" + }, + { + "filename": "495166206_10164927899843356_2646229680737695736_n.jpg", + "relative_url": "/uploads/facebook-060426/495166206_10164927899843356_2646229680737695736_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "315007284a6266f2878c47702a5c45e82cadfe97", + "file_size": 962546, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/495166206_10164927899843356_2646229680737695736_n.jpg" + }, + { + "filename": "504433479_10165460847233356_5037537808079063229_n.jpg", + "relative_url": "/uploads/facebook-060426/504433479_10165460847233356_5037537808079063229_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "5f14bb3e7fd6d3f1b7668a2455784373b3cf2d9e", + "file_size": 362808, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/504433479_10165460847233356_5037537808079063229_n.jpg" + }, + { + "filename": "506594002_10165460847798356_6801445086539726348_n.jpg", + "relative_url": "/uploads/facebook-060426/506594002_10165460847798356_6801445086539726348_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "4fca32c0cbabb2c10838b20aa9c95ca2aeef04bd", + "file_size": 67964, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/506594002_10165460847798356_6801445086539726348_n.jpg" + }, + { + "filename": "515207021_10165460796413356_7035465656300099072_n.jpg", + "relative_url": "/uploads/facebook-060426/515207021_10165460796413356_7035465656300099072_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "3def20843ffcdf8c293569f41459968ad942cc8d", + "file_size": 24036, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/515207021_10165460796413356_7035465656300099072_n.jpg" + }, + { + "filename": "515406890_10165460847593356_2250561871011237348_n.jpg", + "relative_url": "/uploads/facebook-060426/515406890_10165460847593356_2250561871011237348_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "703660acf92e1cf5111c2bbe24875ecfe35d5810", + "file_size": 69399, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/515406890_10165460847593356_2250561871011237348_n.jpg" + }, + { + "filename": "516434687_10165460846843356_7492668183879035239_n.jpg", + "relative_url": "/uploads/facebook-060426/516434687_10165460846843356_7492668183879035239_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "3167a447acb278745fcd5e829935ad9ea97c93d3", + "file_size": 187076, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/516434687_10165460846843356_7492668183879035239_n.jpg" + }, + { + "filename": "516746900_10165460847883356_5536692516259284515_n.jpg", + "relative_url": "/uploads/facebook-060426/516746900_10165460847883356_5536692516259284515_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "0f5118da0c01671ad8dca022cdd2a89f857ebbf2", + "file_size": 199176, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/516746900_10165460847883356_5536692516259284515_n.jpg" + }, + { + "filename": "517669004_10165460849048356_9107647133061018734_n.jpg", + "relative_url": "/uploads/facebook-060426/517669004_10165460849048356_9107647133061018734_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "ef37e710e50804837f29f1ac2844295c6e14b8a8", + "file_size": 92638, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/517669004_10165460849048356_9107647133061018734_n.jpg" + }, + { + "filename": "520491700_10165459654093356_8841314862368753486_n.jpg", + "relative_url": "/uploads/facebook-060426/520491700_10165459654093356_8841314862368753486_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "61d2e5246e1351c8eaab3be508edbef3fc8f903c", + "file_size": 54889, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/520491700_10165459654093356_8841314862368753486_n.jpg" + }, + { + "filename": "521327225_10165460849143356_7607127912863739734_n.jpg", + "relative_url": "/uploads/facebook-060426/521327225_10165460849143356_7607127912863739734_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "1cbe71fd5006522635d2aeae974b9e9bbfa2d2dc", + "file_size": 56919, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/521327225_10165460849143356_7607127912863739734_n.jpg" + }, + { + "filename": "521400941_10165460846418356_2864901854967710765_n.jpg", + "relative_url": "/uploads/facebook-060426/521400941_10165460846418356_2864901854967710765_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "052c1acb654ca1a5b8fe14c342f4f4f05219ef24", + "file_size": 274767, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/521400941_10165460846418356_2864901854967710765_n.jpg" + }, + { + "filename": "521427032_10165460847753356_5860891320429451752_n.jpg", + "relative_url": "/uploads/facebook-060426/521427032_10165460847753356_5860891320429451752_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "db1a2c87b20f41de0c1588f6d52be574234f1803", + "file_size": 59373, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/521427032_10165460847753356_5860891320429451752_n.jpg" + }, + { + "filename": "522004434_10165460846548356_1607923897930236407_n.jpg", + "relative_url": "/uploads/facebook-060426/522004434_10165460846548356_1607923897930236407_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "47e58af222cc6d319d6545c725ae5cd34eaada8f", + "file_size": 52425, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522004434_10165460846548356_1607923897930236407_n.jpg" + }, + { + "filename": "522182143_10165459471338356_1606784056438710351_n.jpg", + "relative_url": "/uploads/facebook-060426/522182143_10165459471338356_1606784056438710351_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "a24c71e11c78cf3da3b950e36586d4aee51a2b33", + "file_size": 391355, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522182143_10165459471338356_1606784056438710351_n.jpg" + }, + { + "filename": "522453807_10165460846603356_6259684892242969873_n.jpg", + "relative_url": "/uploads/facebook-060426/522453807_10165460846603356_6259684892242969873_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "de690be811ee805c9864344ab83b43cade80bdb8", + "file_size": 69590, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522453807_10165460846603356_6259684892242969873_n.jpg" + }, + { + "filename": "522633712_10165460846373356_5230371714367054002_n.jpg", + "relative_url": "/uploads/facebook-060426/522633712_10165460846373356_5230371714367054002_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "8f4c067721004e58736c4ea8a04de0be094454e7", + "file_size": 122567, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522633712_10165460846373356_5230371714367054002_n.jpg" + }, + { + "filename": "522636279_10165460847413356_4318144176740033047_n.jpg", + "relative_url": "/uploads/facebook-060426/522636279_10165460847413356_4318144176740033047_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "3b1ece11fc6fc87181bccc95a4629e62c7e5c0bf", + "file_size": 70503, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522636279_10165460847413356_4318144176740033047_n.jpg" + }, + { + "filename": "522668117_10165460847213356_1893563108470380328_n.jpg", + "relative_url": "/uploads/facebook-060426/522668117_10165460847213356_1893563108470380328_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "682726c150345f0afed1a0fc64c2e8215cf7c7e8", + "file_size": 45270, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522668117_10165460847213356_1893563108470380328_n.jpg" + }, + { + "filename": "522935853_10165460847343356_185224315164191281_n.jpg", + "relative_url": "/uploads/facebook-060426/522935853_10165460847343356_185224315164191281_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "020ce8640b7ca7cdafb7feb69d2afcc1fe1b5c25", + "file_size": 59376, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/522935853_10165460847343356_185224315164191281_n.jpg" + }, + { + "filename": "523221514_10165460846378356_1111240215091017428_n (1).jpg", + "relative_url": "/uploads/facebook-060426/523221514_10165460846378356_1111240215091017428_n (1).jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "96277f972894d0d367d40070968f9218244f221b", + "file_size": 87856, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/523221514_10165460846378356_1111240215091017428_n (1).jpg" + }, + { + "filename": "523865744_10165460847048356_5182246231523944155_n.jpg", + "relative_url": "/uploads/facebook-060426/523865744_10165460847048356_5182246231523944155_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "2bed0e3011b208a7d5764776c4bf5b06888b49a4", + "file_size": 59093, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/523865744_10165460847048356_5182246231523944155_n.jpg" + }, + { + "filename": "524010471_10165460846848356_695497702775547970_n.jpg", + "relative_url": "/uploads/facebook-060426/524010471_10165460846848356_695497702775547970_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "2a39ee20851d494424fa1de453d1ba15f02c8320", + "file_size": 68229, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524010471_10165460846848356_695497702775547970_n.jpg" + }, + { + "filename": "524107103_10165460846738356_699955291386022689_n.jpg", + "relative_url": "/uploads/facebook-060426/524107103_10165460846738356_699955291386022689_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "97c39af243386fc1aa992f63852a6821be35c0df", + "file_size": 551040, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524107103_10165460846738356_699955291386022689_n.jpg" + }, + { + "filename": "524165568_10165460846943356_1339740361325743590_n.jpg", + "relative_url": "/uploads/facebook-060426/524165568_10165460846943356_1339740361325743590_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "a603e14fba450be43b4ad2385082b53c46bf53cd", + "file_size": 341130, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524165568_10165460846943356_1339740361325743590_n.jpg" + }, + { + "filename": "524407015_10165460847228356_2983740593836906185_n.jpg", + "relative_url": "/uploads/facebook-060426/524407015_10165460847228356_2983740593836906185_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "d0943a4f5bfb8df7d6f164d2006e0e56f22c8312", + "file_size": 63815, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524407015_10165460847228356_2983740593836906185_n.jpg" + }, + { + "filename": "524472920_10165460846978356_5806742561007283782_n.jpg", + "relative_url": "/uploads/facebook-060426/524472920_10165460846978356_5806742561007283782_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "d11c74cec17832b8161d2a759d2eb5f20435206f", + "file_size": 62202, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524472920_10165460846978356_5806742561007283782_n.jpg" + }, + { + "filename": "524494622_10165460848878356_8168258628112986117_n.jpg", + "relative_url": "/uploads/facebook-060426/524494622_10165460848878356_8168258628112986117_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "4a6c6bf8fbabf7cc5bc3e587021169af1f71a0db", + "file_size": 49115, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/524494622_10165460848878356_8168258628112986117_n.jpg" + }, + { + "filename": "525689069_10165489321193356_2966819798581987122_n.jpg", + "relative_url": "/uploads/facebook-060426/525689069_10165489321193356_2966819798581987122_n.jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "5232bcd4614c87bff5d2cb550f3eed9efacb81c3", + "file_size": 122020, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/525689069_10165489321193356_2966819798581987122_n.jpg" + }, + { + "filename": "525797305_10165489462568356_6520445644919774104_n (2).jpg", + "relative_url": "/uploads/facebook-060426/525797305_10165489462568356_6520445644919774104_n (2).jpg", + "title": "Untitled Photo", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "41fc61d076da966942112c35f1c3ac006cff3f63", + "file_size": 162408, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/525797305_10165489462568356_6520445644919774104_n (2).jpg" + }, + { + "filename": "baby1.jpg", + "relative_url": "/uploads/facebook-060426/baby1.jpg", + "title": "Baby1", + "description": "Family archive snapshot.", + "tags": [ + "child", + "dave-jr" + ], + "sha1": "26e05ec5588c5b4f009175d792baffd48aad46f2", + "file_size": 258918, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/baby1.jpg" + }, + { + "filename": "balls.jpg", + "relative_url": "/uploads/facebook-060426/balls.jpg", + "title": "Balls", + "description": "Everyday family moment.", + "tags": [ + "playtime" + ], + "sha1": "dd9937bdfb8869c2d18fa617ab5df100989eb67b", + "file_size": 85976, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/balls.jpg" + }, + { + "filename": "beret.jpg", + "relative_url": "/uploads/facebook-060426/beret.jpg", + "title": "Beret", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "d9e585c29a303c14dcaccbeb5b319c201a8692bb", + "file_size": 49159, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/beret.jpg" + }, + { + "filename": "citizen.jpg", + "relative_url": "/uploads/facebook-060426/citizen.jpg", + "title": "Citizen", + "description": "Family outing or travel snapshot.", + "tags": [ + "outing" + ], + "sha1": "118dbac5dee915135b888ddf44aedabcd76d419d", + "file_size": 61712, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/citizen.jpg" + }, + { + "filename": "dressUp.jpg", + "relative_url": "/uploads/facebook-060426/dressUp.jpg", + "title": "Dressup", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "0b7c95bd72c21d717052b2d2ad26282020184d18", + "file_size": 512756, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/dressUp.jpg" + }, + { + "filename": "eating.jpg", + "relative_url": "/uploads/facebook-060426/eating.jpg", + "title": "Eating", + "description": "Everyday family moment.", + "tags": [ + "playtime" + ], + "sha1": "f22011175f4c9036bfc1ffd74668862d6576d6fd", + "file_size": 136710, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/eating.jpg" + }, + { + "filename": "fireman.jpg", + "relative_url": "/uploads/facebook-060426/fireman.jpg", + "title": "Fireman", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "1e63dbc8a19febe80d26f56760c286d2820d3dd2", + "file_size": 65986, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/fireman.jpg" + }, + { + "filename": "fireman2.jpg", + "relative_url": "/uploads/facebook-060426/fireman2.jpg", + "title": "Fireman2", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "2329a93f13497bd5a5275002d01a4aa8c9ff9090", + "file_size": 333858, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/fireman2.jpg" + }, + { + "filename": "fireman3.jpg", + "relative_url": "/uploads/facebook-060426/fireman3.jpg", + "title": "Fireman3", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "ed544b000a2632c09e29d7a3f288e4f9af0e3312", + "file_size": 61821, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/fireman3.jpg" + }, + { + "filename": "fireman4.jpg", + "relative_url": "/uploads/facebook-060426/fireman4.jpg", + "title": "Fireman4", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "b88e418f458e4eb261abf14da448a19b37eaec59", + "file_size": 58324, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/fireman4.jpg" + }, + { + "filename": "gift1.jpg", + "relative_url": "/uploads/facebook-060426/gift1.jpg", + "title": "Gift1", + "description": "Family celebration moment.", + "tags": [ + "holiday" + ], + "sha1": "4b2afa2b8d909bd14eba9d22ca9f27301b33cd5f", + "file_size": 31196, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/gift1.jpg" + }, + { + "filename": "giftagain.jpg", + "relative_url": "/uploads/facebook-060426/giftagain.jpg", + "title": "Giftagain", + "description": "Family celebration moment.", + "tags": [ + "holiday" + ], + "sha1": "f2fa01ff0f4b8881a80fc0e7f9f236da112d0bba", + "file_size": 342958, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/giftagain.jpg" + }, + { + "filename": "glasses.jpg", + "relative_url": "/uploads/facebook-060426/glasses.jpg", + "title": "Glasses", + "description": "Dress-up family snapshot.", + "tags": [ + "dress-up" + ], + "sha1": "7900085f4b5f6c99533f367c8707bd34c2dcf41d", + "file_size": 61247, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/glasses.jpg" + }, + { + "filename": "haircut.jpg", + "relative_url": "/uploads/facebook-060426/haircut.jpg", + "title": "Haircut", + "description": "Everyday family moment.", + "tags": [ + "playtime" + ], + "sha1": "674e56c054ed479831e9ec82f20b532ee4ed114d", + "file_size": 43125, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/haircut.jpg" + }, + { + "filename": "icu.jpg", + "relative_url": "/uploads/facebook-060426/icu.jpg", + "title": "Icu", + "description": "Family archive snapshot.", + "tags": [ + "family" + ], + "sha1": "fe34737a5d3c80d94e03f5b398482a1876e2dbf0", + "file_size": 53448, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/icu.jpg" + }, + { + "filename": "jr_Grandma.jpg", + "relative_url": "/uploads/facebook-060426/jr_Grandma.jpg", + "title": "Dave Jr And Grandma", + "description": "Family portrait moment.", + "tags": [ + "child", + "dave-jr", + "family" + ], + "sha1": "3c63369007ba66fbfce96f1f9b6434a4d568a479", + "file_size": 60796, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/jr_Grandma.jpg" + }, + { + "filename": "marvinGaye.jpg", + "relative_url": "/uploads/facebook-060426/marvinGaye.jpg", + "title": "Marvingaye", + "description": "Family archive snapshot.", + "tags": [ + "family" + ], + "sha1": "3240a727570ce2a31edca08983dbc36b17bde85e", + "file_size": 79578, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/marvinGaye.jpg" + }, + { + "filename": "may17.jpg", + "relative_url": "/uploads/facebook-060426/may17.jpg", + "title": "May17", + "description": "Family celebration moment.", + "tags": [ + "holiday" + ], + "sha1": "00d333123c508e0913986596f3d6fbb4f9bdcd52", + "file_size": 66999, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/may17.jpg" + }, + { + "filename": "me1.jpg", + "relative_url": "/uploads/facebook-060426/me1.jpg", + "title": "Me1", + "description": "Family archive snapshot.", + "tags": [ + "dave" + ], + "sha1": "faec696f837d76786c6f5475ea969410eced2b46", + "file_size": 18526, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/me1.jpg" + }, + { + "filename": "me_Nova.jpg", + "relative_url": "/uploads/facebook-060426/me_Nova.jpg", + "title": "Me Villanova", + "description": "Villanova-flavored family snapshot.", + "tags": [ + "dave", + "sports", + "villanova" + ], + "sha1": "97d6828532700032a1597f83a5d72e6d8e15e483", + "file_size": 29886, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/me_Nova.jpg" + }, + { + "filename": "me_Trivia.jpg", + "relative_url": "/uploads/facebook-060426/me_Trivia.jpg", + "title": "Me Trivia", + "description": "Family archive snapshot.", + "tags": [ + "dave", + "music" + ], + "sha1": "35576dee65960ca62e5317f3099139346a493c33", + "file_size": 79275, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/me_Trivia.jpg" + }, + { + "filename": "meAndDave.jpg", + "relative_url": "/uploads/facebook-060426/meAndDave.jpg", + "title": "Dave And Dave Jr", + "description": "Dave with Dave Jr.", + "tags": [ + "child", + "dave", + "dave-jr" + ], + "sha1": "cf365d285f97f9e89fd8c3cfc718d01a289e994b", + "file_size": 57166, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/meAndDave.jpg" + }, + { + "filename": "meseum.jpg", + "relative_url": "/uploads/facebook-060426/meseum.jpg", + "title": "Museum", + "description": "Family outing or travel snapshot.", + "tags": [ + "outing" + ], + "sha1": "9c997b8e7aab6ae8775a78b08f2c62e68665ce9a", + "file_size": 62420, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/meseum.jpg" + }, + { + "filename": "meThanksgivifng.jpg", + "relative_url": "/uploads/facebook-060426/meThanksgivifng.jpg", + "title": "Methanksgivifng", + "description": "Family archive snapshot.", + "tags": [ + "dave" + ], + "sha1": "d99435a9805226381a3fc9f194d4bfe15297d04c", + "file_size": 77450, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/meThanksgivifng.jpg" + }, + { + "filename": "meThanksgivifng2.jpg", + "relative_url": "/uploads/facebook-060426/meThanksgivifng2.jpg", + "title": "Methanksgivifng2", + "description": "Family archive snapshot.", + "tags": [ + "dave" + ], + "sha1": "c3bd7ec1d761c6b91435772fedb8409a5cecd18c", + "file_size": 73990, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/meThanksgivifng2.jpg" + }, + { + "filename": "nova_dave.jpg", + "relative_url": "/uploads/facebook-060426/nova_dave.jpg", + "title": "Villanova Dave", + "description": "Villanova-flavored family snapshot.", + "tags": [ + "sports", + "villanova" + ], + "sha1": "c02edcd5f2d5dca8b09f13b91a442bad0b30e5d4", + "file_size": 381004, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/nova_dave.jpg" + }, + { + "filename": "poland.jpg", + "relative_url": "/uploads/facebook-060426/poland.jpg", + "title": "Poland", + "description": "Family outing or travel snapshot.", + "tags": [ + "outing" + ], + "sha1": "36dc76d2e2085aa8fddcfd1e388d68c52e48c32f", + "file_size": 57290, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/poland.jpg" + }, + { + "filename": "rangers.jpg", + "relative_url": "/uploads/facebook-060426/rangers.jpg", + "title": "Rangers", + "description": "Family archive snapshot.", + "tags": [ + "sports", + "villanova" + ], + "sha1": "b5d7f0ee208f4112f10c8e6f702d9c0460cd0199", + "file_size": 67276, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/rangers.jpg" + }, + { + "filename": "school.jpg", + "relative_url": "/uploads/facebook-060426/school.jpg", + "title": "School", + "description": "Family outing or travel snapshot.", + "tags": [ + "outing" + ], + "sha1": "d90b4ea37c0b48c096cd84e3ee626e2dd76ad087", + "file_size": 199176, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/school.jpg" + }, + { + "filename": "shirt_nova.jpg", + "relative_url": "/uploads/facebook-060426/shirt_nova.jpg", + "title": "Shirt Villanova", + "description": "Villanova-flavored family snapshot.", + "tags": [ + "dress-up", + "sports", + "villanova" + ], + "sha1": "59fdbbaccfd46f4588552d37f213183130525368", + "file_size": 169934, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/shirt_nova.jpg" + }, + { + "filename": "slide.jpg", + "relative_url": "/uploads/facebook-060426/slide.jpg", + "title": "Slide", + "description": "Everyday family moment.", + "tags": [ + "playtime" + ], + "sha1": "251813ec0452ebc4cfb209df83d0187ba1d3f15e", + "file_size": 22812, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/slide.jpg" + }, + { + "filename": "tree.jpg", + "relative_url": "/uploads/facebook-060426/tree.jpg", + "title": "Tree", + "description": "Everyday family moment.", + "tags": [ + "playtime" + ], + "sha1": "4855e84b162100c8dc8e2da650fd6582e9cdd1c7", + "file_size": 90667, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/tree.jpg" + }, + { + "filename": "unnamed (1).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (1).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "56171a6c7469be68bfd7bd0878e18d0f7baabd55", + "file_size": 1346585, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (1).jpg" + }, + { + "filename": "unnamed (2).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (2).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "c5712fccb645bf6f44188bf5c4df3ad88c5ebb62", + "file_size": 972770, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (2).jpg" + }, + { + "filename": "unnamed (3).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (3).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "67d0609a1d0abf62194fd2838383924cf406137b", + "file_size": 1209355, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (3).jpg" + }, + { + "filename": "unnamed (4).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (4).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "275c7ece11053cbfac4f6620a6a7557c10dd24ec", + "file_size": 1913489, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (4).jpg" + }, + { + "filename": "unnamed (5).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (5).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "5250a9573fcfeb9e547c0bebe45e42d7f32447f6", + "file_size": 763885, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (5).jpg" + }, + { + "filename": "unnamed (6).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (6).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "85b633181848d661a62129b202e94032481c2de5", + "file_size": 943074, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (6).jpg" + }, + { + "filename": "unnamed (7).jpg", + "relative_url": "/uploads/facebook-060426/unnamed (7).jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "a79426dddb6893890fb56edfdc3d5176f8b97cdd", + "file_size": 1643496, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed (7).jpg" + }, + { + "filename": "unnamed.jpg", + "relative_url": "/uploads/facebook-060426/unnamed.jpg", + "title": "Unnamed", + "description": "Family archive snapshot awaiting fuller annotation.", + "tags": [ + "family" + ], + "sha1": "2acb39cfcf1e9ac7b9639464eccf2b716b649d76", + "file_size": 1128732, + "duplicate_of": null, + "named_file": false, + "src": "/images/family-lab/facebook-060426/unnamed.jpg" + }, + { + "filename": "villanova1.jpg", + "relative_url": "/uploads/facebook-060426/villanova1.jpg", + "title": "Villanova1", + "description": "Villanova-flavored family snapshot.", + "tags": [ + "sports", + "villanova" + ], + "sha1": "6d6c4200b0bee104b5af3219ba21ad5b5faceb8c", + "file_size": 42330, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/villanova1.jpg" + }, + { + "filename": "withGrandma.jpg", + "relative_url": "/uploads/facebook-060426/withGrandma.jpg", + "title": "With Grandma", + "description": "Family portrait moment.", + "tags": [ + "family" + ], + "sha1": "1c7505d6a6da713d9f83fed376a12e37bb1b21c0", + "file_size": 91307, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/withGrandma.jpg" + }, + { + "filename": "withMom.jpg", + "relative_url": "/uploads/facebook-060426/withMom.jpg", + "title": "With Mom", + "description": "Family portrait moment.", + "tags": [ + "family" + ], + "sha1": "c367322bcb839dabe1eade5854cc3e6278520342", + "file_size": 54939, + "duplicate_of": null, + "named_file": true, + "src": "/images/family-lab/facebook-060426/withMom.jpg" + } +] diff --git a/public/images/family-lab/facebook-060426/marvinGaye.jpg b/public/images/family-lab/facebook-060426/marvinGaye.jpg new file mode 100644 index 0000000..8cc615c Binary files /dev/null and b/public/images/family-lab/facebook-060426/marvinGaye.jpg differ diff --git a/public/images/family-lab/facebook-060426/may17.jpg b/public/images/family-lab/facebook-060426/may17.jpg new file mode 100644 index 0000000..261bf4a Binary files /dev/null and b/public/images/family-lab/facebook-060426/may17.jpg differ diff --git a/public/images/family-lab/facebook-060426/me1.jpg b/public/images/family-lab/facebook-060426/me1.jpg new file mode 100644 index 0000000..a8e9a1a Binary files /dev/null and b/public/images/family-lab/facebook-060426/me1.jpg differ diff --git a/public/images/family-lab/facebook-060426/meAndDave.jpg b/public/images/family-lab/facebook-060426/meAndDave.jpg new file mode 100644 index 0000000..33ada81 Binary files /dev/null and b/public/images/family-lab/facebook-060426/meAndDave.jpg differ diff --git a/public/images/family-lab/facebook-060426/meThanksgivifng.jpg b/public/images/family-lab/facebook-060426/meThanksgivifng.jpg new file mode 100644 index 0000000..421ac67 Binary files /dev/null and b/public/images/family-lab/facebook-060426/meThanksgivifng.jpg differ diff --git a/public/images/family-lab/facebook-060426/meThanksgivifng2.jpg b/public/images/family-lab/facebook-060426/meThanksgivifng2.jpg new file mode 100644 index 0000000..6571cab Binary files /dev/null and b/public/images/family-lab/facebook-060426/meThanksgivifng2.jpg differ diff --git a/public/images/family-lab/facebook-060426/me_Nova.jpg b/public/images/family-lab/facebook-060426/me_Nova.jpg new file mode 100644 index 0000000..c8758bc Binary files /dev/null and b/public/images/family-lab/facebook-060426/me_Nova.jpg differ diff --git a/public/images/family-lab/facebook-060426/me_Trivia.jpg b/public/images/family-lab/facebook-060426/me_Trivia.jpg new file mode 100644 index 0000000..58f5405 Binary files /dev/null and b/public/images/family-lab/facebook-060426/me_Trivia.jpg differ diff --git a/public/images/family-lab/facebook-060426/meseum.jpg b/public/images/family-lab/facebook-060426/meseum.jpg new file mode 100644 index 0000000..49aeec7 Binary files /dev/null and b/public/images/family-lab/facebook-060426/meseum.jpg differ diff --git a/public/images/family-lab/facebook-060426/nova_dave.jpg b/public/images/family-lab/facebook-060426/nova_dave.jpg new file mode 100644 index 0000000..39741a3 Binary files /dev/null and b/public/images/family-lab/facebook-060426/nova_dave.jpg differ diff --git a/public/images/family-lab/facebook-060426/poland.jpg b/public/images/family-lab/facebook-060426/poland.jpg new file mode 100644 index 0000000..4e50ce2 Binary files /dev/null and b/public/images/family-lab/facebook-060426/poland.jpg differ diff --git a/public/images/family-lab/facebook-060426/rangers.jpg b/public/images/family-lab/facebook-060426/rangers.jpg new file mode 100644 index 0000000..3741ce9 Binary files /dev/null and b/public/images/family-lab/facebook-060426/rangers.jpg differ diff --git a/public/images/family-lab/facebook-060426/school.jpg b/public/images/family-lab/facebook-060426/school.jpg new file mode 100644 index 0000000..b705c44 Binary files /dev/null and b/public/images/family-lab/facebook-060426/school.jpg differ diff --git a/public/images/family-lab/facebook-060426/shirt_nova.jpg b/public/images/family-lab/facebook-060426/shirt_nova.jpg new file mode 100644 index 0000000..191f114 Binary files /dev/null and b/public/images/family-lab/facebook-060426/shirt_nova.jpg differ diff --git a/public/images/family-lab/facebook-060426/slide.jpg b/public/images/family-lab/facebook-060426/slide.jpg new file mode 100644 index 0000000..43c63d5 Binary files /dev/null and b/public/images/family-lab/facebook-060426/slide.jpg differ diff --git a/public/images/family-lab/facebook-060426/tree.jpg b/public/images/family-lab/facebook-060426/tree.jpg new file mode 100644 index 0000000..8f66c23 Binary files /dev/null and b/public/images/family-lab/facebook-060426/tree.jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (1).jpg b/public/images/family-lab/facebook-060426/unnamed (1).jpg new file mode 100644 index 0000000..a5df4aa Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (1).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (2).jpg b/public/images/family-lab/facebook-060426/unnamed (2).jpg new file mode 100644 index 0000000..ef85214 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (2).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (3).jpg b/public/images/family-lab/facebook-060426/unnamed (3).jpg new file mode 100644 index 0000000..c58a2d2 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (3).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (4).jpg b/public/images/family-lab/facebook-060426/unnamed (4).jpg new file mode 100644 index 0000000..252ab75 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (4).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (5).jpg b/public/images/family-lab/facebook-060426/unnamed (5).jpg new file mode 100644 index 0000000..5d9b351 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (5).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (6).jpg b/public/images/family-lab/facebook-060426/unnamed (6).jpg new file mode 100644 index 0000000..dd68ce7 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (6).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed (7).jpg b/public/images/family-lab/facebook-060426/unnamed (7).jpg new file mode 100644 index 0000000..5f1c872 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed (7).jpg differ diff --git a/public/images/family-lab/facebook-060426/unnamed.jpg b/public/images/family-lab/facebook-060426/unnamed.jpg new file mode 100644 index 0000000..a5151b4 Binary files /dev/null and b/public/images/family-lab/facebook-060426/unnamed.jpg differ diff --git a/public/images/family-lab/facebook-060426/villanova1.jpg b/public/images/family-lab/facebook-060426/villanova1.jpg new file mode 100644 index 0000000..97d7e97 Binary files /dev/null and b/public/images/family-lab/facebook-060426/villanova1.jpg differ diff --git a/public/images/family-lab/facebook-060426/withGrandma.jpg b/public/images/family-lab/facebook-060426/withGrandma.jpg new file mode 100644 index 0000000..fffdd75 Binary files /dev/null and b/public/images/family-lab/facebook-060426/withGrandma.jpg differ diff --git a/public/images/family-lab/facebook-060426/withMom.jpg b/public/images/family-lab/facebook-060426/withMom.jpg new file mode 100644 index 0000000..8480400 Binary files /dev/null and b/public/images/family-lab/facebook-060426/withMom.jpg differ diff --git a/scripts/__pycache__/build_facebook_album_manifest.cpython-314.pyc b/scripts/__pycache__/build_facebook_album_manifest.cpython-314.pyc new file mode 100644 index 0000000..256adda Binary files /dev/null and b/scripts/__pycache__/build_facebook_album_manifest.cpython-314.pyc differ diff --git a/scripts/build_facebook_album_manifest.py b/scripts/build_facebook_album_manifest.py new file mode 100644 index 0000000..130195f --- /dev/null +++ b/scripts/build_facebook_album_manifest.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import csv +import hashlib +import json +import re +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Iterable + + +SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"} + + +TOKEN_REPLACEMENTS = { + "jr": "Dave Jr", + "nova": "Villanova", + "meanddave": "Dave And Dave Jr", + "meanddave1": "Dave And Dave Jr", + "meandjr": "Dave And Dave Jr", + "meandjr2": "Dave And Dave Jr", + "withdavejr": "With Dave Jr", + "withdavejr1": "With Dave Jr", + "witherin": "With Erin", + "withkevin": "With Kevin", + "withmom": "With Mom", + "withgrandma": "With Grandma", + "jr_brigid": "Dave Jr And Brigid", + "jr_grandma": "Dave Jr And Grandma", + "jr_nova": "Dave Jr Villanova", + "jr_trivia": "Dave Jr Trivia", + "four_jr": "Dave Jr At Four", + "four_jr2": "Dave Jr At Four", + "shirt_nova": "Shirt Villanova", + "me_nova": "Me Villanova", + "citizen": "Citizen", + "meseum": "Museum", + "me_trivia": "Me Trivia", +} + +KNOWN_FACEBOOK_PREFIX_RE = re.compile(r"^\d{6,}_") + + +DESCRIPTION_RULES = [ + ({"villanova", "nova", "jr"}, "Dave Jr in Villanova gear."), + ({"meanddave", "meandjr", "withdavejr"}, "Dave with Dave Jr."), + ({"dave", "dad", "me"}, "Family moment with Dave."), + ({"jr"}, "Family snapshot featuring Dave Jr."), + ({"grandma", "mom", "kevin", "erin", "noreen"}, "Family portrait moment."), + ({"gift", "gifts"}, "Gift-opening or celebration moment."), + ({"school"}, "School-related family snapshot."), + ({"thanksgiving"}, "Holiday family gathering moment."), + ({"trivia"}, "Trivia and music related family snapshot."), + ({"poland"}, "Travel memory from Poland."), + ({"museum"}, "Museum or outing snapshot."), + ({"fireman"}, "Dress-up or costume moment."), +] + + +@dataclass +class MediaRow: + filename: str + relative_url: str + title: str + description: str + tags: list[str] + sha1: str + file_size: int + duplicate_of: str | None = None + named_file: bool = False + + +def slugify(text: str) -> str: + text = text.lower() + text = re.sub(r"[^a-z0-9]+", "-", text) + return text.strip("-") + + +def humanize_stem(stem: str) -> str: + cleaned = stem.strip() + cleaned = re.sub(r"^\d+", "", cleaned) + cleaned = re.sub(r"_\d{6,}.*$", "", cleaned) + cleaned = cleaned.replace("(1)", "").replace("(2)", "").replace("(3)", "") + cleaned = cleaned.replace("(4)", "").replace("(5)", "").replace("(6)", "").replace("(7)", "") + lowered = cleaned.lower().strip() + if lowered in TOKEN_REPLACEMENTS: + return TOKEN_REPLACEMENTS[lowered] + + lowered = lowered.replace("_", " ").replace("-", " ") + lowered = re.sub(r"\s+", " ", lowered).strip() + if not lowered: + return "Untitled Photo" + + words: list[str] = [] + for word in lowered.split(): + if word in {"jr", "jr."}: + words.append("Dave Jr") + elif word in {"nova", "villanova"}: + words.append("Villanova") + elif word in {"me", "dave", "kevin", "erin", "noreen", "mom", "grandma", "poland", "museum", "trivia"}: + words.append(word.capitalize()) + else: + words.append(word.capitalize()) + return " ".join(words) + + +def is_named_file(filename: str, title: str) -> bool: + stem = Path(filename).stem.lower() + if title == "Untitled Photo": + return False + if stem.startswith("unnamed"): + return False + if KNOWN_FACEBOOK_PREFIX_RE.match(stem): + return False + return True + + +def infer_tags(filename: str, title: str) -> list[str]: + text = f"{filename} {title}".lower() + tags: list[str] = [] + + if any(token in text for token in ("jr", "dave jr", "baby")): + tags.extend(["child", "dave-jr"]) + if any(token in text for token in ("me ", " me", "meand", " meand", "dave and")): + tags.append("dave") + if any(token in text for token in ("villanova", "nova", "rangers")): + tags.extend(["villanova", "sports"]) + if any(token in text for token in ("gift", "gifts", "thanksgiving", "christmas", "may17")): + tags.append("holiday") + if any(token in text for token in ("grandma", "mom", "kevin", "erin", "noreen", "brigid")): + tags.append("family") + if any(token in text for token in ("trivia", "music", "marvin gaye")): + tags.append("music") + if any(token in text for token in ("school", "museum", "poland", "citizen")): + tags.append("outing") + if any(token in text for token in ("fireman", "dress", "beret", "shirt", "glasses")): + tags.append("dress-up") + if any(token in text for token in ("haircut", "slide", "tree", "balls", "eating")): + tags.append("playtime") + if not tags: + tags.append("family") + + return sorted(set(tags)) + + +def infer_description(filename: str, title: str) -> str: + text = f"{filename} {title}".lower() + named_file = is_named_file(filename, title) + + if any(token in text for token in ("meanddave", "me and dave", "meandjr", "dave and dave jr")): + return "Dave with Dave Jr." + if any(token in text for token in ("withdavejr", "with dave jr")): + return "Family snapshot with Dave Jr." + if any(token in text for token in ("jr_nova", "villanova", "shirt villanova", "nova dave")): + return "Villanova-flavored family snapshot." + if any(token in text for token in ("gift", "gifts", "thanksgiving", "may17")): + return "Family celebration moment." + if any(token in text for token in ("fireman", "dress", "beret", "glasses")): + return "Dress-up family snapshot." + if any(token in text for token in ("school", "museum", "poland", "citizen")): + return "Family outing or travel snapshot." + if any(token in text for token in ("haircut", "slide", "tree", "balls", "eating")): + return "Everyday family moment." + if any(token in text for token in ("grandma", "mom", "kevin", "erin", "noreen", "brigid")): + return "Family portrait moment." + if any(token in text for token in ("jr", "dave jr")): + return "Family snapshot featuring Dave Jr." + if not named_file: + return "Family archive snapshot awaiting fuller annotation." + return "Family archive snapshot." + + +def sha1_for_file(path: Path) -> str: + digest = hashlib.sha1() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def build_rows(source_dir: Path, url_prefix: str) -> list[MediaRow]: + rows: list[MediaRow] = [] + first_seen_by_hash: dict[str, str] = {} + + for path in sorted(source_dir.iterdir(), key=lambda p: p.name.lower()): + if not path.is_file() or path.suffix.lower() not in SUPPORTED_EXTENSIONS: + continue + + title = humanize_stem(path.stem) + description = infer_description(path.name, title) + tags = infer_tags(path.name, title) + digest = sha1_for_file(path) + duplicate_of = first_seen_by_hash.get(digest) + if duplicate_of is None: + first_seen_by_hash[digest] = path.name + + rows.append( + MediaRow( + filename=path.name, + relative_url=f"{url_prefix.rstrip('/')}/{path.name}", + title=title, + description=description, + tags=tags, + sha1=digest, + file_size=path.stat().st_size, + duplicate_of=duplicate_of, + named_file=is_named_file(path.name, title), + ) + ) + + return rows + + +def write_json(rows: Iterable[MediaRow], target: Path) -> None: + payload = [asdict(row) for row in rows] + target.write_text(json.dumps(payload, indent=2, ensure_ascii=True), encoding="utf-8") + + +def write_csv(rows: Iterable[MediaRow], target: Path) -> None: + fieldnames = [ + "filename", + "relative_url", + "title", + "description", + "tags", + "sha1", + "file_size", + "duplicate_of", + "named_file", + ] + with target.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for row in rows: + data = asdict(row) + data["tags"] = json.dumps(data["tags"], ensure_ascii=True) + writer.writerow(data) + + +def sql_escape(value: str) -> str: + return value.replace("\\", "\\\\").replace("'", "''") + + +def choose_cover_url(rows: list[MediaRow]) -> str: + preferred_rows = [row for row in rows if row.named_file and not row.duplicate_of] + if preferred_rows: + ranked = sorted( + preferred_rows, + key=lambda row: ( + 0 if "dave and dave jr" in row.title.lower() else 1, + 0 if "dave jr" in row.title.lower() else 1, + row.filename.lower(), + ), + ) + return ranked[0].relative_url + unique_rows = [row for row in rows if not row.duplicate_of] + if unique_rows: + return unique_rows[0].relative_url + return rows[0].relative_url + + +def write_sql(rows: list[MediaRow], target: Path, slug: str, album_title: str, cover_url: str, unique_only: bool) -> None: + rows_to_write = [row for row in rows if not row.duplicate_of] if unique_only else rows + lines = [ + "-- Generated by build_facebook_album_manifest.py", + f"SET @folder_slug = '{sql_escape(slug)}';", + f"SET @cover_url = '{sql_escape(cover_url)}';", + "", + "INSERT INTO media_folders (slug, name_en, name_nb, description_en, description_nb, cover_image_url, active, sort_order)", + "VALUES (", + f" @folder_slug,", + f" '{sql_escape(album_title)}',", + f" '{sql_escape(album_title)}',", + " 'Imported Facebook family archive with starter captions generated from filenames. Descriptions should be reviewed and enriched.',", + " 'Importert familiearkiv fra Facebook med starttekster generert fra filnavn. Beskrivelsene bor gjennomgaas og forbedres.',", + " @cover_url,", + " 1,", + " 0", + ")", + "ON DUPLICATE KEY UPDATE", + " name_en = VALUES(name_en),", + " name_nb = VALUES(name_nb),", + " description_en = VALUES(description_en),", + " description_nb = VALUES(description_nb),", + " cover_image_url = VALUES(cover_image_url);", + "", + "SET @folder_id = (SELECT id FROM media_folders WHERE slug = @folder_slug LIMIT 1);", + "", + ] + + for index, row in enumerate(rows_to_write, start=1): + tags_json = json.dumps(row.tags, ensure_ascii=True) + lines.extend( + [ + "INSERT INTO media (type, filename, url, title, description, category, folder_id, tags, credit, created_at)", + "VALUES (", + " 'image',", + f" '{sql_escape(row.filename)}',", + f" '{sql_escape(row.relative_url)}',", + f" '{sql_escape(row.title)}',", + f" '{sql_escape(row.description)}',", + " 'family',", + " @folder_id,", + f" '{sql_escape(tags_json)}',", + " 'Facebook archive import',", + " NOW()", + ")", + "ON DUPLICATE KEY UPDATE", + " title = VALUES(title),", + " description = VALUES(description),", + " folder_id = VALUES(folder_id),", + " tags = VALUES(tags),", + " credit = VALUES(credit);", + "", + ] + ) + + target.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Build a JSON/CSV/SQL manifest for a photo folder.") + parser.add_argument("--source", required=True, help="Path to the source image folder.") + parser.add_argument("--slug", default="facebook-060426", help="Target media folder slug.") + parser.add_argument("--title", default="Facebook Archive / June 4 2026", help="Folder title.") + parser.add_argument("--url-prefix", default="/uploads/facebook-060426", help="URL prefix for deployed images.") + parser.add_argument("--output-dir", default="C:\\wamp64\\www\\davegilligan-new\\tmp", help="Directory for generated files.") + parser.add_argument("--include-duplicates", action="store_true", help="Include duplicate hashes in the SQL seed.") + args = parser.parse_args() + + source_dir = Path(args.source) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + rows = build_rows(source_dir, args.url_prefix) + if not rows: + raise SystemExit("No supported images found.") + + cover_url = choose_cover_url(rows) + stem = slugify(args.slug) + + write_json(rows, output_dir / f"{stem}-manifest.json") + write_csv(rows, output_dir / f"{stem}-manifest.csv") + write_sql( + rows, + output_dir / f"{stem}-seed.sql", + args.slug, + args.title, + cover_url, + unique_only=not args.include_duplicates, + ) + + duplicate_count = sum(1 for row in rows if row.duplicate_of) + print(f"Generated manifest for {len(rows)} files") + print(f"Duplicate files detected: {duplicate_count}") + print(f"Unique rows for SQL seed: {len([row for row in rows if not row.duplicate_of]) if not args.include_duplicates else len(rows)}") + print(f"Cover image: {cover_url}") + print(f"Output directory: {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/scripts/sync-family-lab.mjs b/scripts/sync-family-lab.mjs new file mode 100644 index 0000000..9c3ecfe --- /dev/null +++ b/scripts/sync-family-lab.mjs @@ -0,0 +1,65 @@ +import { mkdir, copyFile, readFile, readdir, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; + +const root = process.cwd(); +const sourceDir = path.join(root, "images", "facebook-060426"); +const manifestPath = path.join(root, "tmp", "facebook-060426-manifest.json"); +const targetDir = path.join(root, "public", "images", "family-lab", "facebook-060426"); +const targetManifest = path.join(targetDir, "manifest.json"); + +const supported = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif"]); + +function titleFromFilename(filename) { + const stem = path.parse(filename).name; + return stem + .replace(/^\d+_[0-9a-z]+_/i, "") + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim() || "Untitled Photo"; +} + +function fallbackRowsFromFiles(files) { + return files.map((filename) => ({ + filename, + title: titleFromFilename(filename), + description: "Family archive snapshot awaiting fuller annotation.", + tags: ["family"], + duplicate_of: null, + named_file: !/^\d{6,}_/.test(filename) && !/^unnamed/i.test(filename), + })); +} + +async function main() { + await mkdir(targetDir, { recursive: true }); + + const sourceFiles = (await readdir(sourceDir)) + .filter((filename) => supported.has(path.extname(filename).toLowerCase())) + .sort((a, b) => a.localeCompare(b)); + + let rows; + if (existsSync(manifestPath)) { + rows = JSON.parse(await readFile(manifestPath, "utf-8")); + } else { + rows = fallbackRowsFromFiles(sourceFiles); + } + + const uniqueRows = rows + .filter((row) => !row.duplicate_of) + .map((row) => ({ + ...row, + src: `/images/family-lab/facebook-060426/${row.filename}`, + })); + + for (const row of uniqueRows) { + await copyFile(path.join(sourceDir, row.filename), path.join(targetDir, row.filename)); + } + + await writeFile(targetManifest, `${JSON.stringify(uniqueRows, null, 2)}\n`, "utf-8"); + console.log(`Family lab synced: ${uniqueRows.length} unique photos`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/components/BusinessIssue.jsx b/src/components/BusinessIssue.jsx new file mode 100644 index 0000000..ef9afeb --- /dev/null +++ b/src/components/BusinessIssue.jsx @@ -0,0 +1,203 @@ +import { useEffect, useState } from "react"; + +function stripHtml(html) { + return html + .replace(//gi, "") + .replace(//gi, "") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function excerpt(html, length = 220) { + const plain = stripHtml(html); + return plain.length <= length ? plain : `${plain.slice(0, length).trimEnd()}...`; +} + +function LoadingBlock() { + return ( +
+
+ Business desk + Fetching live file +
+

Loading the April business issue...

+

The public business desk is pulling its copy from the live PHP archive.

+
+ ); +} + +function ErrorBlock({ message }) { + return ( +
+
+ Business desk + Archive offline +
+

The business issue could not be loaded.

+

{message}

+
+ ); +} + +export default function BusinessIssue() { + const [state, setState] = useState({ + status: "loading", + section: null, + error: "", + }); + + useEffect(() => { + const controller = new AbortController(); + + async function load() { + try { + const response = await fetch("/api/sections.php?lang=en&slug=business", { + signal: controller.signal, + }); + if (!response.ok) throw new Error("The CMS endpoint did not respond cleanly."); + const json = await response.json(); + const section = Array.isArray(json) ? json[0] : json; + if (!section) throw new Error("The business desk payload was empty."); + setState({ status: "ready", section, error: "" }); + } catch (error) { + if (controller.signal.aborted) return; + setState({ + status: "error", + section: null, + error: error instanceof Error ? error.message : "Unknown loading error.", + }); + } + } + + load(); + return () => controller.abort(); + }, []); + + if (state.status === "loading") return ; + if (state.status === "error") return ; + + const { section } = state; + const articles = Array.isArray(section.articles) ? section.articles : []; + const feature = + articles.find((article) => article.title.startsWith("April Feature")) ?? articles[0] ?? null; + const memo = + articles.find((article) => article.title.startsWith("Companion Memo")) ?? articles[1] ?? null; + + return ( +
+
+
+ {section.headline} +

{section.name}

+
+ {feature && ( +
+
+ Lead file + {feature.title} +

{excerpt(feature.body, 210)}

+
+ {memo && ( +
+ Companion memo + {memo.title} +

{excerpt(memo.body, 170)}

+
+ )} +
+ )} +
+ + +
+ +
+
+
+ Feature essay + Agents / labour / Queneau / Ionesco / Prévert +
+ {feature ? ( +
+ ) : ( +

No feature article is available yet.

+ )} +
+ + +
+
+ ); +} diff --git a/src/components/CookieBanner.astro b/src/components/CookieBanner.astro new file mode 100644 index 0000000..e55abc7 --- /dev/null +++ b/src/components/CookieBanner.astro @@ -0,0 +1,158 @@ +--- +import LocaleCopy from "./LocaleCopy.astro"; +import { cookieBannerCopy } from "../data/locales"; +--- + + + + diff --git a/src/components/FamilyAtlas.jsx b/src/components/FamilyAtlas.jsx new file mode 100644 index 0000000..25c56a0 --- /dev/null +++ b/src/components/FamilyAtlas.jsx @@ -0,0 +1,189 @@ +import { useEffect, useState } from "react"; + +const filterPhoto = (photo, filter) => { + if (filter === "all") return true; + if (filter === "named") return photo.namedFile; + return photo.tags?.includes(filter); +}; + +const variantForIndex = (index) => { + const variants = ["tall", "wide", "square", "tall", "square", "wide"]; + return variants[index % variants.length]; +}; + +export default function FamilyAtlas({ photos = [], featured = [], filters = [] }) { + const [activeFilter, setActiveFilter] = useState("all"); + const [lightboxIndex, setLightboxIndex] = useState(0); + const [lightboxOpen, setLightboxOpen] = useState(false); + + const visiblePhotos = photos.filter((photo) => filterPhoto(photo, activeFilter)); + const currentPhoto = lightboxOpen ? visiblePhotos[lightboxIndex] : featured[0] ?? visiblePhotos[0] ?? null; + + useEffect(() => { + if (!lightboxOpen) return undefined; + + const handleKeyDown = (event) => { + if (event.key === "Escape") { + setLightboxOpen(false); + } else if (event.key === "ArrowRight") { + setLightboxIndex((index) => (index + 1) % visiblePhotos.length); + } else if (event.key === "ArrowLeft") { + setLightboxIndex((index) => (index - 1 + visiblePhotos.length) % visiblePhotos.length); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [lightboxOpen, visiblePhotos.length]); + + useEffect(() => { + setLightboxIndex(0); + }, [activeFilter]); + + const openLightbox = (index) => { + setLightboxIndex(index); + setLightboxOpen(true); + }; + + return ( +
+
+
+ Memory atlas / image-led archive test + {visiblePhotos.length} visible frames +
+ +
+ {filters.map((filter) => ( + + ))} +
+
+ +
+ + +
+ {visiblePhotos.map((photo, index) => ( + + ))} +
+
+ + {lightboxOpen && currentPhoto && ( +
setLightboxOpen(false)} + > +
event.stopPropagation()}> + + +
+ + + {currentPhoto.title} + + +
+ +
+

+ Frame {lightboxIndex + 1} / {visiblePhotos.length} +

+

{currentPhoto.title}

+

{currentPhoto.description}

+
+ {(currentPhoto.tags ?? []).map((tag) => ( + {tag.replace("-", " ")} + ))} +
+
+
+
+ )} +
+ ); +} diff --git a/src/components/LocaleCopy.astro b/src/components/LocaleCopy.astro new file mode 100644 index 0000000..84819ec --- /dev/null +++ b/src/components/LocaleCopy.astro @@ -0,0 +1,32 @@ +--- +import type { LocaleCode } from "../data/locales"; + +interface Props { + copy: Record; + className?: string; + tag?: string; + html?: boolean; +} + +const { + copy, + className = "", + tag = "span", + html = false, +} = Astro.props; + +const Tag = tag; +const order: LocaleCode[] = ["en", "fr", "nb"]; +--- + + + {order.map((lang) => ( + + {html ? undefined : copy[lang]} + + ))} + diff --git a/src/components/LocaleSwitcher.astro b/src/components/LocaleSwitcher.astro new file mode 100644 index 0000000..a5dcbe2 --- /dev/null +++ b/src/components/LocaleSwitcher.astro @@ -0,0 +1,69 @@ +--- +import LocaleCopy from "./LocaleCopy.astro"; +import { localeMeta } from "../data/locales"; +--- + +
+
+ + + +
+ + +
+ + diff --git a/src/components/SectionCard.astro b/src/components/SectionCard.astro index 7519bc9..3abf291 100644 --- a/src/components/SectionCard.astro +++ b/src/components/SectionCard.astro @@ -11,6 +11,80 @@ interface Props { } const { slug, label, title, summary, tone, href = "#" } = Astro.props; + +const sectionArt: Record< + string, + { src: string; alt: string; eyebrow: string; caption: string } +> = { + business: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg", + alt: "Grand-Place in Brussels.", + eyebrow: "Ledger", + caption: "Continental deals and operator weather", + }, + education: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Jagiellonian_University_Collegium_Novum%2C_1882_designed_by_Feliks_Ksi%C4%99%C5%BCarski%2C_24_Go%C5%82%C4%99bia_street%2C_Old_Town%2C_Krak%C3%B3w%2C_Poland_%284%29.jpg", + alt: "Collegium Novum at Jagiellonian University in Krakow.", + eyebrow: "Dossier", + caption: "Krakow, memory, and the institutional sublime", + }, + family: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG", + alt: "Riverfront in Kongsberg, Norway.", + eyebrow: "Archive", + caption: "Private weather by the Norwegian river", + }, + "fun-postings": { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg", + alt: "Grand-Place in Brussels at dusk.", + eyebrow: "Poster wall", + caption: "Flyers, evenings, and elegant side quests", + }, + writing: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Escp-Paris.jpg", + alt: "ESCP building in Paris.", + eyebrow: "Notebook", + caption: "Paris pages with brass in the margins", + }, + "jazz-music": { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Level_42_Kongsberg_Jazzfestival_2017_%28214257%29.jpg", + alt: "Performance at Kongsberg Jazzfestival.", + eyebrow: "Low light", + caption: "Kongsberg nights and disciplined swing", + }, + languages: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg", + alt: "Historic buildings in Brussels.", + eyebrow: "Register shift", + caption: "French, English, Norwegian, and trouble", + }, + "ai-lab": { + src: "/images/ai-lab/corpus-grid.svg", + alt: "Illustrated knowledge corpus grid.", + eyebrow: "Machine room", + caption: "Private memory with cited answers", + }, + norway: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG", + alt: "Waterfront in Kongsberg, Norway.", + eyebrow: "Field report", + caption: "Silver city, civic weather, due process", + }, + projects: { + src: "/images/ai-lab/api-flow.svg", + alt: "Diagram of a connected API workflow.", + eyebrow: "Bench test", + caption: "Products, repairs, and live deployments", + }, + cv: { + src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Villanova_University_A_panoramic_shot.jpg", + alt: "Panoramic view of Villanova University.", + eyebrow: "Record", + caption: "A timeline written across cities", + }, +}; + +const art = sectionArt[slug]; ---
@@ -21,6 +95,15 @@ const { slug, label, title, summary, tone, href = "#" } = Astro.props; {tone} + {art && ( +
+ {art.alt} +
+ {art.eyebrow} + {art.caption} +
+
+ )}

{title}

{summary}

Open section diff --git a/src/components/WritingIssue.jsx b/src/components/WritingIssue.jsx new file mode 100644 index 0000000..c5ef799 --- /dev/null +++ b/src/components/WritingIssue.jsx @@ -0,0 +1,215 @@ +import { useEffect, useState } from "react"; + +function stripHtml(html) { + return html + .replace(//gi, "") + .replace(//gi, "") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function excerpt(html, length = 220) { + const plain = stripHtml(html); + if (plain.length <= length) return plain; + return `${plain.slice(0, length).trimEnd()}...`; +} + +function LoadingBlock() { + return ( +
+
+ Writing desk + Fetching live file +
+

Loading the Boris Vian issue...

+

The public writing desk is pulling its copy from the live PHP archive.

+
+ ); +} + +function ErrorBlock({ message }) { + return ( +
+
+ Writing desk + Archive offline +
+

The writing issue could not be loaded.

+

{message}

+
+ ); +} + +export default function WritingIssue() { + const [state, setState] = useState({ + status: "loading", + section: null, + feature: null, + reading: null, + error: "", + }); + + useEffect(() => { + const controller = new AbortController(); + + async function load() { + try { + const [sectionRes, featureRes, readingRes] = await Promise.all([ + fetch("/api/sections.php?lang=en&slug=writing", { signal: controller.signal }), + fetch("/api/pages.php?lang=en&slug=boris-vian-april-2026", { signal: controller.signal }), + fetch("/api/pages.php?lang=en&slug=boris-vian-reading-route", { signal: controller.signal }), + ]); + + if (!sectionRes.ok || !featureRes.ok || !readingRes.ok) { + throw new Error("The CMS endpoints did not respond cleanly."); + } + + const sectionJson = await sectionRes.json(); + const featureJson = await featureRes.json(); + const readingJson = await readingRes.json(); + const section = Array.isArray(sectionJson) ? sectionJson[0] : sectionJson; + + if (!section || !featureJson || !readingJson) { + throw new Error("The writing issue payload was incomplete."); + } + + setState({ + status: "ready", + section, + feature: featureJson, + reading: readingJson, + error: "", + }); + } catch (error) { + if (controller.signal.aborted) return; + setState({ + status: "error", + section: null, + feature: null, + reading: null, + error: error instanceof Error ? error.message : "Unknown loading error.", + }); + } + } + + load(); + return () => controller.abort(); + }, []); + + if (state.status === "loading") return ; + if (state.status === "error") return ; + + const { section, feature, reading } = state; + const leadArticles = Array.isArray(section.articles) ? section.articles : []; + const featureArticle = + leadArticles.find((article) => article.title === feature.title) ?? leadArticles[0] ?? null; + const readingArticle = + leadArticles.find((article) => article.title === reading.title) ?? leadArticles[1] ?? null; + + return ( +
+
+
+ {section.headline} +

{section.name}

+
+ +
+
+ Lead feature + {feature.title} +

{excerpt(feature.content, 210)}

+
+ +
+ Companion route + {reading.title} +

{excerpt(reading.content, 180)}

+
+
+
+ + +
+ +
+
+
+ Feature essay + Boris / Vernon / jazz / Paris +
+
+
+ + +
+
+ ); +} diff --git a/src/data/family-lab.ts b/src/data/family-lab.ts new file mode 100644 index 0000000..3005391 --- /dev/null +++ b/src/data/family-lab.ts @@ -0,0 +1,59 @@ +export const familyLabFeature = { + eyebrow: "Family laboratory / private test issue", + title: "A pataphysical family atlas built from one unruly folder and a boy called Dave Jr.", + lede: + "Not a neutral gallery. Not a cold database. A bright proof-sheet salon for father-and-son evidence, Villanova weather, sleep-heavy domestic grace, and the gentle bureaucratic miracle of naming things before they vanish.", + note: + "This first pass keeps the captions honest, lets the images breathe, and treats the archive as material for a future family desk rather than a pile of uploads.", +}; + +export const familyLabStats = [ + { + value: "94", + label: "unique frames", + note: "The duplicates are folded away so the room reads like an edit, not an export.", + }, + { + value: "Dave Jr", + label: "central orbit", + note: "Your son is the emotional anchor, which is why the named files lead the composition.", + }, + { + value: "Moustache era", + label: "authenticated", + note: "The father-and-son images stay explicit so the archive starts with recognisable truth.", + }, +]; + +export const familyLabFeaturedFilenames = [ + "0meanddave1.jpg", + "meAndDave.jpg", + "jr_nova.jpg", + "villanova1.jpg", + "withGrandma.jpg", + "fireman.jpg", +]; + +export const familyLabNotes = [ + { + title: "Poster logic, not gallery logic", + body: "The first screen behaves like a cover: one big emotional claim, then a controlled spill of proof.", + }, + { + title: "Named frames first", + body: "Files with human names carry the warmth, while the Facebook hashes recede into the archive layer.", + }, + { + title: "Private by temperament", + body: "The design can mature into a family-only desk, but this test page already gives the photographs dignity.", + }, +]; + +export const familyLabFilters = [ + { key: "all", label: "All frames" }, + { key: "named", label: "Named files" }, + { key: "dave-jr", label: "Dave Jr" }, + { key: "dave", label: "Father and son" }, + { key: "villanova", label: "Villanova weather" }, + { key: "family", label: "Family orbit" }, +]; diff --git a/src/data/locales.ts b/src/data/locales.ts new file mode 100644 index 0000000..990e8d1 --- /dev/null +++ b/src/data/locales.ts @@ -0,0 +1,336 @@ +export type LocaleCode = "en" | "fr" | "nb"; + +export const localeOrder: LocaleCode[] = ["en", "fr", "nb"]; + +export const localeMeta: Record = { + en: { + label: "English", + switcher: "EN edition", + note: "The interface changes language first. Long-form features stay in their original edition until translated.", + }, + fr: { + label: "Francais", + switcher: "Cahier FR", + note: "L'interface change de langue d'abord. Les longs articles restent dans leur edition d'origine pour l'instant.", + }, + nb: { + label: "Norsk bokmal", + switcher: "NB utgave", + note: "Grensesnittet skifter sprak forst. Langartiklene blir staende i originalutgaven til de er oversatt.", + }, +}; + +export const localizedSections: Record< + string, + Record +> = { + business: { + en: { title: "Business", tone: "Sharp, practical, anti-buzzword." }, + fr: { title: "Affaires", tone: "Net, pratique, allergique au jargon." }, + nb: { title: "Naeringsliv", tone: "Skarpt, praktisk og anti-buzzword." }, + }, + education: { + en: { title: "Education", tone: "Curious, rigorous, lightly mischievous." }, + fr: { title: "Etudes", tone: "Curieux, rigoureux, legerement malicieux." }, + nb: { title: "Utdanning", tone: "Nysgjerrig, grundig og litt rampete." }, + }, + family: { + en: { title: "Family", tone: "Private-minded, generous, alive." }, + fr: { title: "Famille", tone: "Prive, genereux, bien vivant." }, + nb: { title: "Familie", tone: "Privat, varm og levende." }, + }, + "fun-postings": { + en: { title: "Fun Postings", tone: "Playful, deadpan, collectible." }, + fr: { title: "Annonces Delicieuses", tone: "Ludique, pince-sans-rire, a collectionner." }, + nb: { title: "Leken Oppslagstavle", tone: "Leken, torrr og samleverdig." }, + }, + writing: { + en: { title: "Writing", tone: "Literary, international, smoky." }, + fr: { title: "Ecriture", tone: "Litteraire, international, fumeux dans le bon sens." }, + nb: { title: "Skriving", tone: "Litteraer, internasjonal og litt roykfylt." }, + }, + "jazz-music": { + en: { title: "Jazz and Music", tone: "Velvet, brassy, precise." }, + fr: { title: "Jazz et Musique", tone: "Velours, cuivre, precision." }, + nb: { title: "Jazz og Musikk", tone: "Fløyel, messing og presisjon." }, + }, + languages: { + en: { title: "Languages", tone: "Polyglot, sly, welcoming." }, + fr: { title: "Langues", tone: "Polyglotte, malin, accueillant." }, + nb: { title: "Sprak", tone: "Polyglott, lur og inkluderende." }, + }, + "ai-lab": { + en: { title: "AI Lab", tone: "Forward-looking, grounded, open source friendly." }, + fr: { title: "Laboratoire IA", tone: "Tourne vers l'avenir, solide, ami de l'open source." }, + nb: { title: "AI-lab", tone: "Fremtidsrettet, jordnaer og vennlig mot apen kildekode." }, + }, + norway: { + en: { title: "Norway", tone: "Observant, civic, place-aware." }, + fr: { title: "Norvege", tone: "Observateur, civique, attentif au lieu." }, + nb: { title: "Norge", tone: "Observant, samfunnsbevisst og stedsnart." }, + }, + projects: { + en: { title: "Projects", tone: "Builder energy, clean receipts." }, + fr: { title: "Projets", tone: "Energie d'atelier, comptes propres." }, + nb: { title: "Prosjekter", tone: "Byggerenergi og ryddige spor." }, + }, + cv: { + en: { title: "CV", tone: "Professional, legible, confident." }, + fr: { title: "CV", tone: "Professionnel, lisible, assure." }, + nb: { title: "CV", tone: "Profesjonell, lesbar og trygg." }, + }, + "family-lab": { + en: { title: "Family Lab", tone: "Private archive, atlas, memory room." }, + fr: { title: "Laboratoire Familial", tone: "Archive privee, atlas, chambre de memoire." }, + nb: { title: "Familielab", tone: "Privat arkiv, atlas og minnerom." }, + }, +}; + +type ChromeContext = { + activeSlug: string; + issueDate: Record; + articleKey?: "norway" | "jazz" | "projects" | null; +}; + +const articleLabels: Record> = { + norway: { + en: { label: "Article / Norway desk", note: "Field report / family life / fathers / immigrants" }, + fr: { label: "Article / Cahier Norvege", note: "Reportage / famille / peres / immigration" }, + nb: { label: "Artikkel / Norge-desk", note: "Feltrapport / familieliv / fedre / innvandring" }, + }, + jazz: { + en: { label: "Article / Jazz and Music", note: "Field report / Kongsberg x Paris" }, + fr: { label: "Article / Jazz et Musique", note: "Reportage / Kongsberg x Paris" }, + nb: { label: "Artikkel / Jazz og Musikk", note: "Feltrapport / Kongsberg x Paris" }, + }, + projects: { + en: { label: "Article / Projects desk", note: "Field report / music trivia / Blue Note Rhino / April build" }, + fr: { label: "Article / Cahier Projets", note: "Reportage / quiz musical / Blue Note Rhino / chantier d'avril" }, + nb: { label: "Artikkel / Prosjekt-desk", note: "Feltrapport / musikktrivia / Blue Note Rhino / aprilbygg" }, + }, +}; + +export function getChromeCopy({ activeSlug, issueDate, articleKey }: ChromeContext) { + const currentSection = localizedSections[activeSlug]; + const article = articleKey ? articleLabels[articleKey] : null; + const sectionOrder = { + business: "01", + education: "02", + family: "03", + "fun-postings": "04", + writing: "05", + "jazz-music": "06", + languages: "07", + "ai-lab": "08", + norway: "09", + projects: "10", + cv: "11", + "family-lab": "12", + } as const; + const sectionNumber = sectionOrder[activeSlug as keyof typeof sectionOrder]; + + return { + ribbonIssue: { + en: `Founding issue / ${issueDate.en}`, + fr: `Numero fondateur / ${issueDate.fr}`, + nb: `Grunnutgave / ${issueDate.nb}`, + }, + ribbonNote: currentSection + ? { + en: currentSection.en.tone, + fr: currentSection.fr.tone, + nb: currentSection.nb.tone, + } + : article + ? { + en: article.en.note, + fr: article.fr.note, + nb: article.nb.note, + } + : { + en: "Ringwood / Villanova / Brussels / Paris / Krakow / Oslo / Kongsberg", + fr: "Ringwood / Villanova / Bruxelles / Paris / Cracovie / Oslo / Kongsberg", + nb: "Ringwood / Villanova / Brussel / Paris / Krakow / Oslo / Kongsberg", + }, + issueLabel: currentSection + ? { + en: `Section ${sectionNumber} / ${currentSection.en.title}`, + fr: `Section ${sectionNumber} / ${currentSection.fr.title}`, + nb: `Seksjon ${sectionNumber} / ${currentSection.nb.title}`, + } + : article + ? { + en: article.en.label, + fr: article.fr.label, + nb: article.nb.label, + } + : { + en: "Founding issue / Personal edition", + fr: "Numero fondateur / Edition personnelle", + nb: "Grunnutgave / Personlig utgave", + }, + mastheadLine: { + en: "Jazz desk / machine room / family archive / pataphysical bulletin", + fr: "Cahier jazz / salle des machines / archive familiale / bulletin pataphysique", + nb: "Jazzdesk / maskinrom / familiearkiv / pataphysisk bulletin", + }, + domainPrompt: { + en: "Private AI. Jazz rooms. Civic weather. Pataphysical field notes.", + fr: "IA privee. Salles de jazz. Meteo civique. Notes de terrain pataphysiques.", + nb: "Privat AI. Jazzrom. Samfunnsvaer. Pataphysiske feltnotater.", + }, + domainLede: { + en: "A bright retro paper for machine rooms, multilingual weather, Norway, family rights, live culture, and the deliberate misuse of the impossible.", + fr: "Un journal retro-lumineux pour les salles des machines, le temps multilingue, la Norvege, les droits familiaux, la culture vivante et l'usage delibere de l'impossible.", + nb: "Et lyst retroblad for maskinrom, flerspraklig vaer, Norge, familierett, levende kultur og bevisst misbruk av det umulige.", + }, + languageNote: { + en: localeMeta.en.note, + fr: localeMeta.fr.note, + nb: localeMeta.nb.note, + }, + footerHeadline: { + en: "Jazz desk, machine room, civic archive, and practical impossibility.", + fr: "Cahier jazz, salle des machines, archive civique et impossibilite pratique.", + nb: "Jazzdesk, maskinrom, samfunnsarkiv og praktisk umulighet.", + }, + footerBody: { + en: "Built in Astro with React islands, backed by PHP and SQL, and edited like a proper newspaper rather than a consultant PDF with lipstick.", + fr: "Construit avec Astro et des ilots React, soutenu par PHP et SQL, puis edite comme un vrai journal plutot qu'un PDF de conseil maquille.", + nb: "Bygget i Astro med React-oyer, drevet av PHP og SQL, og redigert som en ordentlig avis i stedet for en konsulent-PDF med leppestift.", + }, + footerNoteLeft: { + en: "Astro + React islands + PHP + SQL", + fr: "Astro + ilots React + PHP + SQL", + nb: "Astro + React-oyer + PHP + SQL", + }, + footerNoteRight: { + en: "Blue Note Logic / Gilligan TECH / Kongsberg / multilingual edition", + fr: "Blue Note Logic / Gilligan TECH / Kongsberg / edition multilingue", + nb: "Blue Note Logic / Gilligan TECH / Kongsberg / flerspraklig utgave", + }, + }; +} + +export const footerCallouts = { + blueNoteLogic: { + href: "https://bluenotelogic.com/", + label: { + en: "Blue Note Logic", + fr: "Blue Note Logic", + nb: "Blue Note Logic", + }, + title: { + en: "Private AI and document intelligence with a real memory.", + fr: "IA privee et intelligence documentaire avec une vraie memoire.", + nb: "Privat AI og dokumentintelligens med ekte hukommelse.", + }, + body: { + en: "Blue Note Logic frames AI as owned infrastructure: strategy, deployment, document intelligence, and source-aware systems that keep working knowledge inside the firm.", + fr: "Blue Note Logic traite l'IA comme une infrastructure possedee : strategie, deploiement, intelligence documentaire et systemes cites a la source qui gardent la connaissance dans l'entreprise.", + nb: "Blue Note Logic behandler AI som eid infrastruktur: strategi, utrulling, dokumentintelligens og kildebevisste systemer som holder kunnskapen i virksomheten.", + }, + }, + gilliganTech: { + href: "https://gilligan.tech/", + label: { + en: "Gilligan TECH", + fr: "Gilligan TECH", + nb: "Gilligan TECH", + }, + title: { + en: "Kongsberg-side systems engineering for Nordic SMBs.", + fr: "Ingenierie de systemes depuis Kongsberg pour les PME nordiques.", + nb: "Systemingeniorarbeid fra Kongsberg for nordiske SMB-er.", + }, + body: { + en: "Gilligan Tech is the Norwegian operating arm: AI audits, system builds, fractional CTO work, and sovereign European delivery for companies that want results before theatre.", + fr: "Gilligan Tech est le bras norvegien d'operation : audits IA, constructions de systemes, travail de CTO fractionnaire et livraison souveraine europeenne pour les entreprises qui veulent des resultats avant le theatre.", + nb: "Gilligan Tech er den norske operasjonsarmen: AI-audits, systembygging, fractional CTO-arbeid og suveren europeisk levering for selskaper som vil ha resultater foran teater.", + }, + }, +}; + +export const policyLinkCopy = { + privacy: { + href: "/privacy", + title: { + en: "Privacy policy", + fr: "Politique de confidentialite", + nb: "Personvern", + }, + body: { + en: "How the site handles accounts, family access, forms, and first-party data.", + fr: "Comment le site gere les comptes, l'acces familial, les formulaires et les donnees de premiere main.", + nb: "Hvordan nettstedet handterer kontoer, familieadgang, skjemaer og forstepartsdata.", + }, + }, + cookies: { + href: "/cookies", + title: { + en: "Cookie policy", + fr: "Politique de cookies", + nb: "Cookiepolicy", + }, + body: { + en: "Essential-first consent, optional analytics later, and external embeds only after permission.", + fr: "Consentement essential d'abord, analyses optionnelles ensuite, et contenus externes seulement apres permission.", + nb: "Forst essensielle cookies, valgfri analyse senere, og eksterne innbygginger bare etter samtykke.", + }, + }, +}; + +export const cookieBannerCopy = { + eyebrow: { + en: "Privacy desk / local build", + fr: "Cahier vie privee / build local", + nb: "Personverndesk / lokal bygg", + }, + title: { + en: "This paper keeps optional tracking turned off until you say yes.", + fr: "Ce journal laisse le pistage optionnel eteint tant que vous ne dites pas oui.", + nb: "Denne avisen holder valgfri sporing av til du sier ja.", + }, + body: { + en: "Essential cookies keep sign-in, family access, and language choice working. Analytics and future third-party embeds stay off by default.", + fr: "Les cookies essentiels maintiennent la connexion, l'acces familial et le choix de langue. Les analyses et les futurs contenus tiers restent desactives par defaut.", + nb: "Nodvendige cookies holder innlogging, familieadgang og sprakvalg i gang. Analyse og fremtidige tredjepartsinnbygginger er av som standard.", + }, + acceptAll: { + en: "Accept all", + fr: "Tout accepter", + nb: "Godta alt", + }, + essentialOnly: { + en: "Essential only", + fr: "Essentiels seulement", + nb: "Bare nodvendige", + }, + customize: { + en: "Customize", + fr: "Personnaliser", + nb: "Tilpass", + }, + save: { + en: "Save choices", + fr: "Enregistrer les choix", + nb: "Lagre valgene", + }, + categories: { + essential: { + en: "Essential site operations", + fr: "Fonctions essentielles du site", + nb: "Essensiell drift", + }, + analytics: { + en: "Anonymous analytics later", + fr: "Analyses anonymes plus tard", + nb: "Anonym analyse senere", + }, + embeds: { + en: "External media embeds", + fr: "Integrations medias externes", + nb: "Eksterne medieinnbygginger", + }, + }, +}; diff --git a/src/data/profile.ts b/src/data/profile.ts index e0e7a93..545ecd1 100644 --- a/src/data/profile.ts +++ b/src/data/profile.ts @@ -27,6 +27,10 @@ export type VentureSignal = { strap: string; summary: string; href: string; + imageSrc: string; + imageAlt: string; + imageNote: string; + external?: boolean; }; export type CollageImage = { @@ -54,9 +58,10 @@ export const routeStops: RouteStop[] = [ { place: "Columbia, South Carolina", note: "business school orbit" }, { place: "Paris, France", note: "international MBA" }, { place: "Washington, DC", note: "capital interval" }, - { place: "New York", note: "city tempo" }, + { place: "Manhattan, New York", note: "city tempo" }, { place: "Hamilton, Bermuda", note: "Atlantic detour" }, - { place: "the Midwest", note: "American middle distance" }, + { place: "Washington, DC", note: "federal coda" }, + { place: "Brooklyn, New York", note: "borough voltage" }, { place: "Krakow, Poland", note: "EU studies" }, { place: "Oslo, Norway", note: "Nordic transition" }, { place: "Kongsberg, Norway", note: "current desk" }, @@ -113,11 +118,27 @@ export const ventureDesk: Venture[] = [ export const ventureSignals: VentureSignal[] = [ { - name: "Trivia & Tunes", - strap: "live-hosted games and music-led connection", + name: "Blue Note Logic", + strap: "private AI, document intelligence, and source-cited memory", summary: - "A cultural desk for knowledge, playlists, rooms full of people, and the social engineering of a good night.", - href: "https://triviaandtunes.com/", + "The machine room behind the paper: owned infrastructure, private corpora, multilingual controls, and AI that keeps its receipts.", + href: "https://ai.bluenotelogic.com/", + imageSrc: "/images/ai-lab/hero-lab.svg", + imageAlt: "Illustrated AI lab diagram for Blue Note Logic.", + imageNote: "Private corpus / cited answers / EU hosting", + external: true, + }, + { + name: "Trivia & Tunes", + strap: "live-hosted games, music rounds, and true AI in the loop", + summary: + "A culture product with venue instincts: playlists, game craft, AI grading, and voice features edging toward the microphone.", + href: "https://triviaandtunes.no/", + imageSrc: + "https://commons.wikimedia.org/wiki/Special:Redirect/file/Level_42_Kongsberg_Jazzfestival_2017_%28214257%29.jpg", + imageAlt: "Crowd-facing stage image from Kongsberg Jazzfestival.", + imageNote: "Live rooms / music energy / quiz-night voltage", + external: true, }, { name: "Do Better Norge", @@ -125,6 +146,22 @@ export const ventureSignals: VentureSignal[] = [ summary: "A civic and advocacy desk grounded in primary sources, practical guidance, and a refusal to treat children as procedural debris.", href: "https://dobetternorge.no/", + imageSrc: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG", + imageAlt: "Riverfront view in Kongsberg, Norway.", + imageNote: "Norway desk / family life / civic weather", + external: true, + }, + { + name: "School dossier", + strap: "Brussels, Krakow, Paris, Villanova, and the long route north", + summary: + "The education issue turns institutions into chapters, cities into footnotes, and degrees into a proper migration story.", + href: "/education", + imageSrc: + "https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg", + imageAlt: "Grand-Place in Brussels.", + imageNote: "Brussels / multilingual weather / first European chapter", + external: false, }, ]; @@ -218,7 +255,7 @@ export const aiLabSources: SourceCredit[] = [ }, { label: "Trivia & Tunes", - url: "https://triviaandtunes.com/", + url: "https://triviaandtunes.no/", note: "Used for the live-hosted trivia and music angle on the homepage culture desk.", }, @@ -226,6 +263,12 @@ export const aiLabSources: SourceCredit[] = [ label: "Do Better Norge", url: "https://dobetternorge.no/", note: - "Used for the advocacy and children’s-rights positioning on the homepage civic desk.", + "Used for the advocacy and children's-rights positioning on the homepage civic desk.", + }, + { + label: "Wikimedia Commons", + url: "https://commons.wikimedia.org/", + note: + "Used for the public-domain and openly licensed city, campus, and festival images woven through the homepage and section cards.", }, ]; diff --git a/src/data/site.ts b/src/data/site.ts index af91060..312d840 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -21,12 +21,12 @@ export type SchoolDossier = { }; export const hero = { - kicker: "Blue Note Logic presents", - title: "Dave Gilligan, a literary jazz magazine disguised as a personal site.", + kicker: "Pataphysical bulletin / Kongsberg edition", + title: "A high-tech newsmagazine disguised as one man's improbable paper trail.", lede: - "Writing, technology, music, languages, family history, consulting, and a little applied pataphysics, arranged as a bright editorial salon rather than a brochure.", + "Private AI, jazz basements, multilingual weather, family rights, and systems work from Ringwood to Kongsberg, edited with equal parts brass, evidence, and deliberate mischief.", sublede: - "Built for essays, dossiers, dispatches, experiments, job postings, and AI-assisted editions that can move between English, Norwegian, and French without losing their mood.", + "Inside this issue: Blue Note Logic in the machine room, Gilligan Tech in the field, Do Better Norge in the civic file, Trivia & Tunes in the live-wire culture pages, and school dossiers written like contraband literature.", }; export const launchSections: LaunchSection[] = [ @@ -34,7 +34,7 @@ export const launchSections: LaunchSection[] = [ slug: "business", label: "01", title: "Business", - summary: "Consulting notes, operator essays, client stories, and strategy with sleeves rolled up.", + summary: "Consulting notes, AI architecture, and operator essays for people who prefer working systems to rented theater.", tone: "Sharp, practical, anti-buzzword.", strap: "Operator studies for adults who are tired of consultant vapor.", coverline: "The anti-buzzword ledger.", @@ -56,7 +56,7 @@ export const launchSections: LaunchSection[] = [ slug: "family", label: "03", title: "Family", - summary: "A warmer archive for memory, milestones, and the people who keep the music human.", + summary: "A warmer archive for memory, milestones, and the private weather that keeps the machinery worth running.", tone: "Private-minded, generous, alive.", strap: "The soft archive, still edited like it matters.", coverline: "Domestic front pages.", @@ -67,7 +67,7 @@ export const launchSections: LaunchSection[] = [ slug: "fun-postings", label: "04", title: "Fun Postings", - summary: "Odd notices, cultural flyers, side projects, and delightfully unnecessary announcements.", + summary: "Odd notices, cultural flyers, side projects, and the sort of elegant nonsense that deserves proper typesetting.", tone: "Playful, deadpan, collectible.", strap: "The classified page gets strange and starts wearing cologne.", coverline: "Useful nonsense, neatly set.", @@ -78,18 +78,22 @@ export const launchSections: LaunchSection[] = [ slug: "writing", label: "05", title: "Writing", - summary: "Features, columns, notebooks, and dispatches for readers who like style with backbone.", + summary: "This month's writing desk runs on Boris Vian: novels with trapdoors, Vernon Sullivan weather, Saint-Germain smoke, and bibliography arranged like a contraband route.", tone: "Literary, international, smoky.", - strap: "Columns with brass in the lungs and data in the pockets.", - coverline: "Smoke, syntax, and reportage.", - motif: "Essays, dispatches, notebook pages.", - samples: ["On systems and sorrow", "Nordic field notes", "Paris after the spreadsheet"], + strap: "A low-lit file on Boris Vian, jazz syntax, and the exact science of glorious exception.", + coverline: "Boris Vian in the side door.", + motif: "Novels, jazz, pataphysics, counterfeit signatures, and Paris after midnight.", + samples: [ + "The engineer of exceptions", + "Five doors into Boris Vian", + "Why Saint-Germain still leaks into the prose", + ], }, { slug: "jazz-music", label: "06", title: "Jazz and Music", - summary: "Listening notes, deep cuts, rhythm studies, and the low-lit logic of serious groove.", + summary: "Listening notes, Kongsberg nights, Caveau memories, and the low-lit logic of serious groove.", tone: "Velvet, brassy, precise.", strap: "For records, rooms, and players who understand that rhythm is governance.", coverline: "Blue notes and side doors.", @@ -100,7 +104,7 @@ export const launchSections: LaunchSection[] = [ slug: "languages", label: "07", title: "Languages", - summary: "Translation, vocabulary, cross-border humor, and the pleasures of switching registers.", + summary: "English, French, and Norwegian switching places without losing the joke, the seduction, or the filing detail.", tone: "Polyglot, sly, welcoming.", strap: "A section for mistranslation, seduction, and grammatical diplomacy.", coverline: "The multilingual cabinet.", @@ -111,7 +115,7 @@ export const launchSections: LaunchSection[] = [ slug: "ai-lab", label: "08", title: "AI Lab", - summary: "Built-in tools, experiments, prompt systems, and practical machine intelligence with taste.", + summary: "Private corpora, cited answers, multilingual agents, and practical machine intelligence with actual memory.", tone: "Forward-looking, grounded, open source friendly.", strap: "Machine intelligence without the conference lanyard.", coverline: "The atelier for useful futures.", @@ -122,7 +126,7 @@ export const launchSections: LaunchSection[] = [ slug: "norway", label: "09", title: "Norway", - summary: "Kongsberg dispatches, civic notes, local texture, and Scandinavian reality at street level.", + summary: "Kongsberg dispatches, civic reporting, immigrant-family realities, and Norwegian life observed without brochure language.", tone: "Observant, civic, place-aware.", strap: "A local paper for one town and several realities.", coverline: "Kongsberg, correctly observed.", @@ -133,7 +137,7 @@ export const launchSections: LaunchSection[] = [ slug: "projects", label: "10", title: "Projects", - summary: "Things launched, repaired, modernized, or imagined into being across code, content, and data.", + summary: "Things launched, repaired, modernized, or made slightly dangerous across code, content, venues, and data.", tone: "Builder energy, clean receipts.", strap: "The workshop floor, but art directed.", coverline: "Built, fixed, and shipped.", @@ -207,21 +211,21 @@ export const schoolDossiers: SchoolDossier[] = [ ]; export const fieldNotes = [ - "AI-backed edition controls for English, Norwegian, and French.", - "A magazine structure ready for essays, archives, member access, and dossiers.", - "A future-facing front end sitting on top of PHP, SQL, and role-based permissions.", + "Blue Note Logic keeps the machine room full of private AI, cited answers, and document intelligence that behaves like evidence instead of theater.", + "Trivia & Tunes is now a living product story: venue-grade quiz nights, real AI in the game loop, and voice layers warming up for live testing.", + "Do Better Norge keeps the civic file open on family life, immigrant fathers, due process, and the legal weather in contemporary Norway.", ]; export const editorialPromises = [ - "Keep the light background and the page breathable.", - "Make AI visible as a craft tool, not a gimmick.", - "Treat the CV, the essays, and the family archive with equal design seriousness.", + "Keep the paper light, breathable, and a little dangerous around the edges.", + "Make AI visible as a craft tool, a newsroom instrument, and never a plastic gimmick.", + "Treat the CV, the jazz notebook, the civic archive, and the family pages with equal design seriousness.", ]; export const coverLines = [ - "A literary technology salon with jazz smoke in the margins.", - "Five school dossiers, each signed with a clearly counterfeit blessing.", - "AI in the machinery, not sprayed on top like fresh cologne.", + "A pataphysical field paper with jazz smoke in the margins and SQL under the floorboards.", + "Five school dossiers, each signed with a clearly counterfeit blessing and a straight face.", + "AI in the machinery, not sprayed on top like fresh conference cologne.", ]; export function getSectionHref(section: LaunchSection) { diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index c1f52e4..0e068f7 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,7 +1,11 @@ --- import "../styles/global.css"; +import CookieBanner from "../components/CookieBanner.astro"; +import LocaleCopy from "../components/LocaleCopy.astro"; +import LocaleSwitcher from "../components/LocaleSwitcher.astro"; import SectionMark from "../components/SectionMark.astro"; import { getSectionHref, launchSections } from "../data/site"; +import { footerCallouts, getChromeCopy, localizedSections, policyLinkCopy } from "../data/locales"; interface Props { title?: string; @@ -15,47 +19,42 @@ const { lang = "en", } = Astro.props; -const issueDate = new Intl.DateTimeFormat("en-US", { - month: "long", - day: "numeric", - year: "numeric", -}).format(new Date()); +const now = new Date(); +const issueDate = { + en: new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }).format(now), + fr: new Intl.DateTimeFormat("fr-FR", { + day: "numeric", + month: "long", + year: "numeric", + }).format(now), + nb: new Intl.DateTimeFormat("nb-NO", { + day: "numeric", + month: "long", + year: "numeric", + }).format(now), +}; const pathname = Astro.url.pathname.replace(/\/+$/, "") || "/"; const activeSlug = pathname === "/" ? "home" : pathname.split("/").filter(Boolean)[0]; const primarySlugs = ["business", "education", "writing", "jazz-music", "ai-lab", "norway"]; const primaryNav = launchSections.filter((section) => primarySlugs.includes(section.slug)); const footerNav = launchSections.filter((section) => !primarySlugs.includes(section.slug)); -const currentSection = launchSections.find((section) => section.slug === activeSlug); -const articleMeta = pathname.startsWith("/articles/norway") - ? { - label: "Article / Norway desk", - note: "Field report / family life / fathers / immigrants", - } +const articleKey = pathname.startsWith("/articles/norway") + ? "norway" : pathname.startsWith("/articles/kongsberg-jazz-2026") - ? { - label: "Article / Jazz and Music", - note: "Field report / Kongsberg x Paris", - } + ? "jazz" : pathname.startsWith("/articles/trivia-and-tunes-april-2026") - ? { - label: "Article / Projects desk", - note: "Field report / music trivia / Blue Note Rhino / April build", - } - : null; -const issueLabel = currentSection - ? `Section ${currentSection.label} / ${currentSection.title}` - : articleMeta - ? articleMeta.label - : "Founding issue / Personal edition"; -const ribbonNote = currentSection?.tone - ?? (articleMeta - ? articleMeta.note - : "Ringwood / Villanova / Brussels / Paris / Krakow / Oslo / Kongsberg"); + ? "projects" + : null; +const chromeCopy = getChromeCopy({ activeSlug, issueDate, articleKey }); --- - + @@ -74,28 +73,31 @@ const ribbonNote = currentSection?.tone
- Founding issue / {issueDate} - {ribbonNote} + +
- {issueLabel} - Jazz desk / machine room / family archive / pataphysical bulletin + +

davegilligan.com

Dave Gilligan -

Hybrid IT. Private AI. Jazz rooms. Literary weather.

+

+ +

- A personal site edited like a bright retro paper: systems, music, languages, Norway, - civic weather, and useful mischief under one masthead. +
+ + @@ -116,27 +126,63 @@ const ribbonNote = currentSection?.tone + + + + + + diff --git a/src/pages/business.astro b/src/pages/business.astro index 594bf0d..3db7789 100644 --- a/src/pages/business.astro +++ b/src/pages/business.astro @@ -1,136 +1,13 @@ --- -import { launchSections } from "../data/site"; -import { aiLabSources, routeStops, ventureDesk, ventureSignals } from "../data/profile"; +import BusinessIssue from "../components/BusinessIssue.jsx"; import BaseLayout from "../layouts/BaseLayout.astro"; - -const business = launchSections.find((section) => section.slug === "business"); -const [gilliganTech, blueNoteLogic] = ventureDesk; --- -
-
-
- Section {business?.label} / business desk -

The work ledger, but dressed like a front page.

-

- Consulting, product work, AI infrastructure, live cultural formats, and advocacy all - belong here because the operating style is the same: build useful things, keep the - language clean, and make sure the system still stands up once the meeting ends. -

-
- - -
- -
- {routeStops.slice(0, 8).map((stop) => ( - - {stop.place} - {stop.note} - - ))} -
- -
- {[gilliganTech, blueNoteLogic].map((venture) => ( -
-
- {venture.years} - {venture.role} - {venture.location} -
-

{venture.label}

-

{venture.name}

-

{venture.summary}

-

{venture.detail}

- -
    - {venture.highlights.map((item) => ( -
  • {item}
  • - ))} -
- -

- Source: - {venture.source.label} -

-

{venture.source.note}

-
- ))} -
- -
- {ventureSignals.map((signal) => ( - - {signal.strap} - {signal.name} -

{signal.summary}

-
- ))} -
- -
-
-
- Working thesis - Hybrid desk -
-

- Gilligan Tech is the local operating desk: close to clients, close to constraints, and - comfortable with the practical mess of real organizations. Blue Note Logic is the wider - lab: document intelligence, private corpora, AI services, and the harder technical - infrastructure needed to own outcomes instead of renting them. -

-

- Around that core, Trivia & Tunes proves the live-hosted entertainment and room-energy - side of the profile, while Do Better Norge carries the children's-rights and civic - seriousness that keeps the whole publication from turning into mere aesthetics. -

-
- -
-
- Next issue - AI Lab -
-

Read the machine room.

-

- The AI Lab issue turns the Blue Note Logic and Gilligan Tech material into a dedicated - product and infrastructure story, with credited sources and a more technical editorial - voice. -

- Open AI Lab -
-
- -
-
-
Source Notes
-
- Business copy on this page is paraphrased from official venture sites and labeled so the - editorial voice stays distinct from the source material. -
-
- -
- {aiLabSources.slice(0, 2).map((source) => ( - - ))} -
-
+
+
diff --git a/src/pages/cookies.astro b/src/pages/cookies.astro new file mode 100644 index 0000000..0449382 --- /dev/null +++ b/src/pages/cookies.astro @@ -0,0 +1,102 @@ +--- +import LocaleCopy from "../components/LocaleCopy.astro"; +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

+ +

+

+ +

+

+ +

+
+ +
+
+

+

+
+ +
+

+
    +
  • +
  • +
+
+ +
+

+
    +
  • `dg_ui_lang`:
  • +
  • `dg_cookie_preferences`:
  • +
+
+ +
+

+

+
+ +
+

+

+
+
+
+
diff --git a/src/pages/family-lab.astro b/src/pages/family-lab.astro new file mode 100644 index 0000000..846af74 --- /dev/null +++ b/src/pages/family-lab.astro @@ -0,0 +1,124 @@ +--- +import { readFileSync } from "node:fs"; +import path from "node:path"; +import FamilyAtlas from "../components/FamilyAtlas.jsx"; +import { + familyLabFeature, + familyLabFeaturedFilenames, + familyLabFilters, + familyLabNotes, + familyLabStats, +} from "../data/family-lab"; +import BaseLayout from "../layouts/BaseLayout.astro"; + +const manifestPath = path.join(process.cwd(), "public", "images", "family-lab", "facebook-060426", "manifest.json"); +const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); + +const photos = manifest.map((photo) => ({ + filename: photo.filename, + src: photo.src, + title: photo.title, + description: photo.description, + tags: photo.tags ?? [], + namedFile: Boolean(photo.named_file), +})); + +const photosByFilename = new Map(photos.map((photo) => [photo.filename, photo])); +const featuredPhotos = familyLabFeaturedFilenames + .map((filename) => photosByFilename.get(filename)) + .filter(Boolean); +const namedPhotos = photos.filter((photo) => photo.namedFile).slice(0, 12); +--- + + +
+
+
+ {familyLabFeature.eyebrow} +

{familyLabFeature.title}

+

{familyLabFeature.lede}

+ + +
+ + +
+ +
+
+
+ Visual thesis + Light paper / domestic cinema / archival swagger +
+

{familyLabFeature.note}

+
+ +
+ {familyLabStats.map((item) => ( +
+ {item.value} + {item.label} +

{item.note}

+
+ ))} +
+
+ +
+ {familyLabNotes.map((note) => ( +
+

{note.title}

+

{note.body}

+
+ ))} +
+ +
+
+
Named Frames
+
+ The filenames with actual human memory in them get the first clean spread. +
+
+ +
+ {namedPhotos.map((photo) => ( +
+ {photo.title} +
+ {photo.title} + {photo.description} +
+
+ ))} +
+
+ +
+
+
Archive Atlas
+
+ Browse the cleaned test corpus, switch filters, and open any frame into a full-screen reading room. +
+
+ + +
+
+
diff --git a/src/pages/index.astro b/src/pages/index.astro index cacdfe7..588ddf1 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -11,6 +11,7 @@ import { } from "../data/profile"; import { editorialPromises, + fieldNotes, getSectionHref, hero, launchSections, @@ -41,8 +42,8 @@ const projects = launchSections.find((section) => section.slug === "projects");

{hero.sublede}

@@ -85,12 +86,19 @@ const projects = launchSections.find((section) => section.slug === "projects"); ))} +
+ {fieldNotes.map((note) => ( + {note} + ))} +
+
Four Desks, One Signature
- The new front page is organized around the recurring energies of the work: consultancy, - AI infrastructure, live cultural formats, and children's-rights advocacy. + This front page is not a brochure. It is the running file on the real occupations: + private AI, field consulting, jazz-adjacent live products, advocacy, and the long + European afterglow of the schools.
@@ -121,10 +129,17 @@ const projects = launchSections.find((section) => section.slug === "projects"); @@ -139,13 +154,13 @@ const projects = launchSections.find((section) => section.slug === "projects");

Special report / useful futures

Private AI, cited answers, and machine intelligence with a memory.

- The AI Lab issue is the technical heart of the site: private corpora, document - intelligence, multilingual edition controls, and deployment paths that stay close to the - evidence instead of drifting into vendor theatre. + The AI Lab issue is where the machinery stops posing and starts working: private corpora, + document intelligence, multilingual controls, and deployment paths that stay close to the + evidence instead of wandering off into vendor theatre.

@@ -174,8 +189,9 @@ const projects = launchSections.find((section) => section.slug === "projects");
Elsewhere In The Paper
- These sections stay distinct in tone, but the site now treats them like desks inside one - publication rather than equally weighted navigation blocks. + Every desk has its own rhythm, but the page keeps them under one masthead: less menu, + more newsroom, with clippings, notebooks, civic files, and machine-room traces in plain + sight.
diff --git a/src/pages/privacy.astro b/src/pages/privacy.astro new file mode 100644 index 0000000..3144cd9 --- /dev/null +++ b/src/pages/privacy.astro @@ -0,0 +1,100 @@ +--- +import LocaleCopy from "../components/LocaleCopy.astro"; +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+
+

+ +

+

+ +

+

+ +

+
+ +
+
+

+

+
+ +
+

+
    +
  • +
  • +
  • +
+
+ +
+

+

+
+ +
+

+

+
+ +
+

+

+
+
+
+
diff --git a/src/pages/writing.astro b/src/pages/writing.astro new file mode 100644 index 0000000..5affc4e --- /dev/null +++ b/src/pages/writing.astro @@ -0,0 +1,13 @@ +--- +import WritingIssue from "../components/WritingIssue.jsx"; +import BaseLayout from "../layouts/BaseLayout.astro"; +--- + + +
+ +
+
diff --git a/src/styles/global.css b/src/styles/global.css index 59d1963..b735332 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -749,6 +749,41 @@ img { line-height: 1; } +.section-card__art { + margin: 0 0 0.95rem; + display: grid; + gap: 0.55rem; +} + +.section-card__art img { + width: 100%; + aspect-ratio: 1.55; + object-fit: cover; + border-radius: 1rem; + border: 1px solid rgba(24, 21, 17, 0.08); +} + +.section-card__art figcaption { + display: grid; + gap: 0.18rem; +} + +.section-card__art span { + font-family: var(--font-mono); + font-size: 0.62rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--burgundy); +} + +.section-card__art strong { + font-family: var(--font-body); + font-size: 0.98rem; + line-height: 1.45; + font-weight: 500; + color: var(--ink-soft); +} + .section-card p { margin: 0.7rem 0 0; color: var(--ink-soft); @@ -1306,13 +1341,23 @@ img { .signal-deck__card, .signal-strip__card { display: grid; - gap: 0.45rem; - padding: 1rem 1.05rem; + gap: 0.65rem; + padding: 0.9rem; border-top: 2px solid var(--ink); background: rgba(255, 252, 246, 0.78); text-decoration: none; } +.signal-deck__card img, +.signal-strip__card img { + width: 100%; + aspect-ratio: 1.5; + object-fit: cover; + border-radius: 1rem; + border: 1px solid rgba(24, 21, 17, 0.1); + background: rgba(255, 255, 255, 0.72); +} + .signal-deck__card span, .signal-strip__card span { font-family: var(--font-mono); @@ -1337,6 +1382,15 @@ img { line-height: 1.55; } +.signal-deck__card small, +.signal-strip__card small { + font-family: var(--font-mono); + font-size: 0.62rem; + letter-spacing: 0.11em; + text-transform: uppercase; + color: var(--ink-faint); +} + .ai-observatory { display: grid; grid-template-columns: minmax(0, 1.02fr) minmax(280px, 0.98fr); @@ -3324,6 +3378,515 @@ img { padding-bottom: 3rem; } +.business-page { + position: relative; + padding-bottom: 3rem; +} + +.business-page::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + background: + radial-gradient(circle at 14% 18%, rgba(109, 41, 8, 0.14), transparent 28%), + radial-gradient(circle at 83% 17%, rgba(19, 56, 66, 0.12), transparent 26%), + linear-gradient(180deg, rgba(246, 239, 221, 0.97), rgba(243, 235, 219, 0.92)); +} + +.business-live { + display: grid; + gap: 1.4rem; +} + +.business-live__loading { + margin-top: 1.2rem; + padding: 1.2rem; +} + +.business-live__loading h2 { + margin: 0.2rem 0 0; + font-family: var(--font-display); +} + +.business-live__loading p { + margin: 0.8rem 0 0; + color: var(--ink-soft); +} + +.business-live__hero { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(330px, 0.95fr); + gap: 1rem; + align-items: start; +} + +.business-live__copy h1 { + margin: 0; + max-width: 7ch; + font-family: var(--font-display); + font-size: clamp(3.1rem, 8vw, 6rem); + line-height: 0.88; + letter-spacing: -0.05em; + font-weight: 400; +} + +.business-live__lede { + display: grid; + gap: 0.85rem; + margin-top: 1rem; + max-width: 43rem; + font-size: 1.04rem; + color: var(--ink-soft); +} + +.business-live__lede p { + margin: 0; +} + +.business-live__signals { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; + margin-top: 1.25rem; +} + +.business-live__signal, +.business-live__card { + padding: 1rem 1.05rem; +} + +.business-live__signal span, +.business-live__card .capsule__kicker span:first-child { + display: block; + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.11em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.business-live__signal strong { + display: block; + margin-top: 0.4rem; + font-family: var(--font-display); + font-size: 1.45rem; + line-height: 0.98; +} + +.business-live__signal p { + margin: 0.65rem 0 0; + color: var(--ink-soft); +} + +.business-live__cover { + overflow: hidden; + padding: 0; +} + +.business-live__cover img { + width: 100%; + height: min(36rem, 60vw); + object-fit: cover; + display: block; + filter: sepia(0.08) contrast(1.04) saturate(0.95); +} + +.business-live__cover-copy { + padding: 1rem 1.1rem 1.15rem; +} + +.business-live__cover-copy h2 { + margin: 0.25rem 0 0; + font-family: var(--font-display); + font-size: clamp(2rem, 3vw, 2.8rem); + line-height: 0.94; +} + +.business-live__cover-copy p { + margin: 0.85rem 0 0; + color: var(--ink-soft); +} + +.business-live__credit { + font-size: 0.78rem; +} + +.business-live__columns { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(330px, 0.92fr); + gap: 1rem; + align-items: start; +} + +.business-live__feature { + padding: 1.15rem; +} + +.business-live__side { + display: grid; + gap: 1rem; +} + +.business-live__rich { + display: grid; + gap: 0.9rem; + margin-top: 0.9rem; + color: var(--ink-soft); +} + +.business-live__rich h2, +.business-live__rich h3 { + margin: 0.4rem 0 0; + color: var(--ink); + font-family: var(--font-display); + line-height: 0.96; +} + +.business-live__rich h2 { + font-size: clamp(2rem, 3vw, 2.9rem); +} + +.business-live__rich h3 { + font-size: 1.5rem; +} + +.business-live__rich p, +.business-live__rich ul, +.business-live__rich ol, +.business-live__rich blockquote { + margin: 0; +} + +.business-live__rich ul, +.business-live__rich ol { + padding-left: 1.3rem; + display: grid; + gap: 0.7rem; +} + +.business-live__rich li::marker { + color: var(--rust); +} + +.business-live__rich a { + color: var(--teal); + text-decoration: none; +} + +.business-live__rich a:hover { + text-decoration: underline; +} + +.business-live__rich img { + width: 100%; + border-radius: 0.85rem; + border: 1px solid rgba(26, 23, 20, 0.12); + background: rgba(255, 255, 255, 0.72); +} + +.business-live__rich blockquote { + padding: 1rem 1.05rem; + border-left: 3px solid var(--rust); + background: rgba(255, 255, 255, 0.62); + font-family: var(--font-display); + color: var(--ink); +} + +.business-live__rich--compact h2 { + font-size: 1.72rem; +} + +.business-live__article-list { + list-style: none; + margin: 0.9rem 0 0; + padding: 0; + display: grid; + gap: 0.9rem; +} + +.business-live__article-list li { + padding-top: 0.9rem; + border-top: 1px solid var(--line); +} + +.business-live__article-list li:first-child { + padding-top: 0; + border-top: none; +} + +.business-live__article-list strong { + display: block; + font-family: var(--font-display); + font-size: 1.42rem; + line-height: 0.98; + color: var(--ink); +} + +.business-live__article-list p, +.business-live__card p { + margin: 0.55rem 0 0; + color: var(--ink-soft); +} + +.business-live__meta { + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.writing-page { + position: relative; + padding-bottom: 3rem; +} + +.writing-page::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + background: + radial-gradient(circle at 18% 16%, rgba(148, 87, 31, 0.12), transparent 30%), + radial-gradient(circle at 82% 20%, rgba(33, 71, 98, 0.11), transparent 28%), + linear-gradient(180deg, rgba(246, 239, 221, 0.96), rgba(244, 236, 221, 0.92)); +} + +.writing-live { + display: grid; + gap: 1.4rem; +} + +.writing-live__loading { + margin-top: 1.2rem; + padding: 1.2rem; +} + +.writing-live__loading h2 { + margin: 0.2rem 0 0; + font-family: var(--font-display); +} + +.writing-live__loading p { + margin: 0.8rem 0 0; + color: var(--ink-soft); +} + +.writing-live__hero { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); + gap: 1rem; + align-items: start; +} + +.writing-live__copy h1 { + margin: 0; + max-width: 7ch; +} + +.writing-live__lede { + display: grid; + gap: 0.85rem; + margin-top: 1rem; + max-width: 42rem; + font-size: 1.02rem; + color: var(--ink-soft); +} + +.writing-live__lede p { + margin: 0; +} + +.writing-live__notes { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; + margin-top: 1.25rem; +} + +.writing-live__signal { + padding: 1rem 1.05rem; +} + +.writing-live__signal span, +.writing-live__card .capsule__kicker span:first-child { + display: block; + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.11em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.writing-live__signal strong { + display: block; + margin-top: 0.45rem; + font-family: var(--font-display); + font-size: 1.45rem; +} + +.writing-live__signal p { + margin: 0.65rem 0 0; + color: var(--ink-soft); +} + +.writing-live__cover { + overflow: hidden; + padding: 0; +} + +.writing-live__cover img { + width: 100%; + height: min(34rem, 58vw); + object-fit: cover; + display: block; + filter: saturate(0.94) contrast(1.05); +} + +.writing-live__cover-copy { + padding: 1rem 1.1rem 1.15rem; +} + +.writing-live__cover-copy h2 { + margin: 0.25rem 0 0; + font-family: var(--font-display); + font-size: clamp(2rem, 3vw, 2.6rem); + line-height: 0.94; +} + +.writing-live__cover-copy p { + margin: 0.85rem 0 0; + color: var(--ink-soft); +} + +.writing-live__credit { + font-size: 0.78rem; +} + +.writing-live__columns { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); + gap: 1rem; + align-items: start; +} + +.writing-live__body, +.writing-live__card { + padding: 1.15rem; +} + +.writing-live__side { + display: grid; + gap: 1rem; +} + +.writing-live__rich { + display: grid; + gap: 0.9rem; + margin-top: 0.9rem; + color: var(--ink-soft); +} + +.writing-live__rich h2, +.writing-live__rich h3 { + margin: 0.4rem 0 0; + color: var(--ink); + font-family: var(--font-display); + line-height: 0.96; +} + +.writing-live__rich h2 { + font-size: clamp(2rem, 3vw, 2.9rem); +} + +.writing-live__rich h3 { + font-size: 1.5rem; +} + +.writing-live__rich p, +.writing-live__rich ul, +.writing-live__rich ol, +.writing-live__rich blockquote { + margin: 0; +} + +.writing-live__rich ul, +.writing-live__rich ol { + padding-left: 1.3rem; + display: grid; + gap: 0.7rem; +} + +.writing-live__rich li::marker { + color: var(--rust); +} + +.writing-live__rich a { + color: var(--teal); + text-decoration: none; +} + +.writing-live__rich a:hover { + text-decoration: underline; +} + +.writing-live__rich img { + width: 100%; + border-radius: 0.85rem; + border: 1px solid rgba(26, 23, 20, 0.12); + background: rgba(255, 255, 255, 0.72); +} + +.writing-live__rich blockquote { + padding: 1rem 1.05rem; + border-left: 3px solid var(--rust); + background: rgba(255, 255, 255, 0.6); + font-family: var(--font-display); + color: var(--ink); +} + +.writing-live__rich--compact h2 { + font-size: 1.75rem; +} + +.writing-live__article-list { + list-style: none; + margin: 0.9rem 0 0; + padding: 0; + display: grid; + gap: 0.9rem; +} + +.writing-live__article-list li { + padding-top: 0.9rem; + border-top: 1px solid var(--line); +} + +.writing-live__article-list li:first-child { + padding-top: 0; + border-top: none; +} + +.writing-live__article-list strong { + display: block; + font-family: var(--font-display); + font-size: 1.45rem; + color: var(--ink); +} + +.writing-live__article-list p, +.writing-live__card p { + margin: 0.55rem 0 0; + color: var(--ink-soft); +} + +.writing-live__meta { + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .projects-hero { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr); @@ -3517,3 +4080,1034 @@ img { font-size: clamp(3rem, 15vw, 5rem); } } + +@media (max-width: 1040px) { + .writing-live__hero, + .writing-live__columns { + grid-template-columns: 1fr; + } +} + +@media (max-width: 760px) { + .writing-live__copy h1 { + max-width: none; + font-size: clamp(2.9rem, 15vw, 4.8rem); + } + + .writing-live__notes { + grid-template-columns: 1fr; + } + + .writing-live__cover img { + height: min(25rem, 86vw); + } +} + +.family-lab-page { + display: grid; + gap: 3.8rem; + padding: 2.4rem 0 5.2rem; +} + +.family-lab-hero { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + gap: 2.2rem; + align-items: center; +} + +.family-lab-hero__copy { + position: relative; + z-index: 1; +} + +.family-lab-hero__copy h1 { + margin: 0; + max-width: 13ch; + font-family: var(--font-display); + font-size: clamp(3.2rem, 8vw, 6.8rem); + line-height: 0.88; + letter-spacing: -0.05em; +} + +.family-lab-hero__lede { + margin: 1.25rem 0 0; + max-width: 34rem; + font-size: 1.15rem; + line-height: 1.7; + color: var(--ink-soft); +} + +.family-lab-hero__actions { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + margin-top: 1.8rem; +} + +.family-lab-hero__montage { + position: relative; + min-height: 42rem; + padding: 1rem 0; +} + +.family-lab-hero__montage::before { + content: ""; + position: absolute; + inset: 2rem 1.5rem 2.5rem 3rem; + border-radius: 2.4rem; + background: + radial-gradient(circle at top right, rgba(115, 49, 42, 0.15), transparent 26%), + radial-gradient(circle at bottom left, rgba(31, 93, 95, 0.16), transparent 30%), + linear-gradient(180deg, rgba(255, 252, 245, 0.85), rgba(249, 240, 225, 0.78)); + border: 1px solid rgba(24, 21, 17, 0.08); + box-shadow: 0 30px 80px rgba(37, 26, 13, 0.12); +} + +.family-lab-hero__card { + position: absolute; + width: min(18rem, 46%); + margin: 0; + padding: 0.85rem 0.85rem 1.1rem; + border-radius: 1.7rem; + background: rgba(255, 252, 246, 0.92); + border: 1px solid rgba(24, 21, 17, 0.12); + box-shadow: 0 22px 55px rgba(33, 23, 10, 0.14); + transform-origin: center; + animation: family-card-float 9s ease-in-out infinite; +} + +.family-lab-hero__card img { + width: 100%; + aspect-ratio: 4 / 5; + object-fit: cover; + border-radius: 1.1rem; +} + +.family-lab-hero__card figcaption { + display: grid; + gap: 0.35rem; + padding-top: 0.8rem; +} + +.family-lab-hero__card span, +.family-lab-hero__card small { + display: block; +} + +.family-lab-hero__card span { + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--burgundy); +} + +.family-lab-hero__card small { + color: var(--ink-soft); + font-size: 0.98rem; + line-height: 1.45; +} + +.family-lab-hero__card--1 { + top: 0; + left: 2rem; + --family-rotate: -5deg; + transform: rotate(var(--family-rotate)); +} + +.family-lab-hero__card--2 { + top: 4.8rem; + right: 0; + --family-rotate: 4.5deg; + transform: rotate(var(--family-rotate)); + animation-delay: -2s; +} + +.family-lab-hero__card--3 { + left: 0; + bottom: 3.2rem; + --family-rotate: -2.8deg; + transform: rotate(var(--family-rotate)); + animation-delay: -4s; +} + +.family-lab-hero__card--4 { + right: 3rem; + bottom: 0; + --family-rotate: 6deg; + transform: rotate(var(--family-rotate)); + animation-delay: -6s; +} + +.family-lab-manifesto { + display: grid; + gap: 1.35rem; +} + +.family-lab-manifesto__note { + padding: 1.5rem 1.6rem; +} + +.family-lab-manifesto__note p { + margin: 0; + font-size: 1.06rem; + line-height: 1.65; + color: var(--ink-soft); +} + +.family-lab-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.family-lab-stats article { + padding: 1.25rem 1.1rem 1.35rem; + border-top: 1px solid var(--line-strong); + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, rgba(255, 251, 243, 0.55), rgba(255, 251, 243, 0.1)); +} + +.family-lab-stats strong, +.family-lab-stats span { + display: block; +} + +.family-lab-stats strong { + font-family: var(--font-display); + font-size: clamp(2.5rem, 5vw, 4.2rem); + line-height: 0.95; +} + +.family-lab-stats span { + margin-top: 0.25rem; + font-family: var(--font-mono); + font-size: 0.76rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--teal); +} + +.family-lab-stats p { + margin: 0.85rem 0 0; + color: var(--ink-soft); + line-height: 1.55; +} + +.family-lab-notes { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.family-lab-notes__item { + padding: 1rem 0 0; + border-top: 1px solid var(--line); +} + +.family-lab-notes__item h2 { + margin: 0; + max-width: 10ch; + font-family: var(--font-display); + font-size: clamp(1.9rem, 4vw, 3rem); + line-height: 0.95; +} + +.family-lab-notes__item p { + margin: 0.9rem 0 0; + color: var(--ink-soft); + line-height: 1.62; +} + +.family-lab-strip { + display: grid; + gap: 1.4rem; +} + +.family-lab-strip__rail { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 1rem; +} + +.family-lab-strip__frame { + grid-column: span 3; + margin: 0; +} + +.family-lab-strip__frame:nth-child(5n + 1), +.family-lab-strip__frame:nth-child(5n + 4) { + grid-column: span 4; +} + +.family-lab-strip__frame img { + width: 100%; + aspect-ratio: 4 / 5; + object-fit: cover; + border-radius: 1.6rem; + border: 1px solid rgba(24, 21, 17, 0.08); + box-shadow: 0 18px 40px rgba(35, 25, 14, 0.08); +} + +.family-lab-strip__frame figcaption { + display: grid; + gap: 0.2rem; + padding-top: 0.7rem; +} + +.family-lab-strip__frame strong { + font-size: 1rem; +} + +.family-lab-strip__frame span { + color: var(--ink-soft); + line-height: 1.45; +} + +.family-lab-atlas-shell { + display: grid; + gap: 1.4rem; +} + +.family-atlas { + display: grid; + gap: 1.4rem; +} + +.family-atlas__toolbar { + display: grid; + gap: 0.8rem; +} + +.family-atlas__topline { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 0.7rem; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.family-atlas__filters { + display: flex; + flex-wrap: wrap; + gap: 0.7rem; +} + +.family-atlas__filters button { + border: 1px solid var(--line); + border-radius: 999px; + background: rgba(255, 252, 245, 0.72); + color: var(--ink); + padding: 0.7rem 1rem; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + cursor: pointer; + transition: + transform 180ms ease, + background 180ms ease, + border-color 180ms ease; +} + +.family-atlas__filters button:hover, +.family-atlas__filters button:focus-visible, +.family-atlas__filters button.is-active { + transform: translateY(-2px); + background: rgba(255, 253, 249, 0.96); + border-color: rgba(24, 21, 17, 0.28); +} + +.family-atlas__layout { + display: grid; + grid-template-columns: minmax(0, 20rem) minmax(0, 1fr); + gap: 1.25rem; + align-items: start; +} + +.family-atlas__inspector { + position: sticky; + top: 5.8rem; + display: grid; + gap: 1rem; + padding: 1rem; + border-radius: 1.8rem; + border: 1px solid rgba(24, 21, 17, 0.12); + background: + linear-gradient(180deg, rgba(255, 253, 247, 0.95), rgba(248, 240, 227, 0.9)); + box-shadow: 0 20px 50px rgba(36, 26, 12, 0.08); +} + +.family-atlas__inspector-image img { + width: 100%; + aspect-ratio: 4 / 5; + object-fit: cover; + border-radius: 1.2rem; +} + +.family-atlas__inspector-copy h3, +.family-lightbox__caption h3 { + margin: 0; + font-family: var(--font-display); + font-size: clamp(2rem, 4vw, 3.2rem); + line-height: 0.95; +} + +.family-atlas__inspector-copy p, +.family-lightbox__caption p { + margin: 0.8rem 0 0; + color: var(--ink-soft); + line-height: 1.6; +} + +.family-atlas__inspector-kicker { + margin: 0; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--burgundy); +} + +.family-atlas__tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.95rem; +} + +.family-atlas__tags span { + padding: 0.35rem 0.7rem; + border-radius: 999px; + background: rgba(24, 21, 17, 0.06); + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.family-atlas__featured { + display: grid; + gap: 0.75rem; + padding-top: 0.2rem; + border-top: 1px solid var(--line); +} + +.family-atlas__featured-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.6rem; +} + +.family-atlas__featured-grid button { + display: grid; + gap: 0.45rem; + padding: 0; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; +} + +.family-atlas__featured-grid img { + width: 100%; + aspect-ratio: 1; + object-fit: cover; + border-radius: 1rem; +} + +.family-atlas__featured-grid span { + font-size: 0.9rem; +} + +.family-atlas__masonry { + columns: 3 15rem; + column-gap: 1rem; +} + +.family-atlas__tile { + position: relative; + display: block; + width: 100%; + margin: 0 0 1rem; + padding: 0; + break-inside: avoid; + border: 0; + background: transparent; + cursor: pointer; +} + +.family-atlas__tile img { + width: 100%; + border-radius: 1.55rem; + box-shadow: 0 16px 36px rgba(36, 25, 11, 0.1); + transition: + transform 220ms ease, + box-shadow 220ms ease, + filter 220ms ease; +} + +.family-atlas__tile:hover img, +.family-atlas__tile:focus-visible img { + transform: translateY(-4px) scale(1.01); + box-shadow: 0 24px 46px rgba(36, 25, 11, 0.16); + filter: saturate(1.03); +} + +.family-atlas__tile--wide img { + aspect-ratio: 5 / 3; + object-fit: cover; +} + +.family-atlas__tile--square img { + aspect-ratio: 1; + object-fit: cover; +} + +.family-atlas__tile--tall img { + aspect-ratio: 4 / 5; + object-fit: cover; +} + +.family-atlas__tile-meta { + position: absolute; + left: 1rem; + right: 1rem; + bottom: 1rem; + display: grid; + gap: 0.15rem; + padding: 0.9rem 1rem; + border-radius: 1.15rem; + background: linear-gradient(180deg, rgba(18, 15, 12, 0.08), rgba(18, 15, 12, 0.72)); + color: #fff8ef; + text-align: left; +} + +.family-atlas__tile-meta strong, +.family-atlas__tile-meta small { + display: block; +} + +.family-atlas__tile-meta strong { + font-size: 1rem; +} + +.family-atlas__tile-meta small { + font-family: var(--font-mono); + font-size: 0.64rem; + letter-spacing: 0.14em; + text-transform: uppercase; + opacity: 0.86; +} + +.family-lightbox { + position: fixed; + inset: 0; + z-index: 30; + display: grid; + place-items: center; + padding: 1.5rem; + background: rgba(22, 17, 14, 0.82); + backdrop-filter: blur(16px); +} + +.family-lightbox__sheet { + width: min(1120px, 100%); + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(18rem, 23rem); + gap: 1rem; + padding: 1.1rem; + border-radius: 2rem; + background: linear-gradient(180deg, rgba(252, 248, 239, 0.97), rgba(243, 235, 220, 0.95)); + box-shadow: 0 30px 90px rgba(0, 0, 0, 0.28); +} + +.family-lightbox__close, +.family-lightbox__nav { + border: 0; + border-radius: 999px; + background: rgba(24, 21, 17, 0.1); + color: var(--ink); + padding: 0.75rem 1rem; + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + cursor: pointer; +} + +.family-lightbox__close { + grid-column: 1 / -1; + justify-self: end; +} + +.family-lightbox__frame { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.8rem; + align-items: center; +} + +.family-lightbox__frame img { + width: 100%; + max-height: 76vh; + object-fit: contain; + border-radius: 1.35rem; + background: rgba(255, 255, 255, 0.55); +} + +.family-lightbox__caption { + align-self: stretch; + padding: 0.9rem 0.7rem 0.7rem 0.4rem; +} + +@keyframes family-card-float { + 0%, + 100% { + transform: translateY(0) rotate(var(--family-rotate, 0deg)); + } + 50% { + transform: translateY(-10px) rotate(calc(var(--family-rotate, 0deg) + 0.8deg)); + } +} + +@media (max-width: 1040px) { + .family-lab-hero, + .family-atlas__layout, + .family-lightbox__sheet { + grid-template-columns: 1fr; + } + + .family-lab-hero__montage { + min-height: 36rem; + } + + .family-atlas__inspector { + position: static; + } + + .family-lightbox__caption { + padding: 0 0.2rem 0.4rem; + } +} + +@media (max-width: 1040px) { + .business-live__hero, + .business-live__columns { + grid-template-columns: 1fr; + } +} + +@media (max-width: 760px) { + .business-live__copy h1 { + max-width: none; + font-size: clamp(2.9rem, 15vw, 4.8rem); + } + + .business-live__signals { + grid-template-columns: 1fr; + } + + .business-live__cover img { + height: min(25rem, 86vw); + } +} + +@media (max-width: 840px) { + .family-lab-stats, + .family-lab-notes { + grid-template-columns: 1fr; + } + + .family-lab-strip__rail { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .family-lab-strip__frame, + .family-lab-strip__frame:nth-child(5n + 1), + .family-lab-strip__frame:nth-child(5n + 4) { + grid-column: span 1; + } +} + +@media (max-width: 720px) { + .family-lab-page { + gap: 3rem; + padding-bottom: 4rem; + } + + .family-lab-hero__copy h1 { + max-width: none; + } + + .family-lab-hero__montage { + min-height: 32rem; + padding-top: 0; + } + + .family-lab-hero__montage::before { + inset: 1.5rem 0 2rem; + } + + .family-lab-hero__card { + width: min(15.6rem, 62%); + } + + .family-lab-hero__card--1 { + left: 0.7rem; + } + + .family-lab-hero__card--2 { + top: 3rem; + } + + .family-lab-hero__card--3 { + bottom: 2rem; + } + + .family-lab-hero__card--4 { + right: 0.8rem; + } + + .family-atlas__masonry { + columns: 2 10rem; + } + + .family-lightbox { + padding: 0.8rem; + } + + .family-lightbox__sheet { + padding: 0.85rem; + border-radius: 1.4rem; + } + + .family-lightbox__frame { + grid-template-columns: 1fr; + } + + .family-lightbox__nav--prev { + order: 2; + } + + .family-lightbox__nav--next { + order: 3; + } +} + +@media (max-width: 540px) { + .family-lab-hero__montage { + min-height: 28rem; + } + + .family-lab-hero__card { + width: min(13rem, 66%); + padding: 0.7rem 0.7rem 0.95rem; + border-radius: 1.25rem; + } + + .family-lab-hero__card figcaption { + padding-top: 0.6rem; + } + + .family-atlas__filters button { + padding-inline: 0.85rem; + } + + .family-atlas__masonry { + columns: 1; + } + + .family-atlas__tile-meta { + left: 0.7rem; + right: 0.7rem; + bottom: 0.7rem; + } +} + +.locale-copy__text { + display: none; +} + +html[data-ui-lang="en"] .locale-copy__text[data-locale-option="en"], +html[data-ui-lang="fr"] .locale-copy__text[data-locale-option="fr"], +html[data-ui-lang="nb"] .locale-copy__text[data-locale-option="nb"] { + display: inline; +} + +.locale-switcher { + display: grid; + gap: 0.7rem; + padding: 1rem 0 0.2rem; +} + +.locale-switcher__buttons { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.locale-switcher__button { + appearance: none; + border: 1px solid var(--line); + background: rgba(255, 252, 245, 0.76); + color: var(--ink); + border-radius: 999px; + padding: 0.58rem 0.92rem; + font: inherit; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; + transition: + transform 180ms ease, + background 180ms ease, + border-color 180ms ease, + color 180ms ease; +} + +.locale-switcher__button:hover, +.locale-switcher__button:focus-visible, +.locale-switcher__button.is-active { + transform: translateY(-1px); + border-color: rgba(24, 21, 17, 0.3); + background: rgba(255, 255, 255, 0.96); + color: var(--burgundy); +} + +.locale-switcher__note { + max-width: 52rem; + color: var(--ink-soft); + font-size: 0.96rem; + line-height: 1.5; +} + +.site-footer__callouts, +.site-footer__policies { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + padding-top: 1.25rem; +} + +.footer-callout, +.policy-link { + display: grid; + gap: 0.45rem; + padding: 1.1rem 1.15rem 1.2rem; + border: 1px solid var(--line); + border-radius: 1.5rem; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(255, 252, 245, 0.88)), + rgba(255, 252, 245, 0.78); + box-shadow: 0 16px 42px rgba(40, 28, 12, 0.06); + text-decoration: none; +} + +.footer-callout strong, +.policy-link strong { + font-family: var(--font-display); + font-size: 1.5rem; + line-height: 1; + font-weight: 400; +} + +.footer-callout span, +.policy-link span { + color: var(--ink-soft); + font-size: 1rem; + line-height: 1.55; +} + +.footer-callout__label { + margin: 0; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--teal); +} + +.policy-page { + padding: 2.5rem 0 5rem; +} + +.policy-hero { + display: grid; + gap: 1rem; + padding-bottom: 2rem; +} + +.policy-hero h1 { + margin: 0; + max-width: 14ch; + font-family: var(--font-display); + font-size: clamp(3.6rem, 9vw, 6.5rem); + line-height: 0.94; + font-weight: 400; + letter-spacing: -0.045em; +} + +.policy-hero__lede { + margin: 0; + max-width: 48rem; + color: var(--ink-soft); + font-size: 1.15rem; + line-height: 1.65; +} + +.policy-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.policy-block { + padding: 1.4rem 1.35rem 1.5rem; + border: 1px solid var(--line); + border-radius: 1.6rem; + background: rgba(255, 252, 245, 0.82); + box-shadow: 0 16px 42px rgba(40, 28, 12, 0.06); +} + +.policy-block--full { + grid-column: 1 / -1; +} + +.policy-block h2 { + margin: 0 0 0.7rem; + font-family: var(--font-display); + font-size: 2rem; + line-height: 1; + font-weight: 400; +} + +.policy-block p, +.policy-block li { + color: var(--ink-soft); + font-size: 1.02rem; + line-height: 1.65; +} + +.cookie-banner { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 30; + width: min(34rem, calc(100% - 2rem)); +} + +.cookie-banner__panel { + display: grid; + gap: 1rem; + padding: 1.15rem; + border: 1px solid rgba(24, 21, 17, 0.18); + border-radius: 1.7rem; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(252, 248, 239, 0.96)), + rgba(255, 252, 245, 0.96); + box-shadow: 0 24px 70px rgba(40, 28, 12, 0.16); + backdrop-filter: blur(20px); +} + +.cookie-banner__eyebrow { + margin: 0; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--burgundy); +} + +.cookie-banner h2 { + margin: 0.1rem 0 0.45rem; + font-family: var(--font-display); + font-size: 2rem; + line-height: 1; + font-weight: 400; +} + +.cookie-banner__body { + margin: 0; + color: var(--ink-soft); + line-height: 1.6; +} + +.cookie-banner__actions { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.button--small { + padding: 0.75rem 1rem; +} + +.button--ghost { + border: 1px solid var(--line); + background: transparent; + color: var(--ink); +} + +.cookie-banner__details { + display: grid; + gap: 0.75rem; + padding-top: 0.15rem; +} + +.cookie-toggle { + display: flex; + align-items: flex-start; + gap: 0.8rem; + padding: 0.9rem 0.95rem; + border: 1px solid var(--line); + border-radius: 1.2rem; + background: rgba(255, 255, 255, 0.7); +} + +.cookie-toggle input { + margin-top: 0.25rem; +} + +.cookie-toggle span { + display: grid; + gap: 0.2rem; +} + +.cookie-toggle strong { + font-size: 1rem; + font-weight: 600; +} + +.cookie-toggle em { + color: var(--ink-faint); + font-size: 0.92rem; + font-style: normal; +} + +@media (max-width: 920px) { + .site-footer__callouts, + .site-footer__policies, + .policy-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .locale-switcher { + gap: 0.85rem; + } + + .cookie-banner { + right: 0.75rem; + bottom: 0.75rem; + width: calc(100% - 1.5rem); + } +}