UCS2014 commited on
Commit
9af3791
·
verified ·
1 Parent(s): a2c5b92

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +147 -198
app.py CHANGED
@@ -10,124 +10,108 @@ BASE_DIR = Path(__file__).parent
10
  ASSETS = BASE_DIR / "assets"
11
 
12
  # ========= META =========
13
- st.set_page_config(page_title="ST_LOG 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
- # GOLD header palette (high contrast)
25
- "bg1": "#D4AF37", # gold
26
- "bg2": "#B88917", # deeper gold
27
- "text": "#000052", # dark text in pill for contrast
28
- "tagline_color": "#000000",
29
- },
30
  "page": {
31
- "top_padding": 50,
32
- "container_width": 1120,
33
- "bg_radial": True,
 
 
34
  },
35
  "hero": {
36
- "width": 400,
37
- "margin_bottom": 30,
38
- "logo": ASSETS / "AI_Suite_Log_logo.png",
 
 
 
 
 
 
39
  },
40
  "grid": {
41
- "gap": 50,
42
- "card_width": 340,
 
43
  },
44
  "card": {
45
- "radius": 22,
46
- "border_width": 2,
47
- "pad_v": 24,
48
- "pad_h": 20,
49
- "border": "#0B1220",
50
  "border_hover": "#243447",
51
- "bg_top": "#FFFFFF",
52
- "bg_bot": "#FBFCFE",
 
 
 
 
53
  "title_color": "#0B1220",
 
 
54
  "blurb_color": "#566275",
55
- },
56
- "icon": {
57
- "diam": 118,
58
- "img": 106,
59
- "circle_bg": "#F1F5F9",
60
- "circle_border": "rgba(12,18,32,0.10)",
61
  },
62
  "button": {
63
- "pad_v": 12,
64
- "pad_h": 20,
65
- "radius": 14,
66
- "bg1": "#0B1220",
67
- "bg2": "#162338",
68
- "text": "#FFFFFF",
69
- "border": "rgba(11,18,32,.55)",
70
  },
71
  }
72
 
73
- # ========= CARDS =========
 
74
  CARDS = [
75
  {
76
- "title": " ST_Log_GR",
77
- "blurb": "Real-time gamma-ray log prediction.",
78
- "url": "https://smart-thinking-gr.hf.space/",
79
- "icon": ASSETS / "GR_logo.png",
80
- "style": {
81
- "bg_top": "#EAF7F1",
82
- "bg_bot": "#F6FBF8",
83
- "border": "#0F3D3E",
84
- },
85
- },
86
- {
87
- "title": " ST_Log_Sonic (Ts)",
88
- "blurb": "Predict shear slowness (DtS) in real time.",
89
- "url": "https://smart-thinking-sonic-ts.hf.space",
90
- "icon": ASSETS / "Ts_logo.png",
91
  "style": {
92
- "bg_top": "#EAF7FD",
93
- "bg_bot": "#F5FBFF",
94
  "border": "#0E4A6E",
95
  },
96
  },
97
  {
98
- "title": " ST_Log_Sonic (Tc)",
99
- "blurb": "Predict compressional slowness (DtC) in real time.",
100
- "url": "https://smart-thinking-sonic-tc.hf.space",
101
- "icon": ASSETS / "Tc_logo.png",
102
  "style": {
103
- "bg_top": "#EEF0FF",
104
- "bg_bot": "#F7F8FF",
105
- "border": "#3E4EB8",
106
  },
107
  },
108
  ]
109
 
110
  # ========= Helpers =========
111
  def data_uri(path: Path) -> Optional[str]:
112
- if not path or not path.exists():
113
- return None
114
  mime, _ = mimetypes.guess_type(path.name)
115
- if not mime:
116
- mime = "image/png"
117
  b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
118
  return f"data:{mime};base64,{b64}"
119
 
120
  def img_tag(path: Path, alt: str, cls: str = "", style: str = "") -> str:
121
  uri = data_uri(path)
122
- if not uri:
123
- return ""
124
  cls_attr = f' class="{cls}"' if cls else ""
125
  style_attr = f' style="{style}"' if style else ""
126
  return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />'
127
 
