Wednesday, 15 April 2026

Film Review: Black Mirror Series Seven (Part 2/3)

This next episode, the rather pretentiously titled Bête Noire, deals with gaslighting. Tech? Well, there's plenty. Maybe even too much.

The Premise

Maria starts to question her reality when ex-schoolmate, Verity, joins her company Ditta (which had a brief mention in the previous episode; fun fact!). She soon finds out that reality is being manipulated with the intention of driving her to suicide.

The Characters

Sienna Kelley is Maria Skinner. She delivers one hell of a portrayal of an argumentative control freak who always needs to be right, the not-so-reformed bully who instinctively reverts to form once the conditions present themselves. It's sometimes subtle, but Kelley pulls it off. It helps that the argumentative control freak personality is absolutely a thing, and I know more than a few people like that.

Rosy McEwen is Verity Greene. It's also a compelling performance from her, perhaps even more so considering the rather unbelievable premise. She's a victim of bullying who finds near limitless power but decides to spend it tormenting her former bullies. I find this thoroughly unrelatable because in her shoes, I'd be busy exploring so many possibilities and cementing my position. Despite this, McEwen manages to elicit sympathy.

Ben Baily Smith as Gabe, the boss. I found his portrayal as a laid-back hill boss, utterly watchable. Gabe bikes to work and comes off as chummy and sensitive, and tries to be reasonable and tolerant. Which can be tough if you have a pushy mofo like Maria as a subordinate. Just saying!

Michael Workéyè plays Kae, Maria's boyfriend. He's nice and goofy, and really quite the emotionally available guy. Another guy who tries to be reasonable with Maria and puts up with her bullshit. I'm starting to see a pattern!

Ben Ashenden as Nick, from the Graphics department. Comes across as enthusiastically friendly where Verity's concerned. Perhaps even over-friendly. Later on, he pushes Maria's buttons in very annoying ways. Overall, a rather immature character, but the actor looked like he had fun.

Elena Sanz as Camille, focus group tester. She reminded me a little of Gemma from M3GAN. Thought she'd have a bigger role here, but it was not to be.

Hannah Griffiths as Luisa. There's this running gag where people constantly steal her almond milk from the fridge. It druves her nuts, and ties into the story.

Amber Grappy as Yudy, the kitchen head. She always has this confused look. I don't know if that's by design.

Ravi Aujla has a brief appearance as Ditta. Good-looking distinguished silver fox guy. Not really interesting otherwise.

The Mood

The episode is bright and colorful, and the closeup visuals of chocolate and confections sure add to the artistry. Later on, this does not change, though the mood takes a turn for the sinister, which somehow gets worse considering everything is so visually... cheerful. In effect, the entire episode looks very polished visually.

What I liked

The storyline concept was pretty creative, even if it strained credulity at times. The themes of bullying, gaslighting and arrested development are pretty relevant and dare I say, timeless.

I like the visuals where they tell us what day of the week it is. It's just so artistic.


The actual gaslighting was pretty good! It started off subtle, with this foreshadowing shot that it was Barney's rather than Bernies... also, there are apparently two versions of this episode being aired, with this as a gag!


... to something like this, removing nut allergies from existence! And using Google to reinforce it, is just too precious!




What I didn't

Of all the titles they could've gone with that would have actually made sense, "Bête Noire" doesn't exactly stand out as a solid choice.

Unlikeable characters. Both the protagonist and the antagonist are anything but likeable, and that's even before the reveal at the end that Maria was Verity's bully. Maria is pushy and argumentative. Verity comes off as a tragic victim of bullying who's unable to move past the trauma and as such is in a state of arrested development. No main character comes even close to being sympathetic here.


Unbelievable tech. I mean... something as limitless as the tech that Verity is using, basically runs on what looks like a mini server farm? I suppose it's marginally more believable than that little "quamputer" we saw in Joan Is Awful.

