UCS2014 commited on
Commit
9c6863e
·
verified ·
1 Parent(s): 7b1c3d4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -173
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # -*- coding: utf-8 -*-
2
- from typing import Optional
3
  import base64, mimetypes
4
  from html import escape
5
  from pathlib import Path
@@ -12,82 +12,104 @@ ASSETS = BASE_DIR / "assets"
12
  # ========= META =========
13
  st.set_page_config(page_title="ST_LOG SUITE — Apps", page_icon="🧭", layout="wide")
14
 
15
- # ========= TEXT =========
16
- SUITE_NAME = "ST_LOG SUITE"
17
- SUITE_TAGLINE = "Generating AI-Based Well Logging Profiles While Drilling"
18
-
19
- # ========= COLOR PALETTE =========
20
- NAVY_900 = "##388E3C"
21
- NAVY_700 = "##388E3C"
22
- SLATE_600 = "#4DD0E1"
23
- PAPER_TOP = "#D32F2F"
24
- PAPER_BOT = "#FFD54F"
25
- GOLD = "#EAB308"
26
-
27
- # ========= SIZE & SPACING CONTROLS (EDIT THESE) =========
28
- TOP_PADDING_PX = 50 # top padding of the whole page
29
- STRIP_GAP_PX = 10 # space between pill and tagline
30
- STRIP_PILL_PAD_V_PX = 8
31
- STRIP_PILL_PAD_H_PX = 14
32
- STRIP_PILL_FONT_PX = 16
33
- TAGLINE_FONT_PX = 15
34
- STRIP_BELOW_GAP_PX = 30 # space between the top-left strip row and the hero
35
-
36
- HERO_LOGO_WIDTH_PX = 400 # width of the hero logo
37
- HERO_MARGIN_BOTTOM_PX= 30 # space under the hero logo
38
-
39
- GRID_GAP_PX = 50 # space between cards
40
- CARD_WIDTH_PX = 340 # card width
41
- CARD_RADIUS_PX = 22
42
- CARD_BORDER_PX = 2
43
- CARD_PAD_V_PX = 24
44
- CARD_PAD_H_PX = 20
45
-
46
- ICON_DIAM_PX = 118 # outer circle
47
- ICON_IMG_PX = 106 # image inside the circle
48
-
49
- TITLE_FONT_PX = 25 # app title size
50
- BLURB_FONT_PX = 16
51
- BUTTON_FONT_PX = 16
52
- BUTTON_PAD_V_PX = 12
53
- BUTTON_PAD_H_PX = 20
54
- BUTTON_RADIUS_PX = 14
55
-
56
- # ========= BEHAVIOR / OPTIONS =========
57
- SHOW_CARD_CHIP = False # show small suite chip inside each card?
58
- USE_TINTED_CARD_BG = True # very light background tint per card (3–5%)
59
-
60
- # ========= ASSETS =========
61
- HERO_LOGO = ASSETS / "AI_Suite_Log_logo.png"
62
- ICON_GR = ASSETS / "GR_logo.png"
63
- ICON_TS = ASSETS / "Ts_logo.png"
64
- ICON_TC = ASSETS / "Tc_logo.png"
65
-
66
- APPS = [
 
 
 
 
 
 
 
67
  {
68
  "title": "ST_Log_GR",
69
- "url": "https://smart-thinking-gr.hf.space/",
70
  "blurb": "Real-time gamma-ray log prediction.",
71
- "icon": ICON_GR,
72
- "tint": "linear-gradient(180deg, rgba(14,116,144,0.05) 0%, rgba(14,116,144,0.02) 100%)",
 
 
 
 
 
 
 
 
 
 
73
  },
74
  {
75
  "title": "ST_Log_Sonic (Ts)",
76
- "url": "https://smart-thinking-sonic-ts.hf.space",
77
  "blurb": "Predict shear slowness (DtS) in real time.",
78
- "icon": ICON_TS,
79
- "tint": "linear-gradient(180deg, rgba(2,132,199,0.05) 0%, rgba(2,132,199,0.02) 100%)",
 
 
 
 
80
  },
81
  {
82
  "title": "ST_Log_Sonic (Tc)",
83
- "url": "https://smart-thinking-sonic-tc.hf.space",
84
  "blurb": "Predict compressional slowness (DtC) in real time.",
85
- "icon": ICON_TC,
86
- "tint": "linear-gradient(180deg, rgba(99,102,241,0.05) 0%, rgba(99,102,241,0.02) 100%)",
 
 
 
 
87
  },
88
  ]