128
- # ========= CSS =========
129
- strip = THEME["strip"]; page = THEME["page"]; hero = THEME["hero"]
130
- grid = THEME["grid"]; card = THEME["card"]; icon = THEME["icon"]; button = THEME["button"]
131
 
132
  bg_radial_css = """
133
  background:
@@ -141,96 +125,86 @@ background:
141
  st.markdown(f"""
142
  <style>
143
  :root {{
 
144
  --top-pad: {page["top_padding"]}px;
145
-
146
- --strip-gap: {strip["gap"]}px;
147
- --strip-row-mb: {strip["below_gap"]}px;
148
- --strip-pill-pv: {strip["pill_pad_v"]}px;
149
- --strip-pill-ph: {strip["pill_pad_h"]}px;
150
- --strip-pill-fs: {strip["pill_font"]}px;
151
- --tagline-fs: {strip["tagline_font"]}px;
152
 
153
  --hero-w: {hero["width"]}px;
154
- --hero-mb: {hero["margin_bottom"]}px;
 
 
 
 
155
 
156
  --grid-gap: {grid["gap"]}px;
 
157
  --card-w: {grid["card_width"]}px;
 
158
  --card-r: {card["radius"]}px;
159
  --card-bw: {card["border_width"]}px;
 
 
 
 
160
  --card-pv: {card["pad_v"]}px;
161
  --card-ph: {card["pad_h"]}px;
 
 
 
 
 
162
 
163
- --icon-d: {icon["diam"]}px;
164
- --icon-img: {icon["img"]}px;
165
-
166
- --stripBg1: {strip["bg1"]};
167
- --stripBg2: {strip["bg2"]};
168
- --stripText: {strip["text"]};
169
- --taglineColor: {strip["tagline_color"]};
170
-
171
- --cardStroke: {card["border"]};
172
- --cardStrokeHover: {card["border_hover"]};
173
 
174
  --btn1: {button["bg1"]};
175
  --btn2: {button["bg2"]};
176
  --btnText: {button["text"]};
177
  --btnBorder: {button["border"]};
 
 
 
 
 
178
  }}
179
 
180
  html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
181
  [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
182
 
183
  .block-container {{
184
- max-width: {page["container_width"]}px;
185
  min-height: 100vh;
186
  display: flex; flex-direction: column;
187
- gap: 14px;
188
- padding: var(--top-pad) 0 28px !important;
189
  {bg_radial_css}
190
  }}
191
 
192
- /* ===== TOP-LEFT STRIP ===== */
193
- .suite-row {{
194
- display:flex; align-items:center; gap: var(--strip-gap);
195
- justify-content:flex-start; flex-wrap: wrap;
196
- margin: 0 0 var(--strip-row-mb) 0;
197
- }}
198
- .suite-pill {{
199
- background: linear-gradient(90deg, var(--stripBg1) 0%, var(--stripBg2) 100%);
200
- color: var(--stripText);
201
- padding: var(--strip-pill-pv) var(--strip-pill-ph);
202
- border-radius: 999px;
203
- font-weight: 800; letter-spacing: .25px;
204
- font-size: var(--strip-pill-fs);
205
- box-shadow: 0 6px 14px rgba(2,12,30,.18);
206
- white-space: nowrap;
207
- }}
208
- .suite-tagline {{
209
- color: var(--taglineColor); font-weight: 600; opacity: .95;
210
- font-size: var(--tagline-fs);
211
- white-space: nowrap;
212
- }}
213
-
214
- /* ===== HERO ===== */
215
- .hero {{ text-align:center; margin: 0 0 var(--hero-mb); }}
216
  .hero img {{
217
  width: var(--hero-w); max-width: 92vw; height: auto;
218
- display:block; margin: 0 auto var(--hero-mb);
219
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.10));
220
  }}
 
 
 
 
 
 
 
221
 
222
- /* ===== GRID ===== */
223
  .grid {{
224
  display: grid;
225
- grid-template-columns: repeat(3, var(--card-w));
226
  gap: var(--grid-gap);
227
  justify-content: center; align-items: stretch;
228
- margin-top: 10px;
229
- }}
230
- @media (max-width: 1120px) {{
231
- .grid {{ grid-template-columns: repeat(2, var(--card-w)); gap: calc(var(--grid-gap) - 12px); }}
232
  }}
233
- @media (max-width: 720px) {{
234
  .grid {{ grid-template-columns: 1fr; }}
235
  }}