The scene where Verity alters reality so that Maria has always spoken "Chinese". Honestly, if she's "always spoken Chinese", she should speak it a lot better than the garbage gibberish that came out of Maria's mouth. Badly-spoken Mandarin has always been a pet peeve of mine in Hollywood. Couldn't they have used Japanese, or Korean? Something arguably less easy to get wrong? Geez!

Conclusion

A mixed bag. It was a good gaslighting-style story with just enough corniness to make it enjoyable. And even with the rubbish they tried to pass off as Mandarin, the good outweighed the bad here. Solid episode.

My Rating

7.5 / 10

Next

Hotel Reverie

Monday, 13 April 2026

Film Review: Black Mirror Series Seven (Part 1/3)

Black Mirror Series Seven has arrived! For those not in the know, Black Mirror is an antholotgcal series revolving around tech and media, and its sometimes negative effects on life.

Will Black Mirror Series Seven be even more of a disappointment than its predecessor, or will this offering be a worthy inclusion in the exalted halls of the Black Mirror franchise? That's a question for the ages.


No time like the present - let's dive in and see what we're getting this time.

Warning - not safe for children! 

Bad language, sexual themes, the works. Also, spoilers.

The first episode is Common People.

The Premise

Mike and Amanda are a married couple who find themselves in financial dire straits when Amanda develops a severe medical condition which can be alleviated via means of a new technological breakthrough. They manage at first, but things get progressively worse as services degrade and the costs of maintaining the service keep rising to unsustainable levels.

The Characters

Chris O'Dowd as Mike Waters. He has this very goofy puppy-dog expression that really sells it - the horror of what he has to do to take care of his wife, the frustration at the escalating costs, the weariness at the neverending shitshow. It's all there, and I felt every bit of it. This guy didn't look like a hero, but damn if Chris O'Dowd didn't bring that Everyman Superman vibe.

Rashida Jones as his wife Amanda. She portrays a woman who, through no fault of her own, becomes a walking talking billboard. I really did enjoy the instances where she started spouting ads, so naturally. It was a remarkable performance. One moment she's a caring and empathetic schoolteacher, and next moment she's a shell of her former self, though we see this in progressive stages.

Tracee Ellis Ross as Gaynor, Rivermind's sales rep. I actually thoguht thatbwas quite a nuanced performance. She first appeared as a sympathetic voice of hope, later on appearing still sympathetic even as she started tightening the screws, and at the end of it she was a soulless corporate shill, complete with shoulder-padded blazers and all. The way she delivers the line "pregnancy costs extra", is just diabolical. It's so good!

Nicholas Cirillo is Shane. This guy played the kind of asshole one loves to hate. At first he was just an irresponsible jackass, until somewhere in the third quarter of the episode he goes full douchebag. It was a one-note character but I have to give credit where it's due - his portrayal made me cheer when the character got his comeuppance.


Lisa Gilroy makes an appearance as the unnamed woman in the Rivermind Lux ad. It was delightfully corny and she looked like she was having a ball.

Sabrina Jalees as Angie. Seems like a pointless casting. I suspect her role was originally bigger than just a few throwaway lines. But if they cut her participation, that much, they should have just gone all the way and excised her completely from this episode. WHat did her character do, really? A big fat nothing, that's what.

Donald Sales as Kyle, the foreman. Almost another pointless casting.

Carolyn Taylor as the middle-aged principal Penelope, Amanda's boss. Not a big role but Taylor nailed it as the otherwise sympathetic boss who's sensitive about her middle-aged spinster status.

Lucy Turnbull as Eva. She comes across as emotionally vulnerable, like most kids, but does very little else that is plot-relevant.

Huxley Fisher as Oscar. This kid is something else. In the limited screen time he had, his judgey stare while talking to Amanda had me in stitches. I don't know if that was intentional.

The Mood

It's a picture of domestic bliss at the start, and soon gets bleak and depressing, vintage Black Mirror. The mood rarely shifts. It's constantly slow-moving and dreary, forcing the viewer to face the inexorably miserable ending that is the eventuality.

What I liked