89
 
90
- # ========= HELPERS =========
91
  def data_uri(path: Path) -> Optional[str]:
92
  if not path or not path.exists():
93
  return None
@@ -105,77 +127,90 @@ def img_tag(path: Path, alt: str, cls: str = "", style: str = "") -> str:
105
  style_attr = f' style="{style}"' if style else ""
106
  return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />'
107
 
108
- # ========= CSS =========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  st.markdown(f"""
110
  <style>
111
  :root {{
112
- /* sizes */
113
- --top-pad: {TOP_PADDING_PX}px;
114
- --strip-gap: {STRIP_GAP_PX}px;
115
- --strip-pill-pv: {STRIP_PILL_PAD_V_PX}px;
116
- --strip-pill-ph: {STRIP_PILL_PAD_H_PX}px;
117
- --strip-pill-fs: {STRIP_PILL_FONT_PX}px;
118
- --tagline-fs: {TAGLINE_FONT_PX}px;
119
- --strip-row-mb: {STRIP_BELOW_GAP_PX}px;
120
-
121
- --hero-w: {HERO_LOGO_WIDTH_PX}px;
122
- --hero-mb: {HERO_MARGIN_BOTTOM_PX}px;
123
-
124
- --grid-gap: {GRID_GAP_PX}px;
125
- --card-w: {CARD_WIDTH_PX}px;
126
- --card-r: {CARD_RADIUS_PX}px;
127
- --card-bw: {CARD_BORDER_PX}px;
128
- --card-pv: {CARD_PAD_V_PX}px;
129
- --card-ph: {CARD_PAD_H_PX}px;
130
-
131
- --icon-d: {ICON_DIAM_PX}px;
132
- --icon-img: {ICON_IMG_PX}px;
133
-
134
- --title-fs: {TITLE_FONT_PX}px;
135
- --blurb-fs: {BLURB_FONT_PX}px;
136
- --btn-fs: {BUTTON_FONT_PX}px;
137
- --btn-pv: {BUTTON_PAD_V_PX}px;
138
- --btn-ph: {BUTTON_PAD_H_PX}px;
139
- --btn-r: {BUTTON_RADIUS_PX}px;
140
-
141
- /* colors */
142
- --navy1: {NAVY_900};
143
- --navy2: {NAVY_700};
144
- --gold: {GOLD};
145
- --slate6:{SLATE_600};
146
-
147
- /* outlines / 3D */
148
- --card-stroke: {NAVY_900};
149
- --card-stroke-hover: #1E293B;
150
  }}
151
 
152
  html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
153
  [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
154
 
155
  .block-container {{
156
- max-width: 1120px;
157
  min-height: 100vh;
158
  display: flex; flex-direction: column;
159
  gap: 14px;
160
  padding: var(--top-pad) 0 28px !important;
161
- background:
162
- radial-gradient(980px 460px at 50% -140px,
163
- rgba(2,12,30,0.06) 0%,
164
- rgba(2,12,30,0.04) 28%,
165
- rgba(255,255,255,0.98) 60%,
166
- rgba(255,255,255,1) 100%);
167
  }}
168
 
169
- /* ===== TOP-LEFT STRIP ROW ===== */
170
  .suite-row {{
171
  display:flex; align-items:center; gap: var(--strip-gap);
172
  justify-content:flex-start; flex-wrap: wrap;
173
  margin: 0 0 var(--strip-row-mb) 0;
174
  }}
175
-
176
  .suite-pill {{
177
- background: linear-gradient(90deg, var(--navy1) 0%, var(--navy2) 100%);
178
- color: var(--gold);
179
  padding: var(--strip-pill-pv) var(--strip-pill-ph);
180
  border-radius: 999px;
181
  font-weight: 800; letter-spacing: .25px;
@@ -184,12 +219,12 @@ st.markdown(f"""
184
  white-space: nowrap;
185
  }}
186
  .suite-tagline {{
187
- color: var(--slate6); font-weight: 600; opacity: .95;
188
  font-size: var(--tagline-fs);
189
  white-space: nowrap;
190
  }}
191
 
192
- /* HERO LOGO (below the strip) */
193
  .hero {{ text-align:center; margin: 0 0 var(--hero-mb); }}
194
  .hero img {{
195
  width: var(--hero-w); max-width: 92vw; height: auto;
@@ -197,13 +232,12 @@ st.markdown(f"""
197
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.10));
198
  }}