236
 
@@ -240,93 +214,79 @@ st.markdown(f"""
240
  width: var(--c-w, var(--card-w));
241
  border-radius: var(--c-radius, var(--card-r));
242
  padding: var(--c-pv, var(--card-pv)) var(--c-ph, var(--card-ph));
243
- background: linear-gradient(180deg, var(--c-bg-top, {card["bg_top"]}) 0%, var(--c-bg-bot, {card["bg_bot"]}) 100%);
244
- border: var(--c-bw, var(--card-bw)) solid var(--c-border, var(--cardStroke));
245
- box-shadow:
246
- 0 14px 32px rgba(2,20,35,.12),
247
- 0 1px 0 rgba(255,255,255,0.70) inset,
248
- 0 -1px 6px rgba(255,255,255,0.18) inset;
249
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;
250
- text-align:center; display:flex; flex-direction:column; gap:16px; align-items: center;
251
  }}
252
  .card:hover {{
253
  transform: translateY(-6px) scale(1.01);
254
- border-color: var(--cardStrokeHover);
255
- box-shadow:
256
- 0 22px 56px rgba(2,20,35,.18),
257
- 0 1px 0 rgba(255,255,255,.82) inset,
258
- 0 -1px 10px rgba(255,255,255,.28) inset;
259
  filter: saturate(1.03);
260
  }}
261
 
262
- /* ===== ICON / LOGO — self-contained, not trimmed ===== */
263
- .icon-wrap {{
264
- width: var(--c-icon-d, {icon["diam"]}px); height: var(--c-icon-d, {icon["diam"]}px);
265
- border-radius: 9999px; display:grid; place-items:center;
266
- background: var(--c-icon-bg, {icon["circle_bg"]});
267
- border: 1px solid var(--c-icon-border, {icon["circle_border"]});
268
- box-shadow: inset 0 1px 0 rgba(255,255,255,.95), 0 10px 22px rgba(2,20,35,.07);
269
- }}
270
- .icon-wrap img {{
271
- width: calc(var(--c-icon-img, {icon["img"]}px) - 12px);
272
- height: calc(var(--c-icon-img, {icon["img"]}px) - 12px);
273
- padding: 6px;
274
- box-sizing: border-box;
275
- object-fit: contain; /* never crop */
276
- background: #FFFFFF; /* clean canvas for mixed logos */
277
- border: 2px solid var(--c-border, var(--cardStroke)); /* match card border */
278
- border-radius: 14px;
279
  display:block;
280
  }}
281
 
282
  .card h3 {{
283
- margin: 0; font-size: 25px; font-weight: 900;
284
- color: {card["title_color"]}; letter-spacing: .15px;
 
 
 
285
  }}
286
  .card p {{
287
- color: {card["blurb_color"]}; min-height: 40px; margin: 0 10px; font-size: 16px;
 
 
288
  }}
289
 
290
  .btn {{
291
- display:inline-block; padding: {button["pad_v"]}px {button["pad_h"]}px;
292
- border-radius: {button["radius"]}px;
293
- border: 1px solid {button["border"]};
294
- background: linear-gradient(180deg, {button["bg1"]} 0%, {button["bg2"]} 100%);
295
- font-weight: 800; letter-spacing:.3px; font-size: 16px;
296
- margin: 6px auto 0;
 
 
297
  box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10);
298
- color: {button["text"]};
299
  text-decoration: none;
300
  }}
301
  a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{
302
- color: {button["text"]} !important; text-decoration: none !important;
303
- }}
304
- .btn:hover {{
305
- filter: brightness(1.03) saturate(1.04);
306
- transform: translateY(-1px);
307
- box-shadow: 0 16px 38px rgba(11,18,32,.36);
308
  }}
 
309
 
310
- .footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 26px; }}
311
  .footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }}
312
  </style>
313
  """, unsafe_allow_html=True)
314
 
315
- # ========= Strip (top-left) =========
 
316
  st.markdown(
317
  f"""
318
- <div class="suite-row">
319
- <span class="suite-pill">{"ST_LOG SUITE"}</span>
320
- <span class="suite-tagline">{"Generating AI-Based Well Logging Profiles While Drilling"}</span>
321
  </div>
322
  """,
323
  unsafe_allow_html=True,
324
  )