There's nothing sinister going on, just the combination of technology, corporate greed and human sadism. In short, tech and human beings being horrible to each other. Love it! Did I mention I adore the story? Throughout it all, Mike and Amanda never give up until they absolutely have to. It's awesome and heartbreaking at the same goddamn time. Even without any real shocking twists, this episode delivers.


Using Dum Dummies as the platform on which Mike debases himself in order to raise funds, seemed to be a clever parody of the platform known as OnlyFans, or perhaps other livestreaming platforms.


And speaking of Dum Dummies, there was an amusing callback to a previous episode, The Waldo Moment.

The dynamic between Mike and Amanda was really sweet and funny at the same time. I especially like how they refer to sex as a "happy accident".

That ending was so deliciously downer! Oh man. It looks like we find out just how Mike paid for that last thirty minutes' worth of Rivermind Lux... with his life!


Gaynor using the app to increase her nonchalance level to max, in order to deal with Mike's angry outburst, was such black humor.

What I didn't

I could have done without that bit about Amanda really enjoying the sex under the influence of the Rivermind Lux package, to be honest.

The tech being able to transfer language and motor skills. Like, OK, that bit was overkill and didn't really add anything to the plot. Also, things like parkour are as much physical as they are mental, if not more so. So this bit made no goddamn sense.

Conclusion

Just one word - wow. What. An. Episode. Not only was it quintessential Black Mirror, it had at its core, a story of true, enduring love. There's that bleak ending, the midway twists, and the absurdly dark humor. This episode had it all, and it was the first episode. Honestly, how is the rest of the series going to top this?!

My Rating

9.5 / 10

Next

Bête Noire

Wednesday, 8 April 2026

In defense of small steps