199
 
200
- /* GRID */
201
  .grid {{
202
  display: grid;
203
  grid-template-columns: repeat(3, var(--card-w));
204
  gap: var(--grid-gap);
205
- justify-content: center;
206
- align-items: stretch;
207
  margin-top: 10px;
208
  }}
209
  @media (max-width: 1120px) {{
@@ -213,25 +247,24 @@ st.markdown(f"""
213
  .grid {{ grid-template-columns: 1fr; }}
214
  }}
215
 
216
- /* CARD dark outline + premium 3D look */
217
  .card {{
218
  position: relative;
219
- width: var(--card-w);
220
- border-radius: var(--card-r);
221
- padding: var(--card-pv) var(--card-ph);
222
- background: var(--card-bg, linear-gradient(180deg, {PAPER_TOP} 0%, {PAPER_BOT} 100%));
223
- border: var(--card-bw) solid var(--card-stroke);
224
  box-shadow:
225
  0 14px 32px rgba(2,20,35,.12),
226
  0 1px 0 rgba(255,255,255,0.90) inset,
227
  0 -1px 10px rgba(255,255,255,0.40) inset;
228
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;
229
- text-align:center; display:flex; flex-direction:column; gap:16px;
230
- align-items: center;
231
  }}
232
  .card:hover {{
233
  transform: translateY(-6px) scale(1.01);
234
- border-color: var(--card-stroke-hover);
235
  box-shadow:
236
  0 22px 56px rgba(2,20,35,.18),
237
  0 1px 0 rgba(255,255,255,0.92) inset,
@@ -239,97 +272,118 @@ st.markdown(f"""
239
  filter: saturate(1.03);
240
  }}
241
 
242
- .suite-chip {{
243
- position: absolute; top: 12px; right: 12px;
244
- background: linear-gradient(90deg, var(--navy1) 0%, var(--navy2) 100%);
245
- color: var(--gold); padding: 6px 10px; border-radius: 999px;
246
- font-weight: 700; font-size: 13px; letter-spacing: .2px;
247
- box-shadow: 0 6px 14px rgba(2,12,30,.18);
248
- }}
249
-
250
  .icon-wrap {{
251
- width: var(--icon-d); height: var(--icon-d); border-radius: 9999px;
252
- background: #F1F5F9;
253
- display:grid; place-items:center;
254
- border: 1px solid rgba(12,18,32,0.10);
255
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 10px 22px rgba(2,20,35,.07);
256
  }}
257
  .icon-wrap img {{
258
- width: var(--icon-img); height: var(--icon-img); border-radius:9999px; display:block;
 
259
  }}
260
 
261
  .card h3 {{
262
- margin: 0; font-size: var(--title-fs); font-weight: 900; color:#0b1220;
263
- letter-spacing: .15px;
264
  }}
265
  .card p {{
266
- color:#566275; min-height: 40px; margin: 0 10px; font-size: var(--blurb-fs);
 
267
  }}
268
 
269
  .btn {{
270
- display:inline-block; padding: var(--btn-pv) var(--btn-ph); border-radius: var(--btn-r);
271
- border: 1px solid rgba(11,18,32,.45);
272
- background: linear-gradient(180deg, var(--navy1) 0%, var(--navy2) 100%);
273
- font-weight: 800; letter-spacing:.3px; font-size: var(--btn-fs);
 
274
  margin: 6px auto 0;
275
  box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10);
 
 
276
  }}
277
  a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{
278
- color: #ffffff !important; text-decoration: none !important;
279
  }}