325
 
326
- # ========= Hero =========
327
- hero_html = img_tag(THEME["hero"]["logo"], "ST_LOG SUITE")
328
- st.markdown(f"<div class='hero'>{hero_html}</div>", unsafe_allow_html=True)
329
-
330
  def build_card_vars(style: Dict[str, Any]) -> str:
331
  s = []
332
  if "width" in style: s.append(f"--c-w:{int(style['width'])}px")
@@ -341,30 +301,19 @@ def build_card_vars(style: Dict[str, Any]) -> str:
341
  if "title_fs" in style: s.append(f"--c-title-fs:{int(style['title_fs'])}px")
342
  if "blurb_color" in style: s.append(f"--c-blurb-color:{style['blurb_color']}")
343
  if "blurb_fs" in style: s.append(f"--c-blurb-fs:{int(style['blurb_fs'])}px")
344
- if "btn_bg1" in style: s.append(f"--c-btn-bg1:{style['btn_bg1']}")
345
- if "btn_bg2" in style: s.append(f"--c-btn-bg2:{style['btn_bg2']}")
346
- if "btn_text" in style: s.append(f"--c-btn-text:{style['btn_text']}")
347
- if "btn_border" in style: s.append(f"--c-btn-border:{style['btn_border']}")
348
- if "btn_fs" in style: s.append(f"--c-btn-fs:{int(style['btn_fs'])}px")
349
- if "btn_pad_v" in style: s.append(f"--c-btn-pv:{int(style['btn_pad_v'])}px")
350
- if "btn_pad_h" in style: s.append(f"--c-btn-ph:{int(style['btn_pad_h'])}px")
351
- if "icon_diam" in style: s.append(f"--c-icon-d:{int(style['icon_diam'])}px")
352
- if "icon_img" in style: s.append(f"--c-icon-img:{int(style['icon_img'])}px")
353
- if "icon_bg" in style: s.append(f"--c-icon-bg:{style['icon_bg']}")
354
- if "icon_border" in style: s.append(f"--c-icon-border:{style['icon_border']}")
355
  return "; ".join(s)
356
 
357
  def app_card(card_cfg: Dict[str, Any]) -> str:
358
  style = card_cfg.get("style", {})
359
  vars_inline = build_card_vars(style)
360
- icon_html = img_tag(card_cfg.get("icon"), "icon") if card_cfg.get("icon") and card_cfg["icon"].exists() else ""
361
  target = "_self"
362
  return (
363
  f"<div class='card' style='{vars_inline}'>"
364
- + f"<div class='icon-wrap'>{icon_html}</div>"
365
  + f"<h3>{escape(card_cfg['title'])}</h3>"
366
  + f"<p>{escape(card_cfg['blurb'])}</p>"
367
- + f"<a class='btn' href='{escape(card_cfg['url'])}' target='{target}' rel='noopener'>Run App</a>"
368
  + "</div>"
369
  )
370
 
 
10
  ASSETS = BASE_DIR / "assets"
11
 
12
  # ========= META =========
13
+ st.set_page_config(page_title="Smart Thinking — AI for O&G", layout="wide")
14
 