It was 1997. I was a Temasek Polytechnic student waiting to go into National Service. While hanging out at a running track (imagine, in those days I actually had knees that didn't creak.) I bumped into a schoolmate also waiting for National Service.

He told me he had been preparing for National Service by running three rounds around the running track every day. Three. That was 1.2 kilometers. I was baffled. I laughed in his face and said he might as well not bother. This was a pitifully short distance. What good would it do?

Running in preparation.

I know, I know, I was a jerk. And if I ever see him again, even if he's forgotten all about it, I really should offer him an apology. I'm a kinder, more evolved individual now and all. More importantly, I've spent decades as a web developer and it's not just compassion behind the apology - it's professional clarity.

The lesson behind this

The story here isn't about me saying stupid, needlessly cruel things like some empty-headed moron. I was nineteen, FFS. Saying dumb shit was par for the course.

No, the story is that, in my youthful arrogance, I was blind to the simple fact that everything begins with small steps. Everything. And years later, life would beat that lesson into me over and over. I would attempt a tech career in sunny Singapore. I would stumble repeatedly. Sometimes I would take a few steps forward only to be shunted one step back.

In many ways, life was not kind. It could have been worse, of course. A lot worse. But the fact remains that there were challenges. Challenges that weren't going to be navigated in a single bound. I was dismissed from jobs. Failed probationary periods. Screwed up at work. Joined companies that crashed and burned. Worked for exploitative bosses with the ethics of a swamp leech.

But you know what? It was OK because my failures were due to me actually trying. That was valuable. The thing about freedom, and having agency, is that you own all your success and your failures.

Moving slowly.

The thing is, I wasn't shooting up the corporate ladder. Sometimes I wasn't anywhere near any kind of ladder. It took a few (OK, several) iterations before my career got any kind of traction, and even then, it was a slow grind.

In retrospect, my career was like how software is typically written - iteratively. "Do it once, do it good" is music to the ears. Unfortunately, that's all it often is. The ugly reality is that software evolves. You start off with an MVP that gets fleshed out over time, and requirements are going to change. That's business. That's life, even.

What I know now

A new appreciation for small steps is one of the things I've gained. It's not about how big the step is. It's about the fact that the step is being taken at all. It's a statement of intent, and this can be powerful. Without even a small step in the right direction, you ain't getting anywhere, son.

Pretty much like my ex-schoolmate and his 1.2 kilometer run. A tiny step? Yes. Very tiny. But no less important. Because it was the intention that counted. And later on, whatever success he enjoyed in that area should be attributed to that intention.

It wasn't like he simply went on Social Media and talked about doing it without actually doing it. No, he was doing it when I encountered him. And what did I do? Minimized his efforts like a jackass, that's what.

Wishing for software.

I've come to understand the need to do things in iterations. Small incremental steps if necessary. And it's often necessary. Software isn't simply conjured out of thin air after verbalizing a wish, no matter what the likes of Jensen Huang or Elon Musk would have you believe. It follows a process of repeated refinement. A lot of eventual success is predicated upon simply showing up to do the work, and showing up consistently.

This has also manifested in my reading habits, and even my consumption of media. Before, I always felt the need to watch YouTube videos to the end. Finish the book I borrowed from the library. If I started on a TV series, I would binge it to the end.

No longer.

Now I understand that reading a book to the end, for example, means very little. It doesn't actually mean that I'm finished with it. I can always gain new insights from rereading it. Conversely, if it's not worth my time the first round, I can feel free to cut it loose without wasting any more time on it. Perhaps one day I'll find the motivation to get back to it... and if not, it's not a big deal. Basically, I no longer feel the pressure to finish things I started. Because the "finish" is ultimately artificial.

Just like software. Software is never "finished". It's only ready for shipping, but it will evolve in its lifetime.

Small conclusions

Be comfortable with small beginnings. Showing up is more important than showing up loud. "Go big or go home" is ego and stupidity talking, nothing more.

One small step for man...
T___T

Saturday, 4 April 2026

Web Tutorial: Easter Animation 2026

It's that time of the year again. Easter arrives tomorrow, and I'd like to herald its coming with a HTML animation.

This one uses no images. Just good old CSS sleight-of-hand. For the animation, we'll use jQuery UI. Is it absolutely necessary? No, but I feel like it.

The starting HTML looks like this. We have a div, id container. Below it, we'll have a h1 tag. All divs are set to have red outlines. Trust me, we'll need it.
<!DOCTYPE html>
<html>
  <head>
    <title>Easter 2026</title>

    <style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }        
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div id="container">

    </div>

    <h1>HAPPY EASTER</h1>
  </body>
</html>


Here, we define h1 with some aesthetics. container is set to a small size, centered in the screen via use of margin properties. Round corners and all, because it's cute. I also gave it a thick orange border.
<!DOCTYPE html>
<html>
  <head>
    <title>Easter 2026</title>

    <style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }  

      h1
      {
        font-family: verdana;
        text-align: center;
        color: rgb(255, 200, 0);
      }

      #container
      {
        margin: 100px auto 0 auto;
        border-radius: 50px;
        border: 5px solid rgb(255, 200, 0);
        width: 300px;
        height: 200px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div id="container">

    </div>

    <h1>HAPPY EASTER</h1>
  </body>
</html>


In container, we have two divs - ids aura and hills.
<div id="container">
      <div id="aura">

      </div>  

      <div id="hills">

      </div>
</div>


In hills, we have five other divs. The first three are styled using the CSS class hill. The fourth and fifth have the ids cross and shadow. They contain the HTML crucfix symbol.
<div id="container">
      <div id="aura" class="unrotated">

      </div>  

      <div id="hills">
            <div class="hill"></div>
            <div class="hill"></div>
            <div class="hill"></div>
            <div id="cross">&#10013;</div>
            <div id="shadow">&#10013;</div>
      </div>
</div>


Here's some styling. We want aura to overlap the entirety of container. I've made it bigger than container's width and height, offset the margins accordingly, and turned it into a circle using the border-radius property. This isn't strictly necessary, but we're going to rotate it later, and it's easier to gauge if aura will still cover container that way. As for hills, we want it to cover container's full width but only the bottom half. So I've set the margin-top property accordingly. I've set position to relative and z-index to a positive number to ensure that hills always stays on top of aura in that order. This will be very relevant later.
<style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }

      h1
      {
        font-family: verdana;
        text-align: center;
        color: rgb(255, 200, 0);
      }

      #container
      {
        margin: 100px auto 0 auto;
        border-radius: 50px;
        border: 5px solid rgb(255, 200, 0);
        width: 300px;
        height: 200px;
      }
    
      #aura
      {
        width: 400px;
        height: 400px;
        margin-top: -100px;
        margin-left: -50px;
        border-radius: 50%;
      }

      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
       position: relative;
       z-index: 2;
      }