280
  .btn:hover {{
281
  filter: brightness(1.03) saturate(1.04);
282
  transform: translateY(-1px);
283
  box-shadow: 0 16px 38px rgba(11,18,32,.36);
284
  }}
285
- .btn:focus {{
286
- outline: none;
287
- box-shadow: 0 0 0 4px rgba(30,41,59,.28), 0 16px 38px rgba(11,18,32,.36);
288
- }}
289
 
290
  .footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 26px; }}
291
  .footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }}
292
  </style>
293
  """, unsafe_allow_html=True)
294
 
295
- # ===== TOP-LEFT STRIP (then hero below) =====
296
  st.markdown(
297
  f"""
298
  <div class="suite-row">
299
- <span class="suite-pill">{escape(SUITE_NAME)}</span>
300
- <span class="suite-tagline">{escape(SUITE_TAGLINE)}</span>
301
  </div>
302
  """,
303
  unsafe_allow_html=True,
304
  )
305
 
306
- # ===== HERO (logo below the strip) =====
307
- hero_html = img_tag(HERO_LOGO, "ST_LOG SUITE")
308
  st.markdown(f"<div class='hero'>{hero_html}</div>", unsafe_allow_html=True)
309
 
310
- # ===== CARDS =====
311
- def app_card(app: dict) -> str:
312
- tint = app.get("tint") if USE_TINTED_CARD_BG else f"linear-gradient(180deg, {PAPER_TOP} 0%, {PAPER_BOT} 100%)"
313
- icon_html = img_tag(app.get("icon"), "icon",
314
- style=f"width:{ICON_IMG_PX}px;height:{ICON_IMG_PX}px;border-radius:9999px;") \
315
- if app.get("icon") and app["icon"].exists() else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  target = "_self"
317
- chip = f"<div class='suite-chip'>{escape(SUITE_NAME)}</div>" if SHOW_CARD_CHIP else ""
318
- style_vars = f"--card-bg:{tint};"
319
  return (
320
- f"<div class='card' style='{style_vars}'>"
321
- + chip
322
  + f"<div class='icon-wrap'>{icon_html}</div>"
323
- + f"<h3>{escape(app['title'])}</h3>"
324
- + f"<p>{escape(app['blurb'])}</p>"
325
- + f"<a class='btn' href='{escape(app['url'])}' target='{target}' rel='noopener'>Run App</a>"
326
  + "</div>"
327
  )
328
 
329
- cards_html = "".join(app_card(a) for a in APPS)
330
- st.markdown(f"<div class='grid'>{cards_html}</div>", unsafe_allow_html=True)
331
 
332
- # ===== FOOTER =====
333
  st.markdown(
334
  """
335
  <hr>
 
1
  # -*- coding: utf-8 -*-
2
+ from typing import Optional, Dict, Any
3
  import base64, mimetypes
4
  from html import escape
5
  from pathlib import Path
 
12
  # ========= META =========
13
  st.set_page_config(page_title="ST_LOG SUITE — Apps", page_icon="🧭", layout="wide")
14
 
