UCS2014 commited on
Commit
e28e7f8
·
verified ·
1 Parent(s): 969a122

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +346 -162
app.py CHANGED
@@ -1,189 +1,373 @@
 
 
1
  import base64, mimetypes
2
  from html import escape
3
  from pathlib import Path
4
  import streamlit as st
5
 
6
- # ========= PATHS (subfolder-safe) =========
7
- BASE_DIR = Path(__file__).parent # -> Landing_Page/
8
- ASSETS = BASE_DIR / "assets" # -> Landing_Page/assets/
9
-
10
- # ========= CONTROLS (tweak these) =========
11
- HERO_LOGO_SIZE = 180 # px (top company logo size)
12
- TOP_SPACER = 100 # px (breathing room above hero; raise if the logo looks "trimmed")
13
- CARD_WIDTH = 300 # px (fixed width per card on desktop)
14
- CARD_THUMB_HEIGHT = 180 # px (image area inside each card)
15
- GRID_GAP = 100 # px (space between the cards)
16
- BUTTON_TEXT = "Click to Run APP"
17
- OPEN_IN_NEW_TAB = False # True = open links in a new tab
18
-
19
- # ========= BRAND / APPS =========
20
- COMPANY_NAME = "Smart Thinking - Geomechanics"
21
- TAGLINE = "We Deliver Smart AI-Based Solutions For O&G Industry"
22
-
23
- APP1 = {
24
- "title": "ST_GeoMech_UCS",
25
- "url": "https://smart-thinking-ucs.hf.space/",
26
- "thumb": ASSETS / "app1.png",
27
- "blurb": "Real-Time UCS Prediction",
28
- "bg": "#F7FBFF", # very light blue
29
- "border":"#000000", # light blue border
30
- }
31
- APP2 = {
32
- "title": "ST_GeoMech_Ym",
33
- "url": "https://smart-thinking-ym.hf.space/",
34
- "thumb": ASSETS / "app2.png",
35
- "blurb": "Real-Time Static Young's Modulus Prediction",
36
- "bg": "#F6FFF7", # very light green
37
- "border":"#000000", # light green border
38
- }
39
- # NEW third card (edit title/url/blurb/thumb/colors as needed)
40
- APP3 = {
41
- "title": "ST_GeoMech_SMW",
42
- "url": "https://example.com/", # <-- replace with your Space URL
43
- "thumb": ASSETS / "app3.png", # <-- upload this image
44
- "blurb": "Real-Time Safe Mud Window Prediction",
45
- "bg": "#F9F6FF", # very light purple
46
- "border":"#000000", # light purple border
47
- }
48
 
49
- LOGO_PATH = ASSETS / "logo.png"
 
50
 
