- Contact card in hero emailing inquiries via SMTP (secrets in .env) - Three-step How it works section - Point CTAs at the contact form - Local site port 8082 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>Contact
| @@ -0,0 +1,2 @@ | |||||
| # Copy this file to .env and fill in the real values. .env is never committed. | |||||
| SMTP_PASSWORD=your-mailbox-password | |||||
| @@ -7,3 +7,6 @@ | |||||
| *.log | *.log | ||||
| *.user | *.user | ||||
| *.suo | *.suo | ||||
| # Secrets | |||||
| /.env | |||||
| @@ -6,7 +6,7 @@ A clean Classic ASP application scaffold based on the RouteKit MVC framework. | |||||
| 1. Install IIS Express with Classic ASP enabled. | 1. Install IIS Express with Classic ASP enabled. | ||||
| 2. Run `run_site.cmd`. | 2. Run `run_site.cmd`. | ||||
| 3. Browse to `http://localhost:8080/`. | |||||
| 3. Browse to `http://localhost:8082/`. | |||||
| The landing page is served by `HomeController.Index` through | The landing page is served by `HomeController.Index` through | ||||
| `app/views/Home/landing.asp`. The earlier logo-reference page remains at | `app/views/Home/landing.asp`. The earlier logo-reference page remains at | ||||
| @@ -3,9 +3,21 @@ Class HomeController_Class | |||||
| Public Title | Public Title | ||||
| Public useLayout | Public useLayout | ||||
| ' Contact form state shared with the landing view | |||||
| Private contactErrors | |||||
| Private contactName | |||||
| Private contactEmail | |||||
| Private contactPhone | |||||
| Private contactBusiness | |||||
| Private contactMessage | |||||
| Private contactSent | |||||
| Private Sub Class_Initialize() | Private Sub Class_Initialize() | ||||
| Title = "PostcardMojo Logo Files" | Title = "PostcardMojo Logo Files" | ||||
| useLayout = False | useLayout = False | ||||
| contactErrors = Array() | |||||
| ReDim contactErrors(-1) | |||||
| contactSent = (Request.QueryString("contact") = "sent") | |||||
| End Sub | End Sub | ||||
| Public Sub Index() | Public Sub Index() | ||||
| @@ -13,6 +25,42 @@ Class HomeController_Class | |||||
| <!--#include file="../views/Home/landing.asp" --> | <!--#include file="../views/Home/landing.asp" --> | ||||
| <% | <% | ||||
| End Sub | End Sub | ||||
| ' GetAntiCSRFToken returns an empty value the first time it creates a token, so read it again. | |||||
| Private Function ContactCsrfToken() | |||||
| ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") | |||||
| If Len(ContactCsrfToken) = 0 Then ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") | |||||
| End Function | |||||
| ' POST /contact - validate the inquiry and email the PostcardMojo team. | |||||
| Public Sub Contact() | |||||
| contactName = Trim(Request.Form("name")) | |||||
| contactEmail = Trim(Request.Form("email")) | |||||
| contactPhone = Trim(Request.Form("phone")) | |||||
| contactBusiness = Trim(Request.Form("business")) | |||||
| contactMessage = Trim(Request.Form("message")) | |||||
| ' Honeypot: real visitors never see or fill this field | |||||
| If Len(Request.Form("zq_trap_field")) > 0 Then | |||||
| Response.Redirect "/?contact=sent#contact" | |||||
| End If | |||||
| If Not HTMLSecurity().IsValidAntiCSRFToken("home.contact", Request.Form("csrf_token")) Then | |||||
| contactErrors = Array("Your session expired. Please try again.") | |||||
| Else | |||||
| contactErrors = ContactInquiryService().ValidationErrors(contactName, contactEmail, contactBusiness, contactMessage) | |||||
| End If | |||||
| If UBound(contactErrors) < 0 Then | |||||
| If ContactInquiryService().Notify(contactName, contactEmail, contactPhone, contactBusiness, contactMessage) Then | |||||
| HTMLSecurity().ClearAntiCSRFToken "home.contact" | |||||
| Response.Redirect "/?contact=sent#contact" | |||||
| End If | |||||
| contactErrors = Array("We could not send your message. Please email danielcovington@postcardmojo.com directly.") | |||||
| End If | |||||
| Index | |||||
| End Sub | |||||
| End Class | End Class | ||||
| Dim HomeController_Class__Singleton | Dim HomeController_Class__Singleton | ||||
| @@ -1 +1,3 @@ | |||||
| <!--#include file="../services/EnvService.asp" --> | |||||
| <!--#include file="../services/ContactInquiryService.asp" --> | |||||
| <!--#include file="HomeController.asp" --> | <!--#include file="HomeController.asp" --> | ||||
| @@ -0,0 +1,81 @@ | |||||
| <% | |||||
| ' Validates a visitor inquiry and notifies the PostcardMojo team by email. | |||||
| Class ContactInquiryService_Class | |||||
| Public Function ValidationErrors(name, email, business, message) | |||||
| Dim errs : errs = Array() | |||||
| ReDim errs(-1) | |||||
| If Len(name) = 0 Then AddMessage errs, "Please enter your name." | |||||
| If Len(name) > 100 Then AddMessage errs, "Name must be 100 characters or fewer." | |||||
| If Len(email) = 0 Then | |||||
| AddMessage errs, "Please enter your email address." | |||||
| ElseIf Len(email) > 150 Or Not IsValidEmail(email) Then | |||||
| AddMessage errs, "Please enter a valid email address." | |||||
| End If | |||||
| If Len(business) > 150 Then AddMessage errs, "Business name must be 150 characters or fewer." | |||||
| If Len(message) > 2000 Then AddMessage errs, "Message must be 2000 characters or fewer." | |||||
| ValidationErrors = errs | |||||
| End Function | |||||
| Public Function Notify(name, email, phone, business, message) | |||||
| Dim recipient, smtpServer, smtpPort, smtpUser, body | |||||
| recipient = Setting("ContactRecipient", "covington.daniel@gmail.com") | |||||
| smtpServer = Setting("SmtpServer", "localhost") | |||||
| smtpPort = Setting("SmtpPort", "25") | |||||
| smtpUser = Setting("SmtpUsername", "") | |||||
| body = "New PostcardMojo website inquiry" & vbCrLf & vbCrLf & _ | |||||
| "Name: " & name & vbCrLf & _ | |||||
| "Email: " & email & vbCrLf & _ | |||||
| "Phone: " & phone & vbCrLf & _ | |||||
| "Business: " & business & vbCrLf & vbCrLf & _ | |||||
| "Message:" & vbCrLf & message & vbCrLf & vbCrLf & _ | |||||
| "Received: " & Now() & vbCrLf & _ | |||||
| "IP: " & Request.ServerVariables("REMOTE_ADDR") | |||||
| CDOEmail.From = Setting("SmtpFrom", Choice(Len(smtpUser) > 0, smtpUser, recipient)) | |||||
| CDOEmail.Subject = "PostcardMojo inquiry from " & StripLineBreaks(name) | |||||
| CDOEmail.Body = body | |||||
| CDOEmail.IsBodyHTML = False | |||||
| CDOEmail.SMTPServer = smtpServer | |||||
| CDOEmail.SMTPPort = CLng(smtpPort) | |||||
| CDOEmail.SMTPUsername = smtpUser | |||||
| CDOEmail.SMTPPassword = EnvService().Value("SMTP_PASSWORD", "") | |||||
| CDOEmail.SMTPUseSSL = (LCase(Setting("SmtpUseSsl", "false")) = "true") | |||||
| CDOEmail.AddRecipient "TO", recipient | |||||
| Notify = CDOEmail.Send() | |||||
| End Function | |||||
| Public Function StripLineBreaks(value) | |||||
| StripLineBreaks = Replace(Replace(value, vbCr, " "), vbLf, " ") | |||||
| End Function | |||||
| Private Function Setting(key, fallback) | |||||
| Dim value : value = GetAppSetting(key) | |||||
| If value = "nothing" Or Len(value) = 0 Then value = fallback | |||||
| Setting = value | |||||
| End Function | |||||
| Private Function IsValidEmail(value) | |||||
| Dim re : Set re = New RegExp | |||||
| re.Pattern = "^[^\s@<>,;""]+@[^\s@<>,;""]+\.[A-Za-z]{2,}$" | |||||
| IsValidEmail = re.Test(value) | |||||
| End Function | |||||
| Private Sub AddMessage(ByRef errs, msg) | |||||
| ReDim Preserve errs(UBound(errs) + 1) | |||||
| errs(UBound(errs)) = msg | |||||
| End Sub | |||||
| End Class | |||||
| Dim ContactInquiryService_Class__Singleton | |||||
| Function ContactInquiryService() | |||||
| If IsEmpty(ContactInquiryService_Class__Singleton) Then | |||||
| Set ContactInquiryService_Class__Singleton = New ContactInquiryService_Class | |||||
| End If | |||||
| Set ContactInquiryService = ContactInquiryService_Class__Singleton | |||||
| End Function | |||||
| %> | |||||
| @@ -0,0 +1,56 @@ | |||||
| <% | |||||
| ' Reads KEY=VALUE secrets from the .env file in the project root (outside the | |||||
| ' public web root). Real environment variables take precedence over the file. | |||||
| Class EnvService_Class | |||||
| Private values | |||||
| Private Sub Class_Initialize() | |||||
| Set values = Server.CreateObject("Scripting.Dictionary") | |||||
| values.CompareMode = 1 | |||||
| LoadFile | |||||
| End Sub | |||||
| Public Function Value(key, fallback) | |||||
| Dim shell : Set shell = Server.CreateObject("WScript.Shell") | |||||
| Value = shell.Environment("Process")(key) | |||||
| Set shell = Nothing | |||||
| If Len(Value) > 0 Then Exit Function | |||||
| If values.Exists(key) Then | |||||
| Value = values(key) | |||||
| Else | |||||
| Value = fallback | |||||
| End If | |||||
| End Function | |||||
| Private Sub LoadFile() | |||||
| Dim fso, path, stream, line, pos, key, val | |||||
| Set fso = Server.CreateObject("Scripting.FileSystemObject") | |||||
| path = Server.MapPath("..\.env") | |||||
| If Not fso.FileExists(path) Then Exit Sub | |||||
| Set stream = fso.OpenTextFile(path, 1, False) | |||||
| Do Until stream.AtEndOfStream | |||||
| line = Trim(stream.ReadLine()) | |||||
| If Len(line) > 0 And Left(line, 1) <> "#" Then | |||||
| pos = InStr(line, "=") | |||||
| If pos > 1 Then | |||||
| key = Trim(Left(line, pos - 1)) | |||||
| val = Trim(Mid(line, pos + 1)) | |||||
| If Len(val) > 1 Then | |||||
| If (Left(val, 1) = """" And Right(val, 1) = """") Or (Left(val, 1) = "'" And Right(val, 1) = "'") Then val = Mid(val, 2, Len(val) - 2) | |||||
| End If | |||||
| values(key) = val | |||||
| End If | |||||
| End If | |||||
| Loop | |||||
| stream.Close | |||||
| End Sub | |||||
| End Class | |||||
| Dim EnvService_Class__Singleton | |||||
| Function EnvService() | |||||
| If IsEmpty(EnvService_Class__Singleton) Then | |||||
| Set EnvService_Class__Singleton = New EnvService_Class | |||||
| End If | |||||
| Set EnvService = EnvService_Class__Singleton | |||||
| End Function | |||||
| %> | |||||
| @@ -30,7 +30,7 @@ Response.CodePage = 65001 | |||||
| <a href="#good-fit">Who it is for</a> | <a href="#good-fit">Who it is for</a> | ||||
| <a href="#faq">FAQ</a> | <a href="#faq">FAQ</a> | ||||
| </nav> | </nav> | ||||
| <a class="button button-small button-primary" href="#get-started">Find your spot</a> | |||||
| <a class="button button-small button-primary" href="#contact">Find your spot</a> | |||||
| </div> | </div> | ||||
| </header> | </header> | ||||
| @@ -42,9 +42,9 @@ Response.CodePage = 65001 | |||||
| <div class="hero-copy"> | <div class="hero-copy"> | ||||
| <p class="eyebrow"><span></span> Shared direct mail for local businesses</p> | <p class="eyebrow"><span></span> Shared direct mail for local businesses</p> | ||||
| <h1>Make your business <em>impossible to miss</em> in the mailbox.</h1> | <h1>Make your business <em>impossible to miss</em> in the mailbox.</h1> | ||||
| <p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p> | |||||
| <p class="hero-lede">PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard and is delivered to 5,000 mailboxes. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.</p> | |||||
| <div class="hero-actions"> | <div class="hero-actions"> | ||||
| <a class="button button-primary" href="#get-started">Explore a campaign</a> | |||||
| <a class="button button-primary" href="#contact">Explore a campaign</a> | |||||
| <a class="button button-quiet" href="#how-it-works">See how it works <span aria-hidden="true">→</span></a> | <a class="button button-quiet" href="#how-it-works">See how it works <span aria-hidden="true">→</span></a> | ||||
| </div> | </div> | ||||
| <ul class="hero-points" aria-label="Campaign highlights"> | <ul class="hero-points" aria-label="Campaign highlights"> | ||||
| @@ -61,6 +61,44 @@ Response.CodePage = 65001 | |||||
| </div> | </div> | ||||
| <div class="stage-note"><span aria-hidden="true">↗</span> One card. Multiple businesses. A whole neighborhood of opportunity.</div> | <div class="stage-note"><span aria-hidden="true">↗</span> One card. Multiple businesses. A whole neighborhood of opportunity.</div> | ||||
| </div> | </div> | ||||
| <aside class="contact-card" id="contact" aria-labelledby="contact-title"> | |||||
| <p class="eyebrow eyebrow-on-dark"><span></span> Contact us</p> | |||||
| <h2 id="contact-title">Interested in a spot on the next card?</h2> | |||||
| <% If contactSent Then %> | |||||
| <p class="contact-success" role="status">Thank you! Your message was sent and we will be in touch soon.</p> | |||||
| <% Else %> | |||||
| <form class="contact-form" method="post" action="/contact#contact" novalidate> | |||||
| <% If UBound(contactErrors) >= 0 Then %> | |||||
| <div class="contact-errors" role="alert"> | |||||
| <ul> | |||||
| <% Dim contactErrorIndex : For contactErrorIndex = 0 To UBound(contactErrors) %> | |||||
| <li><%= Server.HTMLEncode(contactErrors(contactErrorIndex)) %></li> | |||||
| <% Next %> | |||||
| </ul> | |||||
| </div> | |||||
| <% End If %> | |||||
| <input type="hidden" name="csrf_token" value="<%= Server.HTMLEncode(ContactCsrfToken()) %>"> | |||||
| <div class="contact-trap" aria-hidden="true"><label>Leave this empty <input type="text" name="zq_trap_field" tabindex="-1" autocomplete="off"></label></div> | |||||
| <label>Name <span aria-hidden="true">*</span> | |||||
| <input type="text" name="name" maxlength="100" required autocomplete="name" value="<%= Server.HTMLEncode(contactName & "") %>"> | |||||
| </label> | |||||
| <label>Email <span aria-hidden="true">*</span> | |||||
| <input type="email" name="email" maxlength="150" required autocomplete="email" value="<%= Server.HTMLEncode(contactEmail & "") %>"> | |||||
| </label> | |||||
| <label>Phone | |||||
| <input type="tel" name="phone" maxlength="40" autocomplete="tel" value="<%= Server.HTMLEncode(contactPhone & "") %>"> | |||||
| </label> | |||||
| <label>Business | |||||
| <input type="text" name="business" maxlength="150" autocomplete="organization" value="<%= Server.HTMLEncode(contactBusiness & "") %>"> | |||||
| </label> | |||||
| <label class="contact-wide">How can we help? | |||||
| <textarea name="message" rows="3" maxlength="2000"><%= Server.HTMLEncode(contactMessage & "") %></textarea> | |||||
| </label> | |||||
| <button class="button button-primary" type="submit">Send my interest</button> | |||||
| </form> | |||||
| <% End If %> | |||||
| </aside> | |||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| @@ -77,33 +115,27 @@ Response.CodePage = 65001 | |||||
| <div class="shell"> | <div class="shell"> | ||||
| <div class="section-heading heading-centered"> | <div class="section-heading heading-centered"> | ||||
| <p class="eyebrow eyebrow-centered"><span></span> Simple by design</p> | <p class="eyebrow eyebrow-centered"><span></span> Simple by design</p> | ||||
| <h2>From your offer to their mailbox.</h2> | |||||
| <p>PostcardMojo turns a group of complementary local businesses into one well-organized, high-visibility mail piece.</p> | |||||
| <h2>Three steps to the mailbox.</h2> | |||||
| <p>Pick your size, give us your ad, and we handle the printing and mailing.</p> | |||||
| </div> | </div> | ||||
| <ol class="steps"> | <ol class="steps"> | ||||
| <li> | <li> | ||||
| <span class="step-number">01</span> | <span class="step-number">01</span> | ||||
| <div class="step-icon" aria-hidden="true">◎</div> | <div class="step-icon" aria-hidden="true">◎</div> | ||||
| <h3>Choose your campaign</h3> | |||||
| <p>Start with the local area and timing that fit your business goals.</p> | |||||
| <h3>Choose your ad size</h3> | |||||
| <p>Pick a 3×4 or a 6×8 space on the shared 9×12 postcard.</p> | |||||
| </li> | </li> | ||||
| <li> | <li> | ||||
| <span class="step-number">02</span> | <span class="step-number">02</span> | ||||
| <div class="step-icon" aria-hidden="true">◇</div> | |||||
| <h3>Claim your category</h3> | |||||
| <p>Reserve a space for your type of business, subject to campaign availability.</p> | |||||
| </li> | |||||
| <li> | |||||
| <span class="step-number">03</span> | |||||
| <div class="step-icon" aria-hidden="true">✦</div> | <div class="step-icon" aria-hidden="true">✦</div> | ||||
| <h3>Shape the offer</h3> | |||||
| <p>Bring your logo, message, and promotion together in a clear, focused ad.</p> | |||||
| <h3>Send your layout or let us design it</h3> | |||||
| <p>Already have an ad? Send it over. Need one? We will design it for you.</p> | |||||
| </li> | </li> | ||||
| <li> | <li> | ||||
| <span class="step-number">04</span> | |||||
| <span class="step-number">03</span> | |||||
| <div class="step-icon" aria-hidden="true">↗</div> | <div class="step-icon" aria-hidden="true">↗</div> | ||||
| <h3>Reach the mailbox</h3> | |||||
| <p>The finished postcard is printed and mailed as one coordinated campaign.</p> | |||||
| <h3>We mail it out</h3> | |||||
| <p>We print the postcard and deliver it to mailboxes in your area.</p> | |||||
| </li> | </li> | ||||
| </ol> | </ol> | ||||
| </div> | </div> | ||||
| @@ -235,7 +267,7 @@ Response.CodePage = 65001 | |||||
| <p>Tell us what you do and where you want to be seen. We will help you explore the right PostcardMojo campaign.</p> | <p>Tell us what you do and where you want to be seen. We will help you explore the right PostcardMojo campaign.</p> | ||||
| </div> | </div> | ||||
| <div class="cta-actions"> | <div class="cta-actions"> | ||||
| <a class="button button-gold" href="mailto:info@postcardmojo.com?subject=PostcardMojo%209x12%20campaign%20inquiry">Request campaign details</a> | |||||
| <a class="button button-gold" href="#contact">Request campaign details</a> | |||||
| <span>Campaign availability varies by area.</span> | <span>Campaign availability varies by area.</span> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -251,7 +283,7 @@ Response.CodePage = 65001 | |||||
| <nav aria-label="Footer navigation"> | <nav aria-label="Footer navigation"> | ||||
| <a href="#how-it-works">How it works</a> | <a href="#how-it-works">How it works</a> | ||||
| <a href="#faq">FAQ</a> | <a href="#faq">FAQ</a> | ||||
| <a href="mailto:info@postcardmojo.com">Contact</a> | |||||
| <a href="#contact">Contact</a> | |||||
| </nav> | </nav> | ||||
| <small>© <%= Year(Date()) %> PostcardMojo. All rights reserved.</small> | <small>© <%= Year(Date()) %> PostcardMojo. All rights reserved.</small> | ||||
| </div> | </div> | ||||
| @@ -159,7 +159,7 @@ | |||||
| <virtualDirectory path="/" physicalPath="%ASPC_STARTER_ROOT%public" /> | <virtualDirectory path="/" physicalPath="%ASPC_STARTER_ROOT%public" /> | ||||
| </application> | </application> | ||||
| <bindings> | <bindings> | ||||
| <binding protocol="http" bindingInformation=":8080:localhost" /> | |||||
| <binding protocol="http" bindingInformation=":8082:localhost" /> | |||||
| </bindings> | </bindings> | ||||
| </site> | </site> | ||||
| <siteDefaults> | <siteDefaults> | ||||
| @@ -5,6 +5,7 @@ | |||||
| router.AddRoute "GET", "/", "HomeController", "Index" | router.AddRoute "GET", "/", "HomeController", "Index" | ||||
| router.AddRoute "GET", "", "HomeController", "Index" | router.AddRoute "GET", "", "HomeController", "Index" | ||||
| router.AddRoute "GET", "/home", "HomeController", "Index" | router.AddRoute "GET", "/home", "HomeController", "Index" | ||||
| router.AddRoute "POST", "/contact", "HomeController", "Contact" | |||||
| ' Dispatch the request (resolves route and executes controller action) | ' Dispatch the request (resolves route and executes controller action) | ||||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | ||||
| @@ -570,7 +570,7 @@ summary { | |||||
| .steps { | .steps { | ||||
| position: relative; | position: relative; | ||||
| display: grid; | display: grid; | ||||
| grid-template-columns: repeat(4, 1fr); | |||||
| grid-template-columns: repeat(3, 1fr); | |||||
| gap: 18px; | gap: 18px; | ||||
| margin: 0; | margin: 0; | ||||
| padding: 0; | padding: 0; | ||||
| @@ -1123,7 +1123,6 @@ summary { | |||||
| gap: 30px; | gap: 30px; | ||||
| } | } | ||||
| .steps, | |||||
| .fit-grid { | .fit-grid { | ||||
| grid-template-columns: repeat(2, 1fr); | grid-template-columns: repeat(2, 1fr); | ||||
| } | } | ||||
| @@ -1428,3 +1427,120 @@ summary { | |||||
| transition-duration: 0.01ms !important; | transition-duration: 0.01ms !important; | ||||
| } | } | ||||
| } | } | ||||
| /* Contact card */ | |||||
| .contact-card { | |||||
| position: relative; | |||||
| z-index: 2; | |||||
| padding: clamp(22px, 3vw, 34px); | |||||
| border-radius: 18px; | |||||
| background: var(--navy); | |||||
| color: var(--white); | |||||
| box-shadow: 0 24px 60px rgba(5, 47, 79, 0.28); | |||||
| } | |||||
| .contact-card h2 { | |||||
| margin: 0 0 18px; | |||||
| font-family: var(--font-display); | |||||
| font-size: clamp(1.5rem, 2.4vw, 2rem); | |||||
| line-height: 1.15; | |||||
| } | |||||
| .contact-form { | |||||
| display: grid; | |||||
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |||||
| gap: 14px 16px; | |||||
| } | |||||
| .contact-form label { | |||||
| display: grid; | |||||
| gap: 6px; | |||||
| font-size: 0.9rem; | |||||
| font-weight: 600; | |||||
| } | |||||
| .contact-form input, | |||||
| .contact-form textarea { | |||||
| width: 100%; | |||||
| padding: 11px 12px; | |||||
| border: 1px solid rgba(255, 255, 255, 0.35); | |||||
| border-radius: 8px; | |||||
| background: var(--white); | |||||
| color: var(--ink); | |||||
| font: inherit; | |||||
| } | |||||
| .contact-wide, | |||||
| .contact-errors, | |||||
| .contact-form .button { | |||||
| grid-column: 1 / -1; | |||||
| } | |||||
| .contact-form .button { | |||||
| justify-self: start; | |||||
| } | |||||
| .contact-errors { | |||||
| padding: 10px 14px; | |||||
| border-radius: 8px; | |||||
| background: #fde8e8; | |||||
| color: #8a1c1c; | |||||
| font-size: 0.92rem; | |||||
| } | |||||
| .contact-errors ul { | |||||
| margin: 0; | |||||
| padding-left: 18px; | |||||
| } | |||||
| .contact-success { | |||||
| margin: 0; | |||||
| color: var(--gold); | |||||
| font-weight: 700; | |||||
| } | |||||
| .contact-trap { | |||||
| position: absolute; | |||||
| left: -9999px; | |||||
| } | |||||
| .hero .hero-grid { | |||||
| width: min(1640px, calc(100% - 32px)); | |||||
| grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.6fr) minmax(280px, 0.75fr); | |||||
| grid-template-areas: "copy card contact"; | |||||
| align-items: center; | |||||
| gap: clamp(20px, 2.5vw, 40px); | |||||
| } | |||||
| .hero-grid .hero-copy { grid-area: copy; } | |||||
| .hero-grid .contact-card { grid-area: contact; } | |||||
| .hero-grid .postcard-stage { grid-area: card; min-height: 0; } | |||||
| .hero-grid .postcard { width: 100%; } | |||||
| .hero-grid h1 { font-size: clamp(2rem, 3vw, 3.1rem); } | |||||
| .hero-grid .contact-form { grid-template-columns: 1fr; gap: 12px; } | |||||
| .hero-grid .stage-note { position: static; margin-top: 16px; max-width: none; } | |||||
| @media (max-width: 1100px) { | |||||
| .hero .hero-grid { | |||||
| grid-template-columns: minmax(0, 1fr) minmax(300px, 0.9fr); | |||||
| grid-template-areas: "copy contact" "card card"; | |||||
| align-items: start; | |||||
| } | |||||
| .hero-grid .postcard { width: min(100%, 610px); } | |||||
| .hero-grid .postcard-stage { min-height: 520px; } | |||||
| } | |||||
| @media (max-width: 700px) { | |||||
| .hero .hero-grid { | |||||
| grid-template-columns: 1fr; | |||||
| grid-template-areas: "copy" "contact" "card"; | |||||
| } | |||||
| } | |||||
| @media (max-width: 700px) { | |||||
| .contact-form { | |||||
| grid-template-columns: 1fr; | |||||
| } | |||||
| } | |||||
| @@ -38,6 +38,13 @@ | |||||
| <!-- Cache-bust parameter name (default: "v") --> | <!-- Cache-bust parameter name (default: "v") --> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <!-- Contact form email (the mailbox password comes from SMTP_PASSWORD in the project-root .env file) --> | |||||
| <add key="ContactRecipient" value="covington.daniel@gmail.com" /> | |||||
| <add key="SmtpServer" value="netsol-smtp-oxcs.hostingplatform.com" /> | |||||
| <add key="SmtpPort" value="465" /> | |||||
| <add key="SmtpUsername" value="danielcovington@postcardmojo.com" /> | |||||
| <add key="SmtpUseSsl" value="true" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
Powered by TurnKey Linux.