15
+ # ========= GLOBAL THEME (one place) =========
16
+ THEME: Dict[str, Any] = {
17
+ "strip": {
18
+ "gap": 10, # px between pill and tagline
19
+ "below_gap": 30, # px between strip row and hero
20
+ "pill_pad_v": 8, # px
21
+ "pill_pad_h": 14, # px
22
+ "pill_font": 16, # px
23
+ "tagline_font": 15, # px
24
+ "bg1": "#90CAF9", # strip gradient start
25
+ "bg2": "#90CAF9", # strip gradient end
26
+ "text": "#EAB308", # gold text in pill
27
+ "tagline_color": "#000000",
28
+ },
29
+ "page": {
30
+ "top_padding": 50, # px top padding for page
31
+ "container_width": 1120, # max content width
32
+ "bg_radial": True, # premium faint radial bg
33
+ },
34
+ "hero": {
35
+ "width": 400, # px
36
+ "margin_bottom": 30, # px space under hero
37
+ "logo": ASSETS / "AI_Suite_Log_logo.png",
38
+ },
39
+ "grid": {
40
+ "gap": 50, # px between cards
41
+ "card_width": 340, # default card width (px)
42
+ },
43
+ "card": {
44
+ "radius": 22, # px
45
+ "border_width": 2, # px
46
+ "pad_v": 24, # px
47
+ "pad_h": 20, # px
48
+ "border": "#0B1220", # dark outline
49
+ "border_hover": "#1E293B",
50
+ "bg_top": "#E57373", # top colour of default card gradient
51
+ "bg_bot": "#E57373", # bottom colour of default card gradient
52
+ "title_color": "#0B1220",
53
+ "blurb_color": "#566275",
54
+ },
55
+ "icon": {
56
+ "diam": 118, # px outer circle
57
+ "img": 106, # px image
58
+ "circle_bg": "#F1F5F9",
59
+ "circle_border": "rgba(12,18,32,0.10)",
60
+ },
61
+ "button": {
62
+ "pad_v": 12, # px
63
+ "pad_h": 20, # px
64
+ "radius": 14, # px
65
+ "bg1": "#90CAF9",
66
+ "bg2": "#90CAF9",
67
+ "text": "#EAB308",
68
+ "border": "rgba(11,18,32,.45)",
69
+ },
70
+ }
71
+
72
+ # ========= CARDS (content + per-card overrides) =========
73
+ CARDS = [
74
  {
75
  "title": "ST_Log_GR",
 
76
  "blurb": "Real-time gamma-ray log prediction.",
77
+ "url": "https://smart-thinking-gr.hf.space/",
78
+ "icon": ASSETS / "GR_logo.png",
79
+ # Optional per-card overrides:
80
+ "style": {
81
+ "bg_top": "rgba(14,116,144,0.05)",
82
+ "bg_bot": "rgba(14,116,144,0.02)",
83
+ # "border": "#0B1220",
84
+ # "title_color": "#0B1220",
85
+ # "blurb_color": "#566275",
86
+ # "width": 360, "radius": 24, "pad_v": 26, "pad_h": 22,
87
+ # "btn_bg1": "#0B1220", "btn_bg2": "#1F2937", "btn_text": "#FFFFFF",
88
+ },
89
  },
90
  {
91
  "title": "ST_Log_Sonic (Ts)",
 
92
  "blurb": "Predict shear slowness (DtS) in real time.",
93
+ "url": "https://smart-thinking-sonic-ts.hf.space",
94
+ "icon": ASSETS / "Ts_logo.png",
95
+ "style": {
96
+ "bg_top": "rgba(2,132,199,0.05)",
97
+ "bg_bot": "rgba(2,132,199,0.02)",
98
+ },
99
  },
100
  {
101
  "title": "ST_Log_Sonic (Tc)",
 
102
  "blurb": "Predict compressional slowness (DtC) in real time.",
103
+ "url": "https://smart-thinking-sonic-tc.hf.space",
104
+ "icon": ASSETS / "Tc_logo.png",
105
+ "style": {
106
+ "bg_top": "rgba(99,102,241,0.05)",
107
+ "bg_bot": "rgba(99,102,241,0.02)",
108
+ },
109
  },
110
  ]
111
 
112
+ # ========= Helpers =========
113
  def data_uri(path: Path) -> Optional[str]:
114
  if not path or not path.exists():
115
  return None
 
127
  style_attr = f' style="{style}"' if style else ""
128
  return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />'
129
 