15
+ # ========= GLOBAL THEME (all knobs live here) =========
16
  THEME: Dict[str, Any] = {
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  "page": {
18
+ "container_width": 1120, # max content width (px)
19
+ "bg_radial": True, # faint radial background
20
+ "section_gap": 48, # px between hero and cards section
21
+ "footer_gap": 36, # px space before footer
22
+ "top_padding": 40, # top padding of the page
23
  },
24
  "hero": {
25
+ # Company logo in the middle
26
+ "logo": ASSETS / "company_logo.png", # <— set your company logo file here
27
+ "width": 360, # rendered pixel width of hero logo
28
+ "tagline": "We Deliver AI-Based Solutions For O&G Industry",
29
+ "tagline_size": 22, # px
30
+ "tagline_weight": 800, # 600/700/800 etc.
31
+ "tagline_color": "#0B1220",
32
+ "logo_bottom": 18, # px space between logo and tagline
33
+ "block_bottom": 28, # px space below the whole hero block
34
  },
35
  "grid": {
36
+ "columns": 2, # number of cards per row (auto-wrap on narrow screens)
37
+ "gap": 40, # px gap between cards
38
+ "card_width": 460, # preferred card width (px)
39
  },
40
  "card": {
41
+ "radius": 22, # px
42
+ "border_width": 2, # px
43
+ "border": "#0B1220", # card outline
 
 
44
  "border_hover": "#243447",
45
+ "bg_top": "#FFFFFF", # gradient top
46
+ "bg_bot": "#FBFCFE", # gradient bottom
47
+ "pad_v": 24, # vertical padding inside card
48
+ "pad_h": 22, # horizontal padding inside card
49
+
50
+ # Title & paragraph
51
  "title_color": "#0B1220",
52
+ "title_size": 26,
53
+ "title_weight": 900,
54
  "blurb_color": "#566275",
55
+ "blurb_size": 16,
56
+
57
+ # Card logo controls (no frames/borders)
58
+ "logo_max_w": 140, # max width of card logo (px)
59
+ "logo_top": 6, # px space above card logo
60
+ "logo_bottom": 8, # px space below card logo
61
  },
62
  "button": {
63
+ "pad_v": 12, "pad_h": 20, "radius": 14,
64
+ "bg1": "#0B1220", "bg2": "#162338",
65
+ "text": "#FFFFFF", "border": "rgba(11,18,32,.55)",
66
+ "font_size": 16, "font_weight": 800,
 
 
 
67
  },
68
  }
69
 
70
+ # ========= CARDS (content + per-card overrides) =========
71
+ # Supply your own icons in /assets and URLs below
72
  CARDS = [
73
  {
74
+ "title": "Geomechanics",
75
+ "blurb": "Predict Rock Mechanical Behaviour Using AI",
76
+ "url": "https://smart-thinking-smw.hf.space", # example target
77
+ "icon": ASSETS / "geomech_logo.png", # <— card logo file
 
 
 
 
 
 
 
 
 
 
 
78
  "style": {
79
+ "bg_top": "#EEF7FF",
80
+ "bg_bot": "#F7FBFF",
81
  "border": "#0E4A6E",
82
  },
83
  },
84
  {
85
+ "title": "Logging",
86
+ "blurb": "Generate Synthetic Logging Profiles Using AI",
87
+ "url": "https://smart-thinking-gr.hf.space", # example target
88
+ "icon": ASSETS / "logging_logo.png", # <— card logo file
89
  "style": {
90
+ "bg_top": "#F8FFF4",
91
+ "bg_bot": "#FBFFF8",
92
+ "border": "#0F3D3E",
93
  },
94
  },
95
  ]
96
 
97
  # ========= Helpers =========
98
  def data_uri(path: Path) -> Optional[str]:
99
+ if not path or not path.exists(): return None
 
100
  mime, _ = mimetypes.guess_type(path.name)
101
+ if not mime: mime = "image/png"
 
102
  b64 = base64.b64encode(path.read_bytes()).decode("utf-8")
103
  return f"data:{mime};base64,{b64}"
104
 
105
  def img_tag(path: Path, alt: str, cls: str = "", style: str = "") -> str:
106
  uri = data_uri(path)
107
+ if not uri: return ""
 
108
  cls_attr = f' class="{cls}"' if cls else ""
109
  style_attr = f' style="{style}"' if style else ""
110
  return f'<img{cls_attr}{style_attr} src="{uri}" alt="{escape(alt)}" />'
111
 
112
+ # ========= CSS (driven by THEME) =========
113
+ page = THEME["page"]; hero = THEME["hero"]
114
+ grid = THEME["grid"]; card = THEME["card"]; button = THEME["button"]
115
 
116
  bg_radial_css = """
117
  background:
 
125
  st.markdown(f"""
126
  <style>