</style>


So far, it's all here.


Now, what happens if we style hill this way? All 150 pixel squares with rounded corners, and rotated 45 degrees.
      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
       position: relative;
       z-index: 2;
      }

      .hill
      {
        width: 150px;
        height: 150px;
        border-radius: 10px;
        background-color: rgb(0, 0, 0);
        transform-origin: 50% 50%;
        transform: rotate(45deg);
      }

</style>


This is what you should have.


We use the nth-of-type pseudoselector because each of these will now have their own properties at this point.
      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
        position: relative;
        z-index: 2;
      }

      .hill
      {
        width: 150px;
        height: 150px;
        border-radius: 10px;
        background-color: rgb(0, 0, 0);
        transform-origin: 50% 50%;
        transform: rotate(45deg);
      }

      .hill:nth-of-type(1)
      {

      }

      .hill:nth-of-type(2)
      {

      }

      .hill:nth-of-type(3)
      {

      }

</style>


The first hill gets moved 50 pixels left.
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{

}

.hill:nth-of-type(3)
{

}


So far so good.


We move the middle "hill" up and to the right. It should placed higher than the other two.
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{
  margin-top: -180px;
  margin-left: 75px;

}

.hill:nth-of-type(3)
{

}


See this?


Finally, we use margin-top and margin-left properties to move the last "hill".
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{
  margin-top: -180px;
  margin-left: 75px;
}

.hill:nth-of-type(3)
{
  margin-top: -120px;
  margin-left: 200px;

}


Yep. It's coming along nicely.


What's next? The cross, of course! For cross, ensure that it's a large font size. margin-top is the property that moves the div vertically upwards while text-align is set to center so that it rests on top of the middle "hill".
  .hill:nth-of-type(3)
  {
    margin-top: -120px;
    margin-left: 200px;
  }  

  #cross
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -270px;
    text-align: center;
  }   
       
</style>


See? There's the cross. Where's shadow? Look to the left "hill", it's hiding there.


What we do here is repeat the styling for shadow, but with a slightly different margin-top property.
  #cross
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -270px;
    text-align: center;
  }

  #shadow
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -100px;
    text-align: center;
  }       
 
</style>


Now shadow overlaps cross, but not perfectly. So the cross beam looks thicker. But that's not a problem. The animation later will take care of this.


For aura, we want the background to have spokes radiating from the center. So that means it's going to be a repeating conic gradient of yellow and orange.
#aura
{
  width: 400px;
  height: 400px;
  margin-top: -100px;
  margin-left: -50px;
  border-radius: 50%;
  background-image: repeating-conic-gradient(rgb(255, 255, 0) 5deg 10deg, rgb(255, 200, 0) 15deg 20deg, rgb(255, 255, 0) 25deg 30deg);
}


Now would you look at that!


At this point, remove the red outlines. And set the overflow property of container to hidden.
div
{
  border: 0px solid rgb(255, 0, 0);
}

h1
{
  font-family: verdana;
  text-align: center;
  color: rgb(255, 200, 0);
}

#container
{
  margin: 100px auto 0 auto;
  border-radius: 50px;
  border: 5px solid rgb(255, 200, 0);
  width: 300px;
  height: 200px;
  overflow: hidden;
}


Oh, this is looking so good.


Time to animate!