130
+ def get_style(card_style: Dict[str, Any], key: str, fallback_key: str = None):
131
+ if key in card_style:
132
+ return card_style[key]
133
+ if fallback_key:
134
+ return THEME["card"][fallback_key]
135
+ return None
136
+
137
+ # ========= CSS (driven from THEME) =========
138
+ strip = THEME["strip"]; page = THEME["page"]; hero = THEME["hero"]
139
+ grid = THEME["grid"]; card = THEME["card"]; icon = THEME["icon"]; button = THEME["button"]
140
+
141
+ bg_radial_css = """
142
+ background:
143
+ radial-gradient(980px 460px at 50% -140px,
144
+ rgba(2,12,30,0.06) 0%,
145
+ rgba(2,12,30,0.04) 28%,
146
+ rgba(255,255,255,0.98) 60%,
147
+ rgba(255,255,255,1) 100%);
148
+ """ if page["bg_radial"] else "background: #fff;"
149
+
150
  st.markdown(f"""
151
  <style>
152
  :root {{
153
+ --top-pad: {page["top_padding"]}px;
154
+
155
+ --strip-gap: {strip["gap"]}px;
156
+ --strip-row-mb: {strip["below_gap"]}px;
157
+ --strip-pill-pv: {strip["pill_pad_v"]}px;
158
+ --strip-pill-ph: {strip["pill_pad_h"]}px;
159
+ --strip-pill-fs: {strip["pill_font"]}px;
160
+ --tagline-fs: {strip["tagline_font"]}px;
161
+
162
+ --hero-w: {hero["width"]}px;
163
+ --hero-mb: {hero["margin_bottom"]}px;
164
+
165
+ --grid-gap: {grid["gap"]}px;
166
+ --card-w: {grid["card_width"]}px;
167
+ --card-r: {card["radius"]}px;
168
+ --card-bw: {card["border_width"]}px;
169
+ --card-pv: {card["pad_v"]}px;
170
+ --card-ph: {card["pad_h"]}px;
171
+
172
+ --icon-d: {icon["diam"]}px;
173
+ --icon-img: {icon["img"]}px;
174
+
175
+ --title-fs: 25px;
176
+ --blurb-fs: 16px;
177
+ --btn-fs: {button["pad_v"] + 4}px; /* just a simple mapping; override below if desired */
178
+
179
+ --stripBg1: {strip["bg1"]};
180
+ --stripBg2: {strip["bg2"]};
181
+ --stripText: {strip["text"]};
182
+ --taglineColor: {strip["tagline_color"]};
183
+
184
+ --cardStroke: {card["border"]};
185
+ --cardStrokeHover: {card["border_hover"]};
186
+
187
+ --btn1: {button["bg1"]};
188
+ --btn2: {button["bg2"]};
189
+ --btnText: {button["text"]};
190
+ --btnBorder: {button["border"]};
191
  }}
192
 
193
  html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
194
  [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
195
 
196
  .block-container {{
197
+ max-width: {page["container_width"]}px;
198
  min-height: 100vh;
199
  display: flex; flex-direction: column;
200
  gap: 14px;
201
  padding: var(--top-pad) 0 28px !important;
202
+ {bg_radial_css}
 
 
 
 
 
203
  }}
204
 
205
+ /* ===== TOP-LEFT STRIP ===== */
206
  .suite-row {{
207
  display:flex; align-items:center; gap: var(--strip-gap);
208
  justify-content:flex-start; flex-wrap: wrap;
209
  margin: 0 0 var(--strip-row-mb) 0;
210
  }}
 
211
  .suite-pill {{
212
+ background: linear-gradient(90deg, var(--stripBg1) 0%, var(--stripBg2) 100%);
213
+ color: var(--stripText);
214
  padding: var(--strip-pill-pv) var(--strip-pill-ph);
215
  border-radius: 999px;
216
  font-weight: 800; letter-spacing: .25px;
 
219
  white-space: nowrap;
220
  }}
221
  .suite-tagline {{
222
+ color: var(--taglineColor); font-weight: 600; opacity: .95;
223
  font-size: var(--tagline-fs);
224
  white-space: nowrap;
225
  }}
226
 
227
+ /* ===== HERO ===== */
228
  .hero {{ text-align:center; margin: 0 0 var(--hero-mb); }}
229
  .hero img {{
230
  width: var(--hero-w); max-width: 92vw; height: auto;
 
232
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.10));
233
  }}
234
 
235
+ /* ===== GRID ===== */
236
  .grid {{
237
  display: grid;
238
  grid-template-columns: repeat(3, var(--card-w));
239
  gap: var(--grid-gap);
240
+ justify-content: center; align-items: stretch;
 
241
  margin-top: 10px;
242
  }}
243
  @media (max-width: 1120px) {{
 
247
  .grid {{ grid-template-columns: 1fr; }}
248
  }}
249
 
250
+ /* ===== CARD (uses per-card CSS vars) ===== */
251
  .card {{
252
  position: relative;
253
+ width: var(--c-w, var(--card-w));
254
+ border-radius: var(--c-radius, var(--card-r));
255
+ padding: var(--c-pv, var(--card-pv)) var(--c-ph, var(--card-ph));
256
+ background: linear-gradient(180deg, var(--c-bg-top, {card["bg_top"]}) 0%, var(--c-bg-bot, {card["bg_bot"]}) 100%);
257
+ border: var(--c-bw, var(--card-bw)) solid var(--c-border, var(--cardStroke));
258
  box-shadow:
259
  0 14px 32px rgba(2,20,35,.12),
260
  0 1px 0 rgba(255,255,255,0.90) inset,
261
  0 -1px 10px rgba(255,255,255,0.40) inset;
262
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;
263
+ text-align:center; display:flex; flex-direction:column; gap:16px; align-items: center;
 
264
  }}
265
  .card:hover {{
266
  transform: translateY(-6px) scale(1.01);
267
+ border-color: var(--cardStrokeHover);
268
  box-shadow:
269
  0 22px 56px rgba(2,20,35,.18),
270
  0 1px 0 rgba(255,255,255,0.92) inset,
 
272
  filter: saturate(1.03);
273
  }}
274
 
 
 
 
 
 
 
 
 
275
  .icon-wrap {{
276
+ width: var(--c-icon-d, {icon["diam"]}px); height: var(--c-icon-d, {icon["diam"]}px);
277
+ border-radius: 9999px; display:grid; place-items:center;
278
+ background: var(--c-icon-bg, {icon["circle_bg"]});
279
+ border: 1px solid var(--c-icon-border, {icon["circle_border"]});
280
  box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 10px 22px rgba(2,20,35,.07);
281
  }}
282
  .icon-wrap img {{
283
+ width: var(--c-icon-img, {icon["img"]}px); height: var(--c-icon-img, {icon["img"]}px);
284
+ border-radius:9999px; display:block;
285
  }}
286
 
287
  .card h3 {{
288
+ margin: 0; font-size: var(--c-title-fs, 25px); font-weight: 900;
289
+ color: var(--c-title-color, {card["title_color"]}); letter-spacing: .15px;
290
  }}
291
  .card p {{
292
+ color: var(--c-blurb-color, {card["blurb_color"]}); min-height: 40px; margin: 0 10px;
293
+ font-size: var(--c-blurb-fs, 16px);
294
  }}
295
 
296
  .btn {{
297
+ display:inline-block; padding: var(--c-btn-pv, {button["pad_v"]}px) var(--c-btn-ph, {button["pad_h"]}px);
298
+ border-radius: var(--c-btn-r, {button["radius"]}px);
299
+ border: 1px solid var(--c-btn-border, {button["border"]});
300
+ background: linear-gradient(180deg, var(--c-btn-bg1, {button["bg1"]}) 0%, var(--c-btn-bg2, {button["bg2"]}) 100%);
301
+ font-weight: 800; letter-spacing:.3px; font-size: var(--c-btn-fs, 16px);
302
  margin: 6px auto 0;
303
  box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10);
304
+ color: var(--c-btn-text, {button["text"]});
305
+ text-decoration: none;
306
  }}
307
  a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{
308
+ color: var(--c-btn-text, {button["text"]}) !important; text-decoration: none !important;
309
  }}
310
  .btn:hover {{
311
  filter: brightness(1.03) saturate(1.04);
312
  transform: translateY(-1px);
313
  box-shadow: 0 16px 38px rgba(11,18,32,.36);
314
  }}
 
 
 
 
315
 
316
  .footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 26px; }}
317
  .footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }}
318
  </style>
319
  """, unsafe_allow_html=True)