51
- # ========= PAGE META =========
52
- page_icon = str(LOGO_PATH) if LOGO_PATH.exists() else "🧭"
53
- st.set_page_config(page_title=f"{COMPANY_NAME} — Apps", page_icon=page_icon, layout="wide")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- # ========= CSS (fits one screen on desktop; no blank area under cards) =========
56
- st.markdown(
57
- f"""
58
- <style>
59
- :root {{
60
- --top-spacer: {TOP_SPACER}px;
61
- --card-width: {CARD_WIDTH}px;
62
- --grid-gap: {GRID_GAP}px;
63
- --tagline-gap: 0px; /* distance between H1 and tagline */
64
- --grid-top-space: 20px; /* space above the cards */
65
- }}
66
-
67
- html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
68
- [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
69
- .block-container {{
70
- max-width: 1120px;
71
- min-height: 100vh;
72
- display: flex; flex-direction: column;
73
- justify-content: center;
74
- gap: 10px;
75
- padding: var(--top-spacer) 0 8px !important;
76
- }}
77
-
78
- /* HERO */
79
- .hero {{ text-align:center; margin: 0 0 8px; }}
80
- .hero img {{
81
- width: {HERO_LOGO_SIZE}px; max-width: 95vw; height: auto;
82
- display:block; margin: 0 auto 8px;
83
- filter: drop-shadow(0 2px 8px rgba(0,0,0,.12));
84
- }}
85
- /* Make tagline closer to the company name */
86
- .hero h1 {{ font-size: 2.3rem; margin: .2rem 0 0; }}
87
- .hero p {{
88
- color: #5a5f6a;
89
- margin: var(--tagline-gap) auto 0;
90
- max-width: 720px;
91
- font-style: italic;
92
- }}
93
-
94
- /* GRID: precise card width + extra space above cards */
95
- .grid {{
96
- display: flex;
97
- gap: var(--grid-gap);
98
- justify-content: center;
99
- align-items: stretch;
100
- flex-wrap: nowrap; /* keep three across on desktop */
101
- margin-top: var(--grid-top-space);
102
- }}
103
- /* When viewport too narrow for 3-up, allow wrapping */
104
- @media (max-width: calc(3 * var(--card-width) + 2 * var(--grid-gap) + 64px)) {{
105
- .grid {{ flex-wrap: wrap; }}
106
- }}
107
-
108
- .card {{
109
- width: var(--card-width);
110
- background: var(--card-bg, #fff);
111
- border: 1px solid var(--card-border, rgba(0,0,0,.06));
112
- border-radius:16px; padding:14px;
113
- box-shadow:0 4px 18px rgba(0,0,0,.05);
114
- transition:transform .12s ease, box-shadow .12s ease;
115
- text-align:center; display:flex; flex-direction:column; gap:10px;
116
- }}
117
- .card:hover {{ transform: translateY(-2px); box-shadow: 0 10px 28px rgba(0,0,0,.08); }}
118
-
119
- .thumb {{
120
- width: 100%;
121
- height: {CARD_THUMB_HEIGHT}px;
122
- border-radius:14px;
123
- border:1px solid rgba(0,0,0,.06);
124
- object-fit: contain; /* show full image */
125
- background:#fff;
126
- }}
127
-
128
- .card h3 {{ margin:6px 0 2px; }}
129
- .card p {{ color:#5a5f6a; min-height:30px; margin:0 8px 2px; }}
130
-
131
- /* Light button with dark text */
132
- .btn {{
133
- display:inline-block; padding:10px 14px; border-radius:12px;
134
- border:1px solid #e5e7eb; text-decoration:none;
135
- background:#f3f4f6; color:#111827; font-weight:500;
136
- }}
137
- .btn:hover {{ background:#e5e7eb; }}
138
-
139
- .footer {{ text-align:center; color:#6b7280; font-size:1.0em; margin-top: 6px; }}
140
- .footer hr {{ margin: 6px 0; }}
141
- </style>
142
- """,
143
- unsafe_allow_html=True,
144
- )
145
 
146
- # ========= IMAGE HELPERS =========
147
- def data_uri(path: Path) -> str | None:
148
- if not path.exists(): return None
 
149
  mime, _ = mimetypes.guess_type(path.name)
150
- if not mime: mime = "image/png"
 
151
  b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
152
  return f"data:{mime};base64,{b64}"
153
 
154
- def img_tag(path: Path, alt: str, cls: str) -> str:
155
  uri = data_uri(path)
156
- if uri:
157
- return f'<img class="{cls}" src="{uri}" alt="{escape(alt)}" />'
158
- return f'<div class="{cls}" style="display:flex;align-items:center;justify-content:center;color:#50545c;background:linear-gradient(180deg,#f6f7fb,#eceff4);">{escape(alt)}</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- # ========= HERO =========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  st.markdown(
162
- '<div class="hero">'
163
- + (img_tag(LOGO_PATH, "logo", "") if LOGO_PATH.exists() else "")
164
- + f"<h1>{escape(COMPANY_NAME)}</h1>"
165
- + f"<p>{escape(TAGLINE)}</p>"
166
- + "</div>",
 
167
  unsafe_allow_html=True,
168
  )
169
 
170
- # ========= CARDS =========
171
- def app_card(app: dict) -> str:
172
- target = "_blank" if OPEN_IN_NEW_TAB else "_self"
173
- style = f"--card-bg:{app.get('bg','#fff')}; --card-border:{app.get('border','rgba(0,0,0,.06)')};"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  return (
175
- f"<div class='card' style='{style}'>"
176
- + img_tag(app["thumb"], app["title"], "thumb")
177
- + f"<h3>{escape(app['title'])}</h3>"
178
- + f"<p>{escape(app['blurb'])}</p>"
179
- + f"<a class='btn' href='{escape(app['url'])}' target='{target}'>{escape(BUTTON_TEXT)}</a>"
180
  + "</div>"
181
  )
182
 
183
- # Render 3 cards side-by-side
184
- st.markdown("<div class='grid'>" + app_card(APP1) + app_card(APP2) + app_card(APP3) + "</div>", unsafe_allow_html=True)
185
 