127
  :root {{
128
+ --container-w: {page["container_width"]}px;
129
  --top-pad: {page["top_padding"]}px;
130
+ --section-gap: {page["section_gap"]}px;
131
+ --footer-gap: {page["footer_gap"]}px;
 
 
 
 
 
132
 
133
  --hero-w: {hero["width"]}px;
134
+ --hero-logo-mb: {hero["logo_bottom"]}px;
135
+ --hero-block-mb: {hero["block_bottom"]}px;
136
+ --hero-tag-fs: {hero["tagline_size"]}px;
137
+ --hero-tag-weight: {hero["tagline_weight"]};
138
+ --hero-tag-color: {hero["tagline_color"]};
139
 
140
  --grid-gap: {grid["gap"]}px;
141
+ --grid-cols: {grid["columns"]};
142
  --card-w: {grid["card_width"]}px;
143
+
144
  --card-r: {card["radius"]}px;
145
  --card-bw: {card["border_width"]}px;
146
+ --card-border: {card["border"]};
147
+ --card-border-hover: {card["border_hover"]};
148
+ --card-bg-top: {card["bg_top"]};
149
+ --card-bg-bot: {card["bg_bot"]};
150
  --card-pv: {card["pad_v"]}px;
151
  --card-ph: {card["pad_h"]}px;
152
+ --card-title-color: {card["title_color"]};
153
+ --card-title-size: {card["title_size"]}px;
154
+ --card-title-weight: {card["title_weight"]};
155
+ --card-blurb-color: {card["blurb_color"]};
156
+ --card-blurb-size: {card["blurb_size"]}px;
157
 
158
+ --logo-max-w: {card["logo_max_w"]}px;
159
+ --logo-top: {card["logo_top"]}px;
160
+ --logo-bottom: {card["logo_bottom"]}px;
 
 
 
 
 
 
 
161
 
162
  --btn1: {button["bg1"]};
163
  --btn2: {button["bg2"]};
164
  --btnText: {button["text"]};
165
  --btnBorder: {button["border"]};
166
+ --btnFS: {button["font_size"]}px;
167
+ --btnFW: {button["font_weight"]};
168
+ --btnPV: {button["pad_v"]}px;
169
+ --btnPH: {button["pad_h"]}px;
170
+ --btnRadius: {button["radius"]}px;
171
  }}
172
 
173
  html, body, [data-testid="stAppViewContainer"] {{ height: 100%; }}
174
  [data-testid="stAppViewContainer"] > .main {{ padding-top: 0 !important; padding-bottom: 0 !important; }}
175
 
176
  .block-container {{
177
+ max-width: var(--container-w);
178
  min-height: 100vh;
179
  display: flex; flex-direction: column;
180
+ gap: var(--section-gap);
181
+ padding: var(--top-pad) 0 var(--footer-gap) !important;
182
  {bg_radial_css}
183
  }}
184
 
185
+ /* ===== HERO (centered company logo + tagline) ===== */
186
+ .hero {{ text-align:center; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  .hero img {{
188
  width: var(--hero-w); max-width: 92vw; height: auto;
189
+ display:block; margin: 0 auto var(--hero-logo-mb);
190
  filter: drop-shadow(0 6px 16px rgba(0,0,0,.10));
191
  }}
192
+ .hero .tagline {{
193
+ font-size: var(--hero-tag-fs);
194
+ font-weight: var(--hero-tag-weight);
195
+ color: var(--hero-tag-color);
196
+ letter-spacing:.2px;
197
+ margin-bottom: var(--hero-block-mb);
198
+ }}
199
 
200
+ /* ===== GRID (two cards) ===== */
201
  .grid {{
202
  display: grid;
203
+ grid-template-columns: repeat(var(--grid-cols), var(--card-w));
204
  gap: var(--grid-gap);
205
  justify-content: center; align-items: stretch;
 
 
 
 
206
  }}
207
+ @media (max-width: 1040px) {{
208
  .grid {{ grid-template-columns: 1fr; }}
209
  }}
210
 
 
214
  width: var(--c-w, var(--card-w));
215
  border-radius: var(--c-radius, var(--card-r));
216
  padding: var(--c-pv, var(--card-pv)) var(--c-ph, var(--card-ph));
217
+ background: linear-gradient(180deg, var(--c-bg-top, var(--card-bg-top)) 0%, var(--c-bg-bot, var(--card-bg-bot)) 100%);
218
+ border: var(--c-bw, var(--card-bw)) solid var(--c-border, var(--card-border));
219
+ box-shadow: 0 14px 32px rgba(2,20,35,.10), 0 1px 0 rgba(255,255,255,0.75) inset;
 
 
 
220
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease, filter .18s ease;
221
+ text-align:center; display:flex; flex-direction:column; gap:16px; align-items:center;
222
  }}