320
 
321
+ # ========= Strip (top-left) =========
322
  st.markdown(
323
  f"""
324
  <div class="suite-row">
325
+ <span class="suite-pill">{"ST_LOG SUITE"}</span>
326
+ <span class="suite-tagline">{"Generating AI-Based Well Logging Profiles While Drilling"}</span>
327
  </div>
328
  """,
329
  unsafe_allow_html=True,
330
  )
331
 
332
+ # ========= Hero =========
333
+ hero_html = img_tag(hero["logo"], "ST_LOG SUITE")
334
  st.markdown(f"<div class='hero'>{hero_html}</div>", unsafe_allow_html=True)
335
 
336
+ # ========= Cards =========
337
+ def build_card_vars(style: Dict[str, Any]) -> str:
338
+ """Turn the per-card style dict into CSS variables for inline override."""
339
+ s = []
340
+ if "width" in style: s.append(f"--c-w:{int(style['width'])}px")
341
+ if "radius" in style: s.append(f"--c-radius:{int(style['radius'])}px")
342
+ if "pad_v" in style: s.append(f"--c-pv:{int(style['pad_v'])}px")
343
+ if "pad_h" in style: s.append(f"--c-ph:{int(style['pad_h'])}px")
344
+
345
+ if "bg_top" in style: s.append(f"--c-bg-top:{style['bg_top']}")
346
+ if "bg_bot" in style: s.append(f"--c-bg-bot:{style['bg_bot']}")
347
+ if "border" in style: s.append(f"--c-border:{style['border']}")
348
+ if "border_width" in style: s.append(f"--c-bw:{int(style['border_width'])}px")
349
+
350
+ if "title_color" in style: s.append(f"--c-title-color:{style['title_color']}")
351
+ if "title_fs" in style: s.append(f"--c-title-fs:{int(style['title_fs'])}px")
352
+ if "blurb_color" in style: s.append(f"--c-blurb-color:{style['blurb_color']}")
353
+ if "blurb_fs" in style: s.append(f"--c-blurb-fs:{int(style['blurb_fs'])}px")
354
+
355
+ if "btn_bg1" in style: s.append(f"--c-btn-bg1:{style['btn_bg1']}")
356
+ if "btn_bg2" in style: s.append(f"--c-btn-bg2:{style['btn_bg2']}")
357
+ if "btn_text" in style: s.append(f"--c-btn-text:{style['btn_text']}")
358
+ if "btn_border" in style: s.append(f"--c-btn-border:{style['btn_border']}")
359
+ if "btn_fs" in style: s.append(f"--c-btn-fs:{int(style['btn_fs'])}px")
360
+ if "btn_pad_v" in style: s.append(f"--c-btn-pv:{int(style['btn_pad_v'])}px")
361
+ if "btn_pad_h" in style: s.append(f"--c-btn-ph:{int(style['btn_pad_h'])}px")
362
+
363
+ if "icon_diam" in style: s.append(f"--c-icon-d:{int(style['icon_diam'])}px")
364
+ if "icon_img" in style: s.append(f"--c-icon-img:{int(style['icon_img'])}px")
365
+ if "icon_bg" in style: s.append(f"--c-icon-bg:{style['icon_bg']}")
366
+ if "icon_border" in style: s.append(f"--c-icon-border:{style['icon_border']}")
367
+
368
+ return "; ".join(s)
369
+
370
+ def app_card(card_cfg: Dict[str, Any]) -> str:
371
+ style = card_cfg.get("style", {})
372
+ vars_inline = build_card_vars(style)
373
+ icon_html = img_tag(card_cfg.get("icon"), "icon") if card_cfg.get("icon") and card_cfg["icon"].exists() else ""
374
  target = "_self"
 
 
375
  return (
376
+ f"<div class='card' style='{vars_inline}'>"
 
377
  + f"<div class='icon-wrap'>{icon_html}</div>"
378
+ + f"<h3>{escape(card_cfg['title'])}</h3>"
379
+ + f"<p>{escape(card_cfg['blurb'])}</p>"
380
+ + f"<a class='btn' href='{escape(card_cfg['url'])}' target='{target}' rel='noopener'>Run App</a>"
381
  + "</div>"
382
  )
383
 
384
+ st.markdown("<div class='grid'>" + "".join(app_card(c) for c in CARDS) + "</div>", unsafe_allow_html=True)
 
385
 
386
+ # ========= Footer =========
387
  st.markdown(
388
  """
389
  <hr>