186
- # ========= FOOTER (compact; no extra spacing) =========
187
  st.markdown(
188
  """
189
  <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
6
  import streamlit as st
7
 
8
+ # ========= PATHS =========
9
+ BASE_DIR = Path(__file__).parent
10
+ ASSETS = BASE_DIR / "assets"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # ========= META =========
13
+ st.set_page_config(page_title="ST_GeoMech SUITE", 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
+ # Premium header palette
25
+ "bg1": "#0B1220", # charcoal
26
+ "bg2": "#142136", # deep navy
27
+ "text": "#E4B83A", # gold text inside pill
28
+ "tagline_color": "#475569",
29
+ },
30
+ "page": {
31
+ "top_padding": 50, # px top padding for page
32
+ "container_width": 1120, # max content width
33
+ "bg_radial": True, # premium faint radial bg
34
+ },
35
+ "hero": {
36
+ "width": 400, # px
37
+ "margin_bottom": 30, # px space under hero
38
+ "logo": ASSETS / "AI_Suite_Log_logo.png",
39
+ },
40
+ "grid": {
41
+ "gap": 50, # px between cards
42
+ "card_width": 340, # default card width (px)
43
+ },
44
+ "card": {
45
+ "radius": 22, # px
46
+ "border_width": 2, # px
47
+ "pad_v": 24, # px
48
+ "pad_h": 20, # px
49
+ "border": "#0B1220", # dark outline
50
+ "border_hover": "#243447",
51
+ # defaults (overridden per card below)
52
+ "bg_top": "#FFFFFF",
53
+ "bg_bot": "#FBFCFE",
54
+ "title_color": "#0B1220",
55
+ "blurb_color": "#566275",
56
+ },
57
+ "icon": {
58
+ "diam": 118, # px outer circle
59
+ "img": 106, # px image
60
+ "circle_bg": "#F1F5F9",
61
+ "circle_border": "rgba(12,18,32,0.10)",
62
+ },
63
+ "button": {
64
+ "pad_v": 12, # px
65
+ "pad_h": 20, # px
66
+ "radius": 14, # px
67
+ # premium navy CTA
68
+ "bg1": "#0B1220",
69
+ "bg2": "#162338",
70
+ "text": "#FFFFFF",
71
+ "border": "rgba(11,18,32,.55)",
72
+ },
73
+ }
74
 
75
+ # ========= CARDS (content + per-card overrides) =========
76
+ # Solid, very-light tints for a premium feel (no washed-out rgba)
77
+ CARDS = [
78
+ {
79
+ "title": " ST_GeoMEch_UCE",
80
+ "blurb": "Real-time Uniconfined Compressive Strength Prediction.",
81
+ "url": "https://smart-thinking-ucs.hf.space/",
82
+ "icon": ASSETS / "UCS_logo.png",
83
+ "style": {
84
+ "bg_top": "#EAF7F1", # minty light
85
+ "bg_bot": "#F6FBF8",
86
+ "border": "#0F3D3E",
87
+ },
88
+ },
89
+ {
90
+ "title": " ST_GeoMech_Ym",
91
+ "blurb": "Real-time Static Young's Modulus Prediction.",
92
+ "url": "https://smart-thinking-ym.hf.space",
93
+ "icon": ASSETS / "Ym_logo.png",
94
+ "style": {
95
+ "bg_top": "#EAF7FD", # airy cyan
96
+ "bg_bot": "#F5FBFF",
97
+ "border": "#0E4A6E",
98
+ },
99
+ },
100
+ {
101
+ "title": " ST_GeoMech_SMW",
102
+ "blurb": "Real-Time Safe Mud Window Prediction",
103
+ "url": "https://smart-thinking-smw.hf.space",
104
+ "icon": ASSETS / "Tc_logo.png",
105
+ "style": {
106
+ "bg_top": "#EEF0FF", # soft indigo
107
+ "bg_bot": "#F7F8FF",
108
+ "border": "#3E4EB8",
109
+ },
110
+ },
111
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ # ========= Helpers =========
114
+ def data_uri(path: Path) -> Optional[str]:
115
+ if not path or not path.exists():
116
+ return None
117
  mime, _ = mimetypes.guess_type(path.name)
118
+ if not mime:
119
+ mime = "image/png"
120
  b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
121
  return f"data:{mime};base64,{b64}"
122
 
123
+ def img_tag(path: Path, alt: str, cls: str = "", style: str = "") -> str:
124
  uri = data_uri(path)
125
+ if not uri:
126
+ return ""
127
+ cls_attr = f' class="{cls}"' if cls else ""
128
+ style_attr = f' style="{style}"' if style else ""
129
+ return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />'
130
+
131
+ # ========= CSS (driven from THEME) =========
132
+ strip = THEME["strip"]; page = THEME["page"]; hero = THEME["hero"]
133
+ grid = THEME["grid"]; card = THEME["card"]; icon = THEME["icon"]; button = THEME["button"]
134
+
135
+ bg_radial_css = """
136
+ background:
137
+ radial-gradient(980px 460px at 50% -140px,
138
+ rgba(2,12,30,0.06) 0%,
139
+ rgba(2,12,30,0.04) 28%,
140
+ rgba(255,255,255,0.98) 60%,
141
+ rgba(255,255,255,1) 100%);
142
+ """ if page["bg_radial"] else "background: #fff;"
143
+
144
+ st.markdown(f"""
145
+ <style>
146
+ :root {{
147
+ --top-pad: {page["top_padding"]}px;
148
+
149
+ --strip-gap: {strip["gap"]}px;
150
+ --strip-row-mb: {strip["below_gap"]}px;
151
+ --strip-pill-pv: {strip["pill_pad_v"]}px;
152
+ --strip-pill-ph: {strip["pill_pad_h"]}px;
153
+ --strip-pill-fs: {strip["pill_font"]}px;
154
+ --tagline-fs: {strip["tagline_font"]}px;
155
+
156
+ --hero-w: {hero["width"]}px;
157
+ --hero-mb: {hero["margin_bottom"]}px;
158
+
159
+ --grid-gap: {grid["gap"]}px;
160
+ --card-w: {grid["card_width"]}px;
161
+ --card-r: {card["radius"]}px;
162
+ --card-bw: {card["border_width"]}px;
163
+ --card-pv: {card["pad_v"]}px;
164
+ --card-ph: {card["pad_h"]}px;
165
+
166
+ --icon-d: {icon["diam"]}px;
167
+ --icon-img: {icon["img"]}px;
168
+
169
+ --stripBg1: {strip["bg1"]};
170
+ --stripBg2: {strip["bg2"]};
171
+ --stripText: {strip["text"]};
172
+ --taglineColor: {strip["tagline_color"]};
173
 
174
+ --cardStroke: {card["border"]};
175
+ --cardStrokeHover: {card["border_hover"]};
176
+
177
+ --btn1: {button["bg1"]};
178
+ --btn2: {button["bg2"]};
179
+ --btnText: {button["text"]};
180
+ --btnBorder: {button["border"]};
181
+ }}
182
+
183
+ html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
184
+ [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
185
+
186
+ .block-container {{
187
+ max-width: {page["container_width"]}px;
188
+ min-height: 100vh;
189
+ display: flex; flex-direction: column;
190
+ gap: 14px;
191
+ padding: var(--top-pad) 0 28px !important;
192
+ {bg_radial_css}
193
+ }}
194
+
195
+ /* ===== TOP-LEFT STRIP ===== */
196
+ .suite-row {{
197
+ display:flex; align-items:center; gap: var(--strip-gap);
198
+ justify-content:flex-start; flex-wrap: wrap;
199
+ margin: 0 0 var(--strip-row-mb) 0;
200
+ }}
201
+ .suite-pill {{
202
+ background: linear-gradient(90deg, var(--stripBg1) 0%, var(--stripBg2) 100%);
203
+ color: var(--stripText);
204
+ padding: var(--strip-pill-pv) var(--strip-pill-ph);
205
+ border-radius: 999px;
206
+ font-weight: 800; letter-spacing: .25px;
207
+ font-size: var(--strip-pill-fs);
208
+ box-shadow: 0 6px 14px rgba(2,12,30,.18);
209
+ white-space: nowrap;
210
+ }}
211
+ .suite-tagline {{
212
+ color: var(--taglineColor); font-weight: 600; opacity: .95;
213
+ font-size: var(--tagline-fs);
214
+ white-space: nowrap;
215
+ }}
216
+
217
+ /* ===== HERO ===== */
218
+ .hero {{ text-align:center; margin: 0 0 var(--hero-mb); }}
219
+ .hero img {{
220
+ width: var(--hero-w); max-width: 92vw; height: auto;
221
+ display:block; margin: 0 auto var(--hero-mb);
222
+ filter: drop-shadow(0 6px 16px rgba(0,0,0,.10));
223
+ }}
224
+
225
+ /* ===== GRID ===== */
226
+ .grid {{
227
+ display: grid;
228
+ grid-template-columns: repeat(3, var(--card-w));
229
+ gap: var(--grid-gap);
230
+ justify-content: center; align-items: stretch;
231
+ margin-top: 10px;
232
+ }}
233
+ @media (max-width: 1120px) {{
234
+ .grid {{ grid-template-columns: repeat(2, var(--card-w)); gap: calc(var(--grid-gap) - 12px); }}
235
+ }}
236
+ @media (max-width: 720px) {{
237
+ .grid {{ grid-template-columns: 1fr; }}
238
+ }}
239
+
240
+ /* ===== CARD ===== */
241
+ .card {{
242
+ position: relative;
243
+ width: var(--c-w, var(--card-w));
244
+ border-radius: var(--c-radius, var(--card-r));
245
+ padding: var(--c-pv, var(--card-pv)) var(--c-ph, var(--card-ph));
246
+ background: linear-gradient(180deg, var(--c-bg-top, {card["bg_top"]}) 0%, var(--c-bg-bot, {card["bg_bot"]}) 100%);
247
+ border: var(--c-bw, var(--card-bw)) solid var(--c-border, var(--cardStroke));
248
+ /* Softer inner highlights → color shows more, keeps 3D */
249
+ box-shadow:
250
+ 0 14px 32px rgba(2,20,35,.12),
251
+ 0 1px 0 rgba(255,255,255,0.70) inset,
252
+ 0 -1px 6px rgba(255,255,255,0.18) inset;
253
+ transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;
254
+ text-align:center; display:flex; flex-direction:column; gap:16px; align-items: center;
255
+ }}
256
+ .card:hover {{
257
+ transform: translateY(-6px) scale(1.01);
258
+ border-color: var(--cardStrokeHover);
259
+ box-shadow:
260
+ 0 22px 56px rgba(2,20,35,.18),
261
+ 0 1px 0 rgba(255,255,255,0.82) inset,
262
+ 0 -1px 10px rgba(255,255,255,0.28) inset;
263
+ filter: saturate(1.03);
264
+ }}
265
+
266
+ .icon-wrap {{
267
+ width: var(--c-icon-d, {icon["diam"]}px); height: var(--c-icon-d, {icon["diam"]}px);
268
+ border-radius: 9999px; display:grid; place-items:center;
269
+ background: var(--c-icon-bg, {icon["circle_bg"]});
270
+ border: 1px solid var(--c-icon-border, {icon["circle_border"]});
271
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 10px 22px rgba(2,20,35,.07);
272
+ }}
273
+ .icon-wrap img {{
274
+ width: var(--c-icon-img, {icon["img"]}px); height: var(--c-icon-img, {icon["img"]}px);
275
+ border-radius:9999px; display:block;
276
+ }}
277
+
278
+ .card h3 {{
279
+ margin: 0; font-size: 25px; font-weight: 900;
280
+ color: {card["title_color"]}; letter-spacing: .15px;
281
+ }}
282
+ .card p {{
283
+ color: {card["blurb_color"]}; min-height: 40px; margin: 0 10px; font-size: 16px;
284
+ }}
285
+
286
+ .btn {{
287
+ display:inline-block; padding: {button["pad_v"]}px {button["pad_h"]}px;
288
+ border-radius: {button["radius"]}px;
289
+ border: 1px solid {button["border"]};
290
+ background: linear-gradient(180deg, {button["bg1"]} 0%, {button["bg2"]} 100%);
291
+ font-weight: 800; letter-spacing:.3px; font-size: 16px;
292
+ margin: 6px auto 0;
293
+ box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10);
294
+ color: {button["text"]};
295
+ text-decoration: none;
296
+ }}
297
+ a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{
298
+ color: {button["text"]} !important; text-decoration: none !important;
299
+ }}
300
+ .btn:hover {{
301
+ filter: brightness(1.03) saturate(1.04);
302
+ transform: translateY(-1px);
303
+ box-shadow: 0 16px 38px rgba(11,18,32,.36);
304
+ }}
305
+
306
+ .footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 26px; }}
307
+ .footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }}
308
+ </style>
309
+ """, unsafe_allow_html=True)
310
+
311
+ # ========= Strip (top-left) =========
312
  st.markdown(
313
+ f"""
314
+ <div class="suite-row">
315
+ <span class="suite-pill">{"ST_LOG SUITE"}</span>
316
+ <span class="suite-tagline">{"Generating AI-Based Well Logging Profiles While Drilling"}</span>
317
+ </div>
318
+ """,
319
  unsafe_allow_html=True,
320
  )
321
 
322
+ # ========= Hero =========
323
+ hero_html = img_tag(THEME["hero"]["logo"], "ST_LOG SUITE")
324
+ st.markdown(f"<div class='hero'>{hero_html}</div>", unsafe_allow_html=True)
325
+
326
+ # ========= Cards =========
327
+ def build_card_vars(style: Dict[str, Any]) -> str:
328
+ s = []
329
+ if "width" in style: s.append(f"--c-w:{int(style['width'])}px")
330
+ if "radius" in style: s.append(f"--c-radius:{int(style['radius'])}px")
331
+ if "pad_v" in style: s.append(f"--c-pv:{int(style['pad_v'])}px")
332
+ if "pad_h" in style: s.append(f"--c-ph:{int(style['pad_h'])}px")
333
+ if "bg_top" in style: s.append(f"--c-bg-top:{style['bg_top']}")
334
+ if "bg_bot" in style: s.append(f"--c-bg-bot:{style['bg_bot']}")
335
+ if "border" in style: s.append(f"--c-border:{style['border']}")
336
+ if "border_width" in style: s.append(f"--c-bw:{int(style['border_width'])}px")
337
+ if "title_color" in style: s.append(f"--c-title-color:{style['title_color']}")
338
+ if "title_fs" in style: s.append(f"--c-title-fs:{int(style['title_fs'])}px")
339
+ if "blurb_color" in style: s.append(f"--c-blurb-color:{style['blurb_color']}")
340
+ if "blurb_fs" in style: s.append(f"--c-blurb-fs:{int(style['blurb_fs'])}px")
341
+ if "btn_bg1" in style: s.append(f"--c-btn-bg1:{style['btn_bg1']}")
342
+ if "btn_bg2" in style: s.append(f"--c-btn-bg2:{style['btn_bg2']}")
343
+ if "btn_text" in style: s.append(f"--c-btn-text:{style['btn_text']}")
344
+ if "btn_border" in style: s.append(f"--c-btn-border:{style['btn_border']}")
345
+ if "btn_fs" in style: s.append(f"--c-btn-fs:{int(style['btn_fs'])}px")
346
+ if "btn_pad_v" in style: s.append(f"--c-btn-pv:{int(style['btn_pad_v'])}px")
347
+ if "btn_pad_h" in style: s.append(f"--c-btn-ph:{int(style['btn_pad_h'])}px")
348
+ if "icon_diam" in style: s.append(f"--c-icon-d:{int(style['icon_diam'])}px")
349
+ if "icon_img" in style: s.append(f"--c-icon-img:{int(style['icon_img'])}px")
350
+ if "icon_bg" in style: s.append(f"--c-icon-bg:{style['icon_bg']}")
351
+ if "icon_border" in style: s.append(f"--c-icon-border:{style['icon_border']}")
352
+ return "; ".join(s)
353
+
354
+ def app_card(card_cfg: Dict[str, Any]) -> str:
355
+ style = card_cfg.get("style", {})
356
+ vars_inline = build_card_vars(style)
357
+ icon_html = img_tag(card_cfg.get("icon"), "icon") if card_cfg.get("icon") and card_cfg["icon"].exists() else ""
358
+ target = "_self"
359
  return (
360
+ f"<div class='card' style='{vars_inline}'>"
361
+ + f"<div class='icon-wrap'>{icon_html}</div>"
362
+ + f"<h3>{escape(card_cfg['title'])}</h3>"
363
+ + f"<p>{escape(card_cfg['blurb'])}</p>"
364
+ + f"<a class='btn' href='{escape(card_cfg['url'])}' target='{target}' rel='noopener'>Run App</a>"
365
  + "</div>"
366
  )
367
 
368
+ st.markdown("<div class='grid'>" + "".join(app_card(c) for c in CARDS) + "</div>", unsafe_allow_html=True)
 
369
 
370
+ # ========= Footer =========
371
  st.markdown(
372
  """
373
  <hr>