33< head >
44< meta charset ="utf-8 ">
55< meta name ="viewport " content ="width=device-width, initial-scale=1 ">
6- < title > Flavormancer</ title >
6+ < title > Flavormancer — taste & aroma from chemical structure</ title >
7+ < meta name ="description " content ="Flavormancer predicts taste & aroma from a molecule's chemical structure — on-prem flavor prediction for food, beverage, and fragrance R&D. ">
8+ < meta property ="og:type " content ="website ">
9+ < meta property ="og:site_name " content ="Flavormancer ">
10+ < meta property ="og:title " content ="Flavormancer — taste & aroma from chemical structure ">
11+ < meta property ="og:description " content ="Predict taste & aroma from chemical structure. On-prem, before you pour. ">
12+ < meta property ="og:image " content ="/static/social.png ">
13+ < meta name ="twitter:card " content ="summary_large_image ">
14+ < meta name ="twitter:title " content ="Flavormancer — taste & aroma from chemical structure ">
15+ < meta name ="twitter:description " content ="Predict taste & aroma from chemical structure. On-prem, before you pour. ">
16+ < meta name ="twitter:image " content ="/static/social.png ">
717< style >
18+ @font-face {font-family : 'Cinzel Decorative' ;src : url ('/static/wordmark.ttf' ) format ('truetype' );font-weight : 700 ;font-display : swap}
819 : root {
9- --surface : # F4F6F4 ; --panel : # FFFFFF ; --line : # DCE3DE ; --ink : # 172019 ;
10- --muted : # 5C6B63 ; --accent : # 2A7C86 ; --accent-soft : # 2A7C8614 ;
11- --brand-1 : # 3A2E7C ; --brand-2 : # 1AA0A8 ; --brand-accent : # E0913C ;
12- --brand-grad : linear-gradient (100deg , # 3A2E7C 0% , # 2A5E9E 45% , # 1AA0A8 100% );
13- --sweet : # E0972E ; --bitter : # 6B4E9E ; --umami : # C0563B ; --sour : # 9FB024 ; --salty : # 4E84B0 ; --aroma : # 2E7D6B ;
20+ --surface : # 0F1319 ; --panel : # 181D24 ; --line : # 2A323C ; --ink : # E7EDEA ;
21+ --muted : # 94A2A9 ; --accent : # 2BC4C4 ; --accent-soft : # 2BC4C42A ;
22+ --brand-1 : # 8A6BE0 ; --brand-2 : # 2BC4C4 ; --brand-accent : # E0913C ; --cream : # D9AB74 ;
23+ --brand-grad : linear-gradient (100deg , # 8A6BE0 0% , # 4E84C8 45% , # 2BC4C4 100% );
24+ --sweet : # E8A94A ; --bitter : # A88AE0 ; --umami : # E0805E ; --sour : # BFD24E ; --salty : # 63A6E0 ; --aroma : # 3FC4AA ;
25+ --struct-bg : # F5F7F5 ; /* light 'paper' for molecule drawings (RDKit draws dark structures) */
1426 --ui : ui-sans-serif, system-ui, -apple-system, "Segoe UI" , Roboto, sans-serif;
1527 --mono : ui-monospace, "SF Mono" , Menlo, Consolas, monospace;
1628 }
1729 * {box-sizing : border-box}
1830 body {margin : 0 ;background : var (--surface );color : var (--ink );font-family : var (--ui );
1931 font-size : 15px ;line-height : 1.5 ;-webkit-font-smoothing : antialiased}
20- header {padding : 16px 28px ;border-bottom : 1px solid var (--line );display : flex;
21- align-items : center;gap : 15px ;background : linear-gradient (180deg , # FBFCFB, # F4F6F4 )}
22- header .logo {width : 54px ;height : 54px ;flex : 0 0 auto}
23- header .brand {display : flex;flex-direction : column;gap : 1px ;min-width : 0 }
24- header h1 {margin : 0 ;font-size : 23px ;font-weight : 750 ;letter-spacing : -.01em ;line-height : 1.1 ;
25- background : var (--brand-grad );-webkit-background-clip : text;background-clip : text;color : transparent}
26- header .sub {font-family : var (--mono );font-size : 12px ;color : var (--muted )}
32+ header {padding : 34px 28px 26px ;border-bottom : 1px solid var (--line );display : flex;
33+ flex-direction : column;align-items : center;gap : 6px ;text-align : center;
34+ background : radial-gradient (130% 150% at 50% -30% , # 1D2A38 0% , var (--surface ) 62% )}
35+ header .logo {width : 172px ;height : 172px ;margin-bottom : 16px ;
36+ filter : drop-shadow (0 6px 30px rgba (120 , 150 , 230 , .32 ))}
37+ header .brand {display : flex;flex-direction : column;align-items : center;gap : 8px }
38+ header h1 {margin : 0 ;font-family : 'Cinzel Decorative' , "Luminari" , Georgia, fantasy;
39+ font-size : 46px ;font-weight : 700 ;letter-spacing : .02em ;line-height : 1 ;
40+ background : var (--brand-grad );-webkit-background-clip : text;background-clip : text;color : transparent;
41+ filter : drop-shadow (0 2px 16px rgba (110 , 140 , 220 , .32 ))}
42+ header .sub {font-family : 'Cinzel Decorative' , "Luminari" , Georgia, serif;font-size : 15px ;
43+ font-weight : 700 ;text-transform : uppercase;letter-spacing : .16em ;color : var (--cream )}
2744 main {max-width : 980px ;margin : 0 auto;padding : 26px 28px 60px }
2845 .bar {display : flex;gap : 10px ;align-items : stretch}
2946 .bar input {flex : 1 ;font-family : var (--mono );font-size : 15px ;padding : 13px 15px ;
6986 .neighbor .nb-name {font-weight : 600 ;font-size : 13px ;text-transform : capitalize;color : var (--ink )}
7087 .neighbor .nb-iupac {font-family : var (--mono );font-size : 10px ;color : var (--muted );word-break : break-all;margin : 1px 0 }
7188 .nb-aroma {display : flex;flex-wrap : wrap;gap : 4px ;margin-top : 3px }
72- .nb-aroma .atag {font-size : 10px ;padding : 1px 7px ;border-radius : 20px ;background : # EAF3F0 ;color : var (--aroma );border : 1px solid # CFE6DF ;text-transform : capitalize}
89+ .nb-aroma .atag {font-size : 10px ;padding : 1px 7px ;border-radius : 20px ;background : transparent;color : var (--aroma );border : 1px solid;text-transform : capitalize}
90+ .kt {display : flex;flex-wrap : wrap;gap : 4px ;margin-top : 3px }
91+ .kt .tchip {font-size : 10px ;font-weight : 600 ;padding : 1px 8px ;border-radius : 20px ;color : # 10140F ;text-transform : capitalize}
92+ .kt .muted {color : var (--muted );font-size : 11px }
7393 .browse {margin : 6px 0 8px }
7494 .browse-head {font-size : 12px ;font-weight : 650 ;text-transform : uppercase;letter-spacing : .04em ;color : var (--muted );margin : 0 0 9px ;display : flex;align-items : center;justify-content : space-between;gap : 8px }
7595 .browse-toggle {display : inline-flex}
94114 .bcell .bname {font-size : 12px ;font-weight : 600 ;margin-top : 5px ;line-height : 1.25 ;text-transform : capitalize;cursor : pointer}
95115 .bcell .bname : hover {color : var (--aroma )}
96116 .map-section {margin : 16px 0 8px ;border-top : 1px solid # EEF0EC ;padding-top : 12px }
97- .map-head {font-size : 13 px ;font-weight : 650 ;cursor : pointer; color : var (--ink ) }
98- .map-head .mapsub {font-weight : 400 ; font-size : 12px ;color : var (--muted )}
117+ .map-head {font-size : 12 px ;font-weight : 650 ;text-transform : uppercase; letter-spacing : .04 em ; color : var (--muted ); cursor : pointer }
118+ .map-head .mapsub {font-weight : 600 ; text-transform : none; letter-spacing : 0 ; font-size : 12px ;color : var (--muted ); opacity : .8 }
99119 .map-legend {display : flex;flex-wrap : wrap;gap : 10px ;margin : 10px 0 6px ;font-size : 12px }
100120 .map-legend span {display : inline-flex;align-items : center;gap : 5px }
101121 .map-legend i {width : 10px ;height : 10px ;border-radius : 50% ;display : inline-block}
152172 .s3-empty {position : absolute;inset : 0 ;display : flex;align-items : center;justify-content : center;color : var (--muted );font-size : 13px }
153173 .domain-banner {background : # FFF6E6 ;border : 1px solid # E8D38A ;color : # 7A5A12 ;border-radius : 8px ;padding : 10px 13px ;font-size : 13px ;margin-bottom : 14px ;line-height : 1.5 }
154174 .hint code {font-family : var (--mono );background : var (--accent-soft );padding : 1px 5px ;border-radius : 4px ;color : var (--accent )}
155- .toggle {font-family : var (--mono );font-size : 12px ;color : var (--muted );margin : 12px 2px 0 ;display : flex;align-items : center;gap : 7px ;cursor : pointer}
156- # mixPanel {margin-top : 16px ;background : var (--panel );border : 1px solid var (--line );border-radius : 12px ;padding : 18px 20px }
175+ .toggle {font-size : 12px ;font-weight : 650 ;text-transform : uppercase;letter-spacing : .04em ;color : var (--muted );margin : 14px 2px 0 ;display : flex;align-items : center;gap : 7px ;cursor : pointer}
176+ # mixPanel {margin-top : 12px ;background : var (--panel );border : 1px solid var (--line );border-radius : 12px ;padding : 18px 20px }
177+ .mix-explain {font-size : 12.5px ;color : var (--muted );line-height : 1.55 ;margin : 0 0 12px ;max-width : 840px }
178+ .mix-explain b {color : var (--ink )}
179+ .mix-examples {font-size : 12px ;color : var (--muted );margin : 0 0 13px ;display : flex;flex-wrap : wrap;gap : 7px ;align-items : center}
180+ .mix-ex {font : inherit;font-size : 11.5px ;padding : 3px 10px ;border : 1px solid var (--line );border-radius : 16px ;background : transparent;color : var (--brand-2 );cursor : pointer}
181+ .mix-ex : hover {border-color : var (--brand-2 )}
157182 .mix-row {display : flex;gap : 8px ;margin-bottom : 8px }
158183 .mix-row input {flex : 1 ;font-family : var (--mono );font-size : 14px ;padding : 9px 12px ;border : 1px solid var (--line );border-radius : 8px ;background : var (--surface )}
159184 .mix-row .rm {border : 1px solid var (--line );background : var (--surface );border-radius : 8px ;padding : 0 13px ;cursor : pointer;color : var (--muted )}
171196 .sg-row {display : flex;gap : 10px ;align-items : center;padding : 8px 10px ;cursor : pointer;border-bottom : 1px solid var (--line )}
172197 .sg-row : last-child {border-bottom : none}
173198 .sg-row : hover {background : var (--accent-soft )}
174- .sg-st {flex : 0 0 auto;width : 56px ;height : 42px ;background : # fff ;border : 1px solid var (--line );border-radius : 5px }
199+ .sg-st {flex : 0 0 auto;width : 56px ;height : 42px ;background : var (--struct-bg );border : 1px solid var (--line );border-radius : 5px }
200+ /* --- dark-mode surface fixes (molecule drawings stay on light 'paper' --struct-bg) --- */
201+ .cat , .st-tab , .mini-3d , .map-reset , .suggest-dd {background : var (--panel )}
202+ .structure , .structure3d , .neighbor .nb-struct , .bcell .bstruct {background : var (--struct-bg )}
203+ .bcell {background : var (--panel )}
204+ .track , .atrack , .tag .predicted {background : # 242C35 }
205+ # flavorMap {background : # 0C1016 }
206+ .odor-note {background : # 1F2831 ;border-color : # 2E3742 }
207+ .aroma-card {background : linear-gradient (180deg , # 17282A, # 181D24 )!important ;border-color : # 2C4A44!important }
208+ .err {background : # 3A1E18 ;border-color : # 5A2E22 ;color : # F0C6BA }
209+ .cat .on {color : # fff }
175210 .sg-st svg {width : 56px ;height : 42px }
176211 .sg-info {min-width : 0 }
177212 .sg-name {font-weight : 600 ;font-size : 13px }
@@ -197,8 +232,20 @@ <h1>Flavormancer</h1>
197232 < b > SMILES</ b > string (e.g. < code > O=Cc1ccc(O)c(OC)c1</ code > ). SMILES omit implicit
198233 hydrogens — water is just < code > O</ code > . These models are for < b > organic</ b > molecules.</ div >
199234
200- < label class ="toggle "> < input type ="checkbox " id ="mixToggle "> Mixture mode — screen 2+ ingredients for documented hazards</ label >
201- < div id ="mixPanel " style ="display:none ">
235+ < label class ="toggle "> < input type ="checkbox " id ="mixToggle " checked > < span id ="mixCaret "> ▾</ span > Mixture screen — flag documented hazards when ingredients combine</ label >
236+ < div id ="mixPanel ">
237+ < p class ="mix-explain "> Enter two or more ingredients and Flavormancer flags < b > documented
238+ precursor→product hazards</ b > — combinations known to form a harmful compound (e.g. a
239+ preservative + an acid → a carcinogen). It's a < b > curated safety screen, not a general
240+ reaction predictor</ b > : a hazard fires only when the required roles co-occur, and
241+ process-gated ones (heat / refining / fermentation) only when you mark that process.
242+ It also gives a per-ingredient taste read and a single-molecule palette match.</ p >
243+ < div class ="mix-examples "> < span > Try a known bad combo:</ span >
244+ < button type ="button " class ="mix-ex " data-ings ="sodium benzoate|ascorbic acid "> benzoate + vitamin C → benzene</ button >
245+ < button type ="button " class ="mix-ex " data-ings ="sodium nitrite|dimethylamine "> nitrite + amine → nitrosamine</ button >
246+ < button type ="button " class ="mix-ex " data-ings ="glucose|asparagine " data-proc ="high_heat "> sugar + asparagine + heat → acrylamide</ button >
247+ < button type ="button " class ="mix-ex " data-ings ="ethanol|urea "> ethanol + urea → ethyl carbamate</ button >
248+ </ div >
202249 < div id ="mixRows "> </ div >
203250 < button class ="mix-add " id ="mixAdd "> + add ingredient</ button >
204251 < div class ="mix-proc "> Process applied:
@@ -217,9 +264,9 @@ <h1>Flavormancer</h1>
217264 </ div >
218265
219266 < div class ="map-section ">
220- < div class ="map-head " id ="mapToggle "> < span id ="mapCaret "> ▸ </ span > Flavor-space map
267+ < div class ="map-head " id ="mapToggle "> < span id ="mapCaret "> ▾ </ span > Flavor-space map
221268 < span class ="mapsub "> — molecules placed by structural similarity; taste classes cluster</ span > </ div >
222- < div id ="mapWrap " style =" display:none " >
269+ < div id ="mapWrap ">
223270 < div class ="map-controls ">
224271 < span class ="map-toggle3d "> < button type ="button " id ="map2d " class ="st-tab on "> 2D</ button > < button type ="button " id ="map3d " class ="st-tab "> 3D</ button > </ span >
225272 < span id ="mapLegend " class ="map-legend "> </ span >
@@ -421,7 +468,7 @@ <h2>Aroma</h2>
421468 <div class="nb-name">${ x . name || '—' } </div>
422469 ${ x . iupac ?`<div class="nb-iupac">${ x . iupac } </div>` :'' }
423470 <div class="s">${ x . smiles } </div>
424- <div class="kt">${ x . known_tastes . length ?x . known_tastes . join ( ' · ' ) : '— '} </div>
471+ <div class="kt">${ x . known_tastes . length ?x . known_tastes . map ( t => `<span class="tchip" style="background: ${ TASTE_COLORS [ t ] || 'var(--muted)' } "> ${ t } </span>` ) . join ( '' ) : '<span class="muted">no known taste</span> '} </div>
425472 ${ ( x . aroma && x . aroma . length ) ?`<div class="nb-aroma" title="aroma (found = documented, else predicted •)">${ x . aroma . map ( a => `<span class="atag" style="color:${ aromaColor ( a . odor ) } ;border-color:${ aromaColor ( a . odor ) } 44">${ a . odor } ${ a . source === 'predicted' ?' •' :'' } </span>` ) . join ( '' ) } </div>` :'' }
426473 </div>
427474 <div class="sim">${ Math . round ( x . similarity * 100 ) } % match</div>
@@ -641,7 +688,7 @@ <h2>Aroma</h2>
641688initBrowse ( ) ;
642689
643690// --- flavor-space map: 2D (zoom/pan) + rotatable 3D point cloud, colored by taste ---
644- const MAP_COLORS = { sweet :'#E0972E ' , bitter :'#6B4E9E ' , umami :'#C0563B ' , sour :'#9FB024 ' , salty :'#4E84B0 ' , other :'#C3C9C0 ' } ;
691+ const MAP_COLORS = { sweet :'#E8A94A ' , bitter :'#A88AE0 ' , umami :'#E0805E ' , sour :'#BFD24E ' , salty :'#63A6E0 ' , other :'#8A96A0 ' } ;
645692let _mapPts = null , _mapReady = false , _mapMode = '2d' , _mapProj = null ;
646693let _v2 = { s :1 , ox :0 , oy :0 } , _v3 = { rx :- 0.45 , ry :0.6 } , _drag = null , _dragged = false ;
647694
@@ -712,6 +759,7 @@ <h2>Aroma</h2>
712759 cv . addEventListener ( 'click' , ( ) => { if ( _dragged ) { _dragged = false ; return ; } const p = _mapLastHover ; if ( p ) { q . value = p . smiles ; window . scrollTo ( { top :0 , behavior :'smooth' } ) ; run ( ) ; } } ) ;
713760}
714761let _mapLastHover = null ;
762+ loadMap ( ) ; // map is open by default
715763
716764// --- mixture mode ---
717765function mixAddRow ( val ) {
@@ -723,13 +771,20 @@ <h2>Aroma</h2>
723771 div . append ( inp , rm ) ; $ ( 'mixRows' ) . appendChild ( div ) ;
724772}
725773$ ( 'mixAdd' ) . addEventListener ( 'click' , ( ) => mixAddRow ( ) ) ;
774+ // mixture is its own collapsible section — it no longer hides the search bar/results
726775$ ( 'mixToggle' ) . addEventListener ( 'change' , e => {
727776 const on = e . target . checked ;
728- $ ( 'mixPanel' ) . style . display = on ?'block' :'none' ;
729- document . querySelector ( '.bar' ) . style . display = on ?'none' :'flex' ;
730- [ 'results' , 'behaviorCard' , 'aromaCard' , 'footnote' , 'err' ] . forEach ( id => $ ( id ) . style . display = 'none' ) ;
777+ $ ( 'mixPanel' ) . style . display = on ?'block' :'none' ; $ ( 'mixCaret' ) . textContent = on ?'▾' :'▸' ;
731778 if ( on && ! $ ( 'mixRows' ) . children . length ) { mixAddRow ( ) ; mixAddRow ( ) ; }
732779} ) ;
780+ if ( $ ( 'mixToggle' ) . checked && ! $ ( 'mixRows' ) . children . length ) { mixAddRow ( ) ; mixAddRow ( ) ; } // open by default
781+ // click a bad-combo example -> load its ingredients + process and run the screen
782+ document . querySelectorAll ( '.mix-ex' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => {
783+ $ ( 'mixRows' ) . innerHTML = '' ;
784+ b . dataset . ings . split ( '|' ) . forEach ( s => mixAddRow ( s ) ) ;
785+ document . querySelectorAll ( '.proc' ) . forEach ( c => { c . checked = ( b . dataset . proc || '' ) . split ( ',' ) . includes ( c . value ) ; } ) ;
786+ $ ( 'mixCheck' ) . click ( ) ;
787+ } ) ) ;
733788$ ( 'mixResults' ) . addEventListener ( 'click' , e => {
734789 const card = e . target . closest ( '.mix-ing[data-smi]' ) ;
735790 if ( card && card . dataset . smi ) { $ ( 'mixToggle' ) . checked = false ; $ ( 'mixToggle' ) . dispatchEvent ( new Event ( 'change' ) ) ; q . value = card . dataset . smi ; window . scrollTo ( { top :0 , behavior :'smooth' } ) ; run ( ) ; }
0 commit comments