Add the classes unrotated and rotated. We want aura to rotate, see? So unrotated is the start state and rotated is the end state. I also use the transition property in both cases, to determine duration and style. Not a big issue, carry on.
#aura
{
  width: 400px;
  height: 400px;
  margin-top: -100px;
  margin-left: -50px;
  border-radius: 50%;
  background-image: repeating-conic-gradient(rgb(255, 255, 0) 5deg 10deg, rgb(255, 200, 0) 15deg 20deg, rgb(255, 255, 0) 25deg 30deg);
}

.unrotated
{
  transform: rotate(0deg);
  transition: 10s ease;
}

.rotated
{
  transform: rotate(360deg);      
  transition: 10s ease;
}


#hills
{
  width: 300px;
  height: 100px;
  margin-top: -120px;
  position: relative;
  z-index: 2;
}


What we'll want to do here is set the class of aura to unrotated.
<div id="aura" class="unrotated">
  
</div>  


Now use the toggleClass() method on aura.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
  });
</script>


And then use jQuery UI's "puff" effect on shadow. We'll set the animation to last 1 second, and specify that the end size of shadow will be 250% of the original.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
    $("#shadow").effect( "puff", {percent: 250}, 1000 );
  });
</script>


See the effect! It looks like the crucifix with a fading black aura amid the rotating spokes.


And if you want to make this continuous, put that in a setInterval() function.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
    $("#shadow").effect("puff", {percent: 250}, 1000 );

    setInterval
    (
      function()
      {
        $("#aura").toggleClass("rotated");
        $("#shadow").effect("puff", {percent: 250}, 1000 );
      },
      10000
    );

  });
</script>


Auran't you glad it's Easter?
T___T

Sunday, 29 March 2026

Web Tutorial: Chuck Norris Memorial

A legend has left us. On the 19th of this month, one Chuck Norris, martial artist and action movie icon, passed away. One of the things that really stood out in the Chuck Norris mythos was... well, the Chuck Norris mythos. Remember back in the 2000s, how popular "Chuck Norris facts" became?

Well, today, in loving ass-kicking memory, we'll do something like this! It'll be a page that returns a random Chuck Norris fact each time. But this is a tech blog, so the fact has to be tech-based! And this is an image that we'll be using, which I generated using MetaAI.

chucknorris.jpg

Let's begin by creating a PHP page. We'll deal with the HTML portion first. We'll also use some jQuery UI to create nice animations. Note that in the body, we have div tags styled using the CSS classes number, fact and rip
<!DOCTYPE html>
<html>
  <head>
    <title>In Memory of Chuck Norris</title>

    <style>
  
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="number"></div>
    <br />
    <div class="fact"></div>
    <div class="rip">R.I.P 19<sup>th</sup> March 2026</div>
  </body>
</html>


Let's add some PHP. This currently is just one line, declaring fact as a string. The value is one of my favorite Chuck Norris "facts". In the div styled using the CSS class fact, display the value of fact. And in the div styled using the CSS class number, let's have a random number to humorously display which number this "fact" is supposed to be.
<?php
  $fact = "Chuck Norris can divide by zero";
?>


<!DOCTYPE html>
<html>
  <head>
    <title>In Memory of Chuck Norris</title>

    <style>
  
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="number">Fact #<?php echo rand(100, 100000); ?>:</div>
    <br />
    <div class="fact"><?php echo $fact; ?></div>
    <div class="rip">R.I.P 19<sup>th</sup> March 2026</div>
  </body>
</html>


This is just text right now.


Now let's style the body tag. We first want to use the image as a background.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
  }  

</style>


Then we want to style the text. I made it large font, with a white outline using the text-shadow property.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
    font-size: 60px;
    font-family: georgia;
    text-shadow: -2px -2px 2px rgb(255, 255, 255), 2px -2px 2px rgb(255, 255, 255), -2px 2px 2px rgb(255, 255, 255), 2px 2px 2px rgb(255, 255, 255);

  }  
</style>


Nice contrast, eh?