223
  .card:hover {{
224
  transform: translateY(-6px) scale(1.01);
225
+ border-color: var(--card-border-hover);
226
+ box-shadow: 0 22px 56px rgba(2,20,35,.18);
 
 
 
227
  filter: saturate(1.03);
228
  }}
229
 
230
+ /* Plain logo (NO frame/border) */
231
+ .card .logo-img {{
232
+ margin-top: var(--logo-top);
233
+ margin-bottom: var(--logo-bottom);
234
+ max-width: var(--logo-max-w);
235
+ width: 100%;
236
+ height: auto;
237
+ object-fit: contain;
 
 
 
 
 
 
 
 
 
238
  display:block;
239
  }}
240
 
241
  .card h3 {{
242
+ margin: 0;
243
+ font-size: var(--card-title-size);
244
+ font-weight: var(--card-title-weight);
245
+ color: var(--c-title-color, var(--card-title-color));
246
+ letter-spacing: .15px;
247
  }}
248
  .card p {{
249
+ color: var(--c-blurb-color, var(--card-blurb-color));
250
+ font-size: var(--card-blurb-size);
251
+ margin: 0 6px;
252
  }}
253
 
254
  .btn {{
255
+ display:inline-block;
256
+ padding: var(--btnPV) var(--btnPH);
257
+ border-radius: var(--btnRadius);
258
+ border: 1px solid var(--btnBorder);
259
+ background: linear-gradient(180deg, var(--btn1) 0%, var(--btn2) 100%);
260
+ font-weight: var(--btnFW);
261
+ font-size: var(--btnFS);
262
+ letter-spacing:.3px;
263
  box-shadow: 0 12px 28px rgba(11,18,32,.28), inset 0 1px 0 rgba(255,255,255,.10);
264
+ color: var(--btnText);
265
  text-decoration: none;
266
  }}
267
  a.btn, a.btn:link, a.btn:visited, a.btn:hover, a.btn:active, a.btn:focus {{
268
+ color: var(--btnText) !important; text-decoration: none !important;
 
 
 
 
 
269
  }}
270
+ .btn:hover {{ filter: brightness(1.03) saturate(1.04); transform: translateY(-1px); }}
271
 
272
+ .footer {{ text-align:center; color:#3f4a5a; font-size:0.96em; margin-top: 8px; }}
273
  .footer hr {{ margin: 12px 0; border-color: rgba(0,0,0,.08); }}
274
  </style>
275
  """, unsafe_allow_html=True)
276
 
277
+ # ========= HERO (centered) =========
278
+ hero_html = img_tag(hero["logo"], "Company Logo")
279
  st.markdown(
280
  f"""
281
+ <div class="hero">
282
+ {hero_html}
283
+ <div class="tagline">{escape(hero["tagline"])}</div>
284
  </div>
285
  """,
286
  unsafe_allow_html=True,
287
  )
288
 
289
+ # ========= Cards =========
 
 
 
290
  def build_card_vars(style: Dict[str, Any]) -> str:
291
  s = []
292
  if "width" in style: s.append(f"--c-w:{int(style['width'])}px")
 
301
  if "title_fs" in style: s.append(f"--c-title-fs:{int(style['title_fs'])}px")
302
  if "blurb_color" in style: s.append(f"--c-blurb-color:{style['blurb_color']}")
303
  if "blurb_fs" in style: s.append(f"--c-blurb-fs:{int(style['blurb_fs'])}px")
 
 
 
 
 
 
 
 
 
 
 
304
  return "; ".join(s)
305
 
306
  def app_card(card_cfg: Dict[str, Any]) -> str:
307
  style = card_cfg.get("style", {})
308
  vars_inline = build_card_vars(style)
309
+ icon_html = img_tag(card_cfg.get("icon"), "logo", cls="logo-img") if card_cfg.get("icon") and card_cfg["icon"].exists() else ""
310
  target = "_self"
311
  return (
312
  f"<div class='card' style='{vars_inline}'>"
313
+ + f"{icon_html}"
314
  + f"<h3>{escape(card_cfg['title'])}</h3>"
315
  + f"<p>{escape(card_cfg['blurb'])}</p>"
316
+ + f"<a class='btn' href='{escape(card_cfg.get('url', '#'))}' target='{target}' rel='noopener'>Explore</a>"
317
  + "</div>"
318
  )
319