Now let's focus on the CSS classes number, fact and rip. It's mostly positional. I made number bolder and floated it left. fact is also floated left. rip has position property set to fixed and is anchored to the bottom right of the screen via the right and bottom properties. I'ave also adjusted the font size.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
    font-size: 60px;
    font-family: georgia;
    text-shadow: -2px -2px 2px rgb(255, 255, 255), 2px -2px 2px rgb(255, 255, 255), -2px 2px 2px rgb(255, 255, 255), 2px 2px 2px rgb(255, 255, 255);
  }

  .number
  {
    font-weight: bold;
    width: 10em;
    float: left;
  }

  .fact
  {
    width: 20em;
    float: left;
  }

  .rip
  {
    font-size: 0.5em;
    height: 1.5em;
    position: fixed;
    bottom: 0;
    right: 0;
  }    
  
</style>


See what I mean?


OK, next up... animation! For this, we want to set the display property of the fact CSS class, to none. This effectively hides the "fact". That's because we want to use jQuery to make it fade in.
.fact
{
  width: 20em;
  float: left;
  display: none;
}


In the script tag, do this so that the code only runs once the HTML is loaded.
<script>
  $(document).ready(function() {

  });

</script>


This causes the "fact" to fade in over the course of 5 seconds.
<script>
  $(document).ready(function() {
    $(".fact").fadeIn(5000);
  });
</script>


Then we use the effect() method on this element. The "bounce" effect belongs to jQuery UI, and here we specify a 1 second duration.
<script>
  $(document).ready(function() {
    $(".number").effect("bounce", 1000);
    $(".fact").fadeIn(5000);
  });
</script>


We include an object with the times property set to 5, so that it bounces 5 times in 1 second. (Sounds like a really lousy credit card, but there you go.)
<script>
  $(document).ready(function() {
    $(".number").effect("bounce", {times: 5}, 1000);
    $(".fact").fadeIn(5000);
  });
</script>


See how the "fact" fades in as the "number" bounces!


Now for the most exciting part... leveraging on OpenAI's ChatGPT to generate a random Chuck Norris "fact". For this, we're leveraging on ChatGPT's API. First, declare key, org and url. These should already have been set up as a new project in ChatGPT. Then create headers as an array of strings. This is what we'll be sending to the URL defined at url.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];


  $fact = "Chuck Norris can divide by zero";
?>


We then construct the prompt to send. Here. I specify the JSON object that ChatGPT should give me, and explicitly specify the value. I want a Chuck Norris "fact", and I also want it to be tech-related. Because those are the ones I love. That's for content. role is set to "user". All this is in the array, obj, which is in turn part of messages.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;


  $fact = "Chuck Norris can divide by zero";
?>


Then we create the parent, data. Here we specify the model. Then we set messages, and max_tokens. This one won't be text-heavy. I reckon 500 tokens should be enough.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;


  $fact = "Chuck Norris can divide by zero";
?>


And here's the final use of cURL, to send data to the API endpoint.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $curl = curl_init($url);
  curl_setopt($curl, CURLOPT_POST, 1);
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

  $result = curl_exec($curl);
  if (curl_errno($curl))
  {
   echo 'Error:' . curl_error($curl);
  }

  curl_close($curl);  


  $fact = "Chuck Norris can divide by zero";
?>


We grab the response and extract the required value. And then we change fact's value from a hard-coded string, to that extracted value.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $curl = curl_init($url);
  curl_setopt($curl, CURLOPT_POST, 1);
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

  $result = curl_exec($curl);
  if (curl_errno($curl))
  {
   echo 'Error:' . curl_error($curl);
  }

  curl_close($curl);  

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $result = json_decode($result);
  $content = $result->choices[0]->message->content;
  $content = json_decode($content);


  $fact = $content->fact;  
?>


See? The facts change now.


Different fact.


Another different fact.


R.I.P, Mr Norris!

Rumour has it that you've been dead for years. Death just hasn't plucked up the courage to tell you.

Did you know that Chuck Norris can delete the Recycle Bin?